/* eslint-disable no-undef */
// VisiListen — Editorial theme interactive app
// Screens: list → player → result → admin

const { Icon, CatTag, Spinner, SAMPLE_QUIZZES, SAMPLE_TRANSCRIPT, SAMPLE_QUESTIONS, CAT_MAP, auth, db } = window;
const { useState, useEffect, useRef, useCallback } = React;

// ── Config ────────────────────────────────────────────────────────────────────
const BACKEND_URL = 'https://visilisten-backend-k7uxhwseyq-uc.a.run.app';

// ── Auth ──────────────────────────────────────────────────────────────────────
function useAuth() {
  const [user, setUser] = useState(null);
  useEffect(() => {
    // Handle Google redirect result (mobile linkWithRedirect flow)
    auth.getRedirectResult().then(result => {
      // If result is null, it means no redirect operation is pending.
      // If it's successful, onIdTokenChanged will handle the user update.
    }).catch(async (error) => {
      console.error("Redirect Error:", error);
      if (error.code === 'auth/credential-already-in-use') {
        if (error.credential) {
          try {
            await auth.signInWithCredential(error.credential);
          } catch (e) {
            console.error("Failed to sign in with credential:", e);
            alert("登入失敗請重試：" + e.message);
          }
        } else {
          // If no credential, force a normal sign in
          auth.signInWithRedirect(new firebase.auth.GoogleAuthProvider());
        }
      } else {
        alert("驗證發生錯誤：" + error.message);
      }
    });
    // onIdTokenChanged fires on sign-in AND on account linking (unlike onAuthStateChanged)
    return auth.onIdTokenChanged(u => {
      if (u) {
        setUser(u);
      } else {
        auth.signInAnonymously().catch(console.error);
      }
    });
  }, []);
  return { uid: user?.uid || null, isAnonymous: user?.isAnonymous ?? true, user };
}

// ── User progress ─────────────────────────────────────────────────────────────
function useUserStats(uid) {
  const [stats, setStats] = useState({ done: 0 });
  useEffect(() => {
    if (!uid) return;
    return db.collection('users').doc(uid).onSnapshot(
      snap => {
        const d = snap.data() || {};
        setStats({ done: d.done || 0 });
      },
      () => {}
    );
  }, [uid]);
  return stats;
}

async function saveProgress(uid, quizId, score, total, answers) {
  if (!uid || !quizId) return;
  const userRef = db.collection('users').doc(uid);
  await userRef.collection('completed').doc(quizId).set({
    score, total, answers: answers || null, completed_at: new Date().toISOString(),
  });
}

// ── Completed map ─────────────────────────────────────────────────────────────
function useCompletedMap(uid) {
  const [completedMap, setCompletedMap] = useState(new Map());
  useEffect(() => {
    if (!uid) return;
    return db.collection('users').doc(uid).collection('completed').onSnapshot(
      snap => {
        const m = new Map();
        snap.docs.forEach(d => m.set(d.id, d.data()));
        setCompletedMap(m);
      },
      () => {}
    );
  }, [uid]);
  return completedMap;
}

// ── Routing ───────────────────────────────────────────────────────────────────
function useRoute() {
  const [route, setRoute] = useState({ name: 'list' });
  const go = useCallback((name, params = {}) => setRoute({ name, ...params }), []);
  return [route, go];
}

// ── Dark mode persistence ─────────────────────────────────────────────────────
function useDark() {
  const [dark, setDark] = useState(() => localStorage.getItem('vl-dark') === '1');
  const toggle = () => setDark(d => {
    const next = !d;
    localStorage.setItem('vl-dark', next ? '1' : '0');
    return next;
  });
  return [dark, toggle];
}

// ── Root App ──────────────────────────────────────────────────────────────────
function App() {
  const [route, go] = useRoute();
  const [dark, toggleDark] = useDark();
  const { uid, isAnonymous, user } = useAuth();
  const themeClass = dark ? 't-edit-dark' : 't-edit';

  // Deep-link: #quiz=QUIZ_ID → navigate to player
  useEffect(() => {
    const match = window.location.hash.match(/^#quiz=(.+)$/);
    if (!match) return;
    const quizId = match[1];
    window.history.replaceState(null, '', window.location.pathname);
    db.collection('quizzes').doc(quizId).get().then(snap => {
      if (snap.exists) go('player', { quiz: snap.data() });
    }).catch(() => {});
  }, []);

  return (
    <div className={`app-shell ${themeClass}`}>
      {route.name === 'list'   && <ListPage   go={go} dark={dark} toggleDark={toggleDark} uid={uid} isAnonymous={isAnonymous} user={user}/>}
      {route.name === 'player' && <PlayerPage go={go} quiz={route.quiz}/>}
      {route.name === 'result' && <ResultPage go={go} quiz={route.quiz} answers={route.answers} uid={uid}/>}
      {route.name === 'review' && <ReviewPage go={go} quiz={route.quiz}/>}
      {route.name === 'admin'  && <AdminPage  go={go}/>}
    </div>
  );
}

// ── User Avatar ───────────────────────────────────────────────────────────────
function UserAvatar({ user, isAnonymous, onClick }) {
  const isLinked = !isAnonymous;
  const photo = user?.photoURL || user?.providerData?.[0]?.photoURL;
  const name = user?.displayName || user?.providerData?.[0]?.displayName || '?';

  return (
    <button
      onClick={onClick}
      aria-label="帳號狀態"
      style={{
        width: 34, height: 34, borderRadius: 999,
        border: isLinked ? 'none' : '1px solid var(--line)',
        background: isLinked ? 'var(--primary)' : 'var(--surface)',
        color: isLinked ? 'var(--primary-ink)' : 'var(--ink-3)',
        display: 'grid', placeItems: 'center',
        cursor: 'pointer', position: 'relative',
        overflow: 'hidden', flexShrink: 0,
        fontFamily: 'var(--font-serif)', fontSize: 14, fontWeight: 700,
        transition: 'all .2s',
      }}
    >
      {photo ? (
        <img src={photo} alt="" style={{ width: 34, height: 34, objectFit: 'cover' }}/>
      ) : isLinked ? (
        <span style={{ lineHeight: 1 }}>{name[0]}</span>
      ) : (
        <Icon name="user" size={16}/>
      )}
      {/* Status dot */}
      <div style={{
        position: 'absolute', bottom: 0, right: 0,
        width: 9, height: 9, borderRadius: 999,
        background: isLinked ? 'var(--correct)' : 'var(--surface-2)',
        border: '1.5px solid var(--bg)',
      }}/>
    </button>
  );
}

// ── User Menu ─────────────────────────────────────────────────────────────────
function UserMenu({ user, isAnonymous, onClose }) {
  const [linking, setLinking] = useState(false);
  const isLinked = !isAnonymous;
  const photo = user?.photoURL || user?.providerData?.[0]?.photoURL;
  const name = user?.displayName || user?.providerData?.[0]?.displayName || 'Google 使用者';

  const handleLink = async () => {
    setLinking(true);
    try {
      const provider = new firebase.auth.GoogleAuthProvider();
      try {
        await auth.currentUser.linkWithPopup(provider);
        onClose();
      } catch (error) {
        if (error.code === 'auth/credential-already-in-use' && error.credential) {
          await auth.signInWithCredential(error.credential);
          onClose();
        } else if (error.code === 'auth/popup-blocked' || error.code === 'auth/popup-closed-by-user' || /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent)) {
          // Fallback to redirect flow if popup is blocked/closed or if we are on mobile and popup fails
          await auth.currentUser.linkWithRedirect(provider);
        } else {
          throw error;
        }
      }
    } catch (err) {
      console.error("Link Error:", err);
      setLinking(false);
      alert("無法登入：" + err.message);
    }
  };

  return (
    <div onClick={onClose} style={{ position: 'absolute', inset: 0, zIndex: 20 }}>
      <div
        onClick={e => e.stopPropagation()}
        style={{
          position: 'absolute', top: 62, right: 16,
          background: 'var(--surface)', border: '1px solid var(--line)',
          borderRadius: 8, padding: '16px', width: 230,
          boxShadow: '0 8px 32px rgba(0,0,0,0.14)', zIndex: 21,
        }}
      >
        {isLinked ? (
          <>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
              {photo ? (
                <img src={photo} alt="" style={{ width: 36, height: 36, borderRadius: 999, objectFit: 'cover', flexShrink: 0 }}/>
              ) : (
                <div style={{
                  width: 36, height: 36, borderRadius: 999,
                  background: 'var(--primary)', color: 'var(--primary-ink)',
                  display: 'grid', placeItems: 'center',
                  fontFamily: 'var(--font-serif)', fontSize: 15, fontWeight: 700, flexShrink: 0,
                }}>
                  {name !== 'Google 使用者' ? name[0] : '?'}
                </div>
              )}
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)', marginBottom: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {name}
                </div>
                <div style={{ fontSize: 11, color: 'var(--correct)', fontFamily: 'var(--font-mono)', fontWeight: 600 }}>
                  ● Google 已連結
                </div>
              </div>
            </div>
            <div style={{ fontSize: 11, color: 'var(--ink-3)', lineHeight: 1.5, fontFamily: 'var(--font-mono)', marginBottom: 12 }}>
              進度已跨裝置同步
            </div>
            <button
              onClick={() => { auth.signOut(); onClose(); }}
              className="btn-pill ghost"
              style={{ width: '100%', padding: '10px', fontSize: 13, color: 'var(--wrong)', background: 'var(--wrong-soft)' }}
            >
              登出
            </button>
          </>
        ) : (
          <>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
              <div style={{
                width: 36, height: 36, borderRadius: 999,
                background: 'var(--surface-2)', color: 'var(--ink-3)',
                display: 'grid', placeItems: 'center', flexShrink: 0,
              }}>
                <Icon name="user" size={18}/>
              </div>
              <div>
                <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)' }}>匿名使用中</div>
                <div style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>進度僅存於此裝置</div>
              </div>
            </div>
            <div style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.55, marginBottom: 12 }}>
              連結 Google 帳號可跨裝置同步進度，不影響現有進度。
            </div>
            <button
              onClick={handleLink}
              disabled={linking}
              className="btn-pill primary"
              style={{ width: '100%', padding: '10px', fontSize: 13 }}
            >
              {linking ? <Spinner size={13}/> : <><Icon name="user" size={14}/> 以 Google 帳號登入</>}
            </button>
          </>
        )}
      </div>
    </div>
  );
}

// ── LIST PAGE ─────────────────────────────────────────────────────────────────
function ListPage({ go, dark, toggleDark, uid, isAnonymous, user }) {
  const [cat, setCat] = useState(null);
  const [onlyUnfinished, setOnlyUnfinished] = useState(false);
  const [showAdminGate, setShowAdminGate] = useState(false);
  const [showUserMenu, setShowUserMenu] = useState(false);
  const [allQuizzes, setAllQuizzes] = useState(null);
  const completedMap = useCompletedMap(uid);
  const [quote] = useState(() => {
    const q = window.QUOTES || [];
    return q[Math.floor(Math.random() * q.length)] || 'Keep listening, keep improving.';
  });

  useEffect(() => {
    return db.collection('quizzes').orderBy('created_at', 'desc').onSnapshot(
      snap => setAllQuizzes(snap.docs.map(d => d.data())),
      () => setAllQuizzes([])
    );
  }, []);

  const cats = Object.entries(CAT_MAP).map(([k, v]) => ({ k, ...v }));
  const quizzes = (allQuizzes || [])
    .filter(q => !cat || q.cat === cat)
    .filter(q => !onlyUnfinished || !completedMap.has(q.id));

  return (
    <>
      {/* AppBar */}
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '16px 20px 10px', flexShrink: 0,
      }}>
        <div style={{
          fontFamily: 'var(--font-serif)', fontSize: 26, fontWeight: 700,
          letterSpacing: '-0.02em', color: 'var(--ink)',
        }}>
          VisiListen.
        </div>
        <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
          <UserAvatar user={user} isAnonymous={isAnonymous} onClick={() => setShowUserMenu(v => !v)}/>
          <button className="chrome-btn" onClick={toggleDark} aria-label="切換深色模式">
            <Icon name={dark ? 'sun' : 'moon'} size={16}/>
          </button>
          <button className="chrome-btn" onClick={() => setShowAdminGate(true)} aria-label="Admin">
            <Icon name="key" size={16}/>
          </button>
        </div>
      </div>

      {/* User menu */}
      {showUserMenu && (
        <UserMenu user={user} isAnonymous={isAnonymous} onClose={() => setShowUserMenu(false)}/>
      )}

      {/* Progress card (no streak) */}
      <div style={{ padding: '0 20px 14px', flexShrink: 0 }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 12,
          padding: '14px 16px',
          background: 'var(--surface)', border: '1px solid var(--line)',
          borderRadius: 'var(--radius-card)',
        }}>
          <div style={{
            width: 38, height: 38, borderRadius: 'var(--radius-card)',
            background: 'var(--primary)', color: 'var(--primary-ink)',
            display: 'grid', placeItems: 'center', flexShrink: 0,
          }}>
            <Icon name="headphones" size={18}/>
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{
              fontSize: 14, fontWeight: 600, color: 'var(--ink)',
              fontFamily: 'var(--font-serif)', lineHeight: 1.45,
            }}>
              {quote}
            </div>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', flexShrink: 0, gap: 2 }}>
            <div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--correct)', fontWeight: 600 }}>
              {completedMap.size} 完成
            </div>
            <button
              onClick={() => setOnlyUnfinished(v => !v)}
              style={{
                fontFamily: 'var(--font-mono)', fontSize: 12,
                color: onlyUnfinished ? 'var(--primary)' : 'var(--ink-3)',
                fontWeight: onlyUnfinished ? 700 : 400,
                background: 'none', border: 'none', cursor: 'pointer', padding: 0,
                textDecoration: onlyUnfinished ? 'underline' : 'none',
              }}
            >
              {Math.max(0, (allQuizzes || []).length - completedMap.size)} 未完成
            </button>
          </div>
        </div>
      </div>

      {/* Filter chips */}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, padding: '0 20px 14px', flexShrink: 0 }}>
        {cats.map((c) => (
          <button key={c.k} onClick={() => setCat(cat === c.k ? null : c.k)} style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            padding: '8px 14px', borderRadius: 'var(--radius-btn)',
            border: '1px solid var(--line)',
            background: cat === c.k ? 'var(--ink)' : 'var(--surface)',
            color: cat === c.k ? 'var(--bg)' : 'var(--ink-2)',
            fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', flexShrink: 0, cursor: 'pointer',
          }}>
            <Icon name={c.icon} size={13} stroke={2}/> {c.label}
          </button>
        ))}
      </div>

      {/* Quiz cards */}
      <div className="page-scroll" style={{ padding: '0 20px 40px' }}>
        {allQuizzes === null ? (
          <div style={{ textAlign: 'center', padding: '48px 20px', color: 'var(--ink-3)' }}>
            <Spinner size={24}/>
          </div>
        ) : quizzes.length === 0 ? (
          <div style={{ textAlign: 'center', padding: '48px 20px', color: 'var(--ink-3)' }}>
            <Icon name="headphones" size={32}/><br/>
            <span style={{ display: 'block', marginTop: 12, fontSize: 14 }}>
              {onlyUnfinished ? '所有測驗都已完成！' : '此分類目前沒有測驗'}
            </span>
          </div>
        ) : quizzes.map((q, i) => (
          <QuizCard key={q.id} q={q} isNew={i === 0 && cat === null && !onlyUnfinished}
            onStart={() => completedMap.has(q.id) ? go('review', { quiz: q, userAnswers: completedMap.get(q.id).answers }) : go('player', { quiz: q })}
            completed={completedMap.has(q.id)}
          />
        ))}
      </div>

      {showAdminGate && (
        <AdminGate
          onClose={() => setShowAdminGate(false)}
          onOk={() => { setShowAdminGate(false); go('admin'); }}
        />
      )}
    </>
  );
}

function QuizCard({ q, isNew, onStart, completed }) {
  return (
    <article
      onClick={onStart}
      style={{
        background: 'var(--surface)', borderRadius: 'var(--radius-card)',
        overflow: 'hidden', marginBottom: 16,
        border: '1px solid var(--line)', cursor: 'pointer',
      }}
      onMouseDown={e => e.currentTarget.style.transform = 'scale(0.99)'}
      onMouseUp={e => e.currentTarget.style.transform = ''}
      onMouseLeave={e => e.currentTarget.style.transform = ''}
    >
      <div className="img-box" style={{ aspectRatio: '16 / 10' }}>
        {q.image_url && <img src={q.image_url} alt={q.title} loading="lazy"/>}
        {isNew && (
          <div style={{
            position: 'absolute', top: 12, left: 12,
            background: 'var(--ink)', color: 'var(--bg)',
            fontSize: 10, fontWeight: 700, padding: '4px 8px',
            letterSpacing: '0.08em', textTransform: 'uppercase',
            fontFamily: 'var(--font-mono)',
          }}>NEW</div>
        )}
        {completed && (
          <div style={{
            position: 'absolute', top: 12, right: 12,
            width: 28, height: 28, borderRadius: 999,
            background: 'var(--correct)', color: '#fff',
            display: 'grid', placeItems: 'center',
          }}>
            <Icon name="check" size={14} stroke={2.6}/>
          </div>
        )}
      </div>
      <div style={{ padding: '16px 18px 18px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
          <CatTag cat={q.cat}/>
          <span style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>{q.date}</span>
        </div>
        <h2 style={{
          fontFamily: 'var(--font-serif)', fontSize: 24, fontWeight: 600,
          color: 'var(--ink)', lineHeight: 1.2, margin: '0 0 6px', letterSpacing: '-0.015em',
        }}>
          {q.title}
        </h2>
        <p style={{ fontSize: 13, color: 'var(--ink-2)', lineHeight: 1.55, margin: '0 0 14px' }}>
          {q.en}
        </p>
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          paddingTop: 12, borderTop: '1px solid var(--line)',
        }}>
          <div style={{ display: 'flex', gap: 14, fontSize: 12, color: 'var(--ink-3)' }}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
              <Icon name="headphones" size={13}/> {q.n} 題
            </span>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
              <Icon name="clock" size={13}/> {q.mins} 分鐘
            </span>
          </div>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 13, fontWeight: 700, color: 'var(--primary)' }}>
            開始 <Icon name="arrow" size={14} stroke={2.4}/>
          </span>
        </div>
      </div>
    </article>
  );
}

function AdminGate({ onClose, onOk }) {
  const [val, setVal] = useState('');
  const inputRef = useRef();
  useEffect(() => { inputRef.current?.focus(); }, []);

  const handleOk = () => {
    if (!val.trim()) return;
    sessionStorage.setItem('vl-admin-key', val.trim());
    onOk();
  };

  return (
    <div className="overlay" onClick={onClose}>
      <div className="sheet" onClick={e => e.stopPropagation()}>
        <h3 style={{ fontFamily: 'var(--font-serif)', fontSize: 22, margin: '0 0 6px', fontWeight: 600, color: 'var(--ink)' }}>
          Admin Key
        </h3>
        <p style={{ fontSize: 13, color: 'var(--ink-2)', margin: '0 0 16px' }}>請輸入管理金鑰繼續</p>
        <input
          ref={inputRef}
          type="password"
          placeholder="••••••••"
          value={val}
          onChange={e => setVal(e.target.value)}
          onKeyDown={e => e.key === 'Enter' && handleOk()}
          style={{
            width: '100%', padding: '12px 14px',
            border: '1px solid var(--line)', borderRadius: 'var(--radius-btn)',
            fontSize: 14, background: 'var(--bg)', color: 'var(--ink)',
            fontFamily: 'var(--font-mono)', marginBottom: 12, outline: 'none',
          }}
        />
        <div style={{ display: 'flex', gap: 8 }}>
          <button onClick={onClose} className="btn-pill ghost" style={{ flex: 1, padding: '12px' }}>取消</button>
          <button onClick={handleOk} className="btn-pill primary" style={{ flex: 1, padding: '12px' }}>進入</button>
        </div>
      </div>
    </div>
  );
}

// ── PLAYER PAGE ───────────────────────────────────────────────────────────────
function PlayerPage({ go, quiz }) {
  const [details, setDetails] = useState(null);

  useEffect(() => {
    if (!quiz) return;
    if (quiz.quiz_items) {
      setDetails(quiz);
      return;
    }
    const unsub = db.collection('quizzes').doc(quiz.id).collection('details').doc('content').onSnapshot(
      snap => setDetails(snap.exists ? snap.data() : null),
      console.error
    );
    return () => unsub();
  }, [quiz]);

  const questions = (details?.quiz_items || SAMPLE_QUESTIONS).map(item =>
    item.question ? {
      q: item.question, options: item.options,
      correct: item.answer_index, explain: item.explanation,
    } : item
  );
  const transcript = (details?.transcript || SAMPLE_TRANSCRIPT).map(t =>
    t.speaker ? {
      who: t.speaker === 'Woman' ? 'F' : 'M',
      name: t.speaker === 'Woman' ? 'Sarah' : 'Daniel',
      text: t.text,
    } : t
  );
  const audioUrl = details?.audio_url || null;

  const total = questions.length;
  const totalSec = 83;

  const [qIdx, setQIdx] = useState(0);
  const [answers, setAnswers] = useState(Array(total).fill(null));
  const [submitted, setSubmitted] = useState(Array(total).fill(false));
  const [showReview, setShowReview] = useState(false);

  const audioRef = useRef(null);
  const [paused, setPaused] = useState(true);
  const [currentT, setCurrentT] = useState(0);
  const [duration, setDuration] = useState(totalSec);

  useEffect(() => {
    if (!audioUrl) return;
    const a = new Audio(audioUrl);
    audioRef.current = a;
    a.addEventListener('loadedmetadata', () => setDuration(a.duration));
    a.addEventListener('timeupdate', () => setCurrentT(a.currentTime));
    a.addEventListener('ended', () => setPaused(true));
    return () => { a.pause(); };
  }, [audioUrl]);

  useEffect(() => {
    if (audioUrl) return;
    if (paused) return;
    const id = setInterval(() => {
      setCurrentT(t => {
        if (t >= duration) { setPaused(true); return duration; }
        return t + 0.5;
      });
    }, 500);
    return () => clearInterval(id);
  }, [paused, audioUrl, duration]);

  const swipeRef = useRef({ x: 0, active: false });
  const [swipeX, setSwipeX] = useState(0);

  if (!details) {
    return <div style={{ textAlign: 'center', padding: '100px 20px', color: 'var(--ink-3)' }}><Spinner size={32}/></div>;
  }

  const togglePlay = () => {
    if (audioRef.current) {
      if (paused) audioRef.current.play();
      else audioRef.current.pause();
    }
    setPaused(p => !p);
  };

  const seek = (delta) => {
    const next = Math.max(0, Math.min(duration, currentT + delta));
    if (audioRef.current) audioRef.current.currentTime = next;
    setCurrentT(next);
  };

  const seekTo = (ratio) => {
    const next = ratio * duration;
    if (audioRef.current) audioRef.current.currentTime = next;
    setCurrentT(next);
  };

  const Q = questions[qIdx];
  const sel = answers[qIdx];
  const isSubmitted = submitted[qIdx];

  const select = (i) => {
    if (isSubmitted) return;
    setAnswers(prev => { const a = [...prev]; a[qIdx] = i; return a; });
  };

  const submit = () => {
    if (sel === null) return;
    setSubmitted(prev => { const s = [...prev]; s[qIdx] = true; return s; });
    setShowReview(false);
  };

  const nextQ = () => {
    if (qIdx < total - 1) { setQIdx(qIdx + 1); setShowReview(false); }
    else go('result', { quiz: { ...quiz, ...details }, answers });
  };
  const onTouchStart = e => { swipeRef.current = { x: e.touches[0].clientX, active: true }; };
  const onTouchMove  = e => {
    if (!swipeRef.current.active) return;
    setSwipeX(e.touches[0].clientX - swipeRef.current.x);
  };
  const onTouchEnd = () => {
    if (!swipeRef.current.active) return;
    const dx = swipeX;
    swipeRef.current.active = false;
    setSwipeX(0);
    if (dx < -60 && isSubmitted) nextQ();
    else if (dx > 60 && qIdx > 0) { setQIdx(qIdx - 1); setShowReview(false); }
  };

  const fmt = s => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
  const progress = duration > 0 ? currentT / duration : 0;

  return (
    <>
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '12px 16px 8px', flexShrink: 0,
      }}>
        <button className="chrome-btn" onClick={() => go('list')}>
          <Icon name="chevL" size={18}/>
        </button>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          {questions.map((_, i) => (
            <button
              key={i}
              className={`dot ${i === qIdx ? 'active' : ''}`}
              style={{
                background: submitted[i]
                  ? (answers[i] === questions[i].correct ? 'var(--correct)' : 'var(--wrong)')
                  : i === qIdx ? 'var(--primary)' : undefined,
                opacity: submitted[i] || i === qIdx ? 1 : 0.35,
                cursor: submitted[i] ? 'pointer' : 'default',
              }}
              onClick={() => submitted[i] && setQIdx(i)}
            />
          ))}
        </div>
        <button className="chrome-btn" onClick={() => handleShare(quiz)}>
          <Icon name="share" size={16}/>
        </button>
      </div>

      <div
        className="page-scroll" style={{ padding: '4px 16px 16px' }}
        onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd}
      >
        <div style={{ position: 'relative', marginBottom: 16 }}>
          <div className="img-box" style={{ aspectRatio: '4 / 3', borderRadius: 'var(--radius-card)' }}>
            {quiz?.image_url && <img src={quiz.image_url} alt={quiz.title} loading="eager"/>}
          </div>
          <div style={{
            position: 'absolute', top: 14, left: 14,
            background: 'var(--surface)', border: '1px solid var(--line)',
            padding: '6px 10px', borderRadius: 'var(--radius-card)',
            fontSize: 11, fontWeight: 700, color: 'var(--ink)',
            fontFamily: 'var(--font-mono)', letterSpacing: '0.04em',
          }}>
            {quiz ? quiz.title.toUpperCase() : 'SHIPMENT DELAY'} · {quiz ? (CAT_MAP[quiz.cat]?.label || '') : '商務'}
          </div>
        </div>

        <AudioBar progress={progress} currentT={currentT} duration={duration} fmt={fmt} onSeek={seekTo}/>

        <div style={{ transform: `translateX(${swipeX}px)`, transition: swipeX === 0 ? 'transform .25s' : 'none' }}>
          <QuestionCard q={Q} qIdx={qIdx} total={total} sel={sel} submitted={isSubmitted} onSelect={select}/>
        </div>

        {!isSubmitted && currentT < 5 && (
          <div style={{
            marginTop: 8, padding: '10px 14px',
            background: 'var(--primary-soft)', border: '1px dashed var(--primary)',
            fontSize: 12, color: 'var(--ink-2)', fontFamily: 'var(--font-mono)', textAlign: 'center',
          }}>
            ↓ 先聽完音檔再作答 · 可隨時 ±5 秒
          </div>
        )}

        {isSubmitted && (
          <>
            {!showReview && (
              <button className="btn-pill ghost" onClick={() => setShowReview(true)}
                style={{ width: '100%', marginTop: 16, padding: '14px' }}>
                <Icon name="doc" size={14}/> 查看解析與逐字稿
              </button>
            )}
            {showReview && <ReviewBlock q={Q} transcript={transcript}/>}
          </>
        )}

        <div style={{ height: 130 }}/>
      </div>

      <PlayerDock
        paused={paused} onPlay={togglePlay}
        onBack5={() => seek(-5)} onFwd5={() => seek(5)}
        rightAction={
          isSubmitted
            ? (
              <button className="btn-pill primary" onClick={nextQ} style={{ padding: '12px 18px', fontSize: 14 }}>
                {qIdx < total - 1 ? '下一題' : '看結果'} <Icon name="arrow" size={14} stroke={2.4}/>
              </button>
            )
            : (
              <button
                className={`btn-pill ${sel === null ? 'disabled' : 'primary'}`}
                onClick={submit} disabled={sel === null}
                style={{ padding: '12px 18px', fontSize: 14 }}>
                提交答案
              </button>
            )
        }
      />
    </>
  );
}

function AudioBar({ progress, currentT, duration, fmt, onSeek }) {
  const trackRef = useRef();
  const handleClick = e => {
    const rect = trackRef.current.getBoundingClientRect();
    onSeek((e.clientX - rect.left) / rect.width);
  };
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 22, padding: '0 4px' }}>
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', minWidth: 32 }}>{fmt(currentT)}</span>
      <div className="progress-track" ref={trackRef} onClick={handleClick}>
        <div className="progress-fill" style={{ width: `${progress * 100}%` }}/>
        <div className="progress-thumb" style={{ left: `${progress * 100}%` }}/>
      </div>
      <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', minWidth: 32, textAlign: 'right' }}>{fmt(duration)}</span>
    </div>
  );
}

function QuestionCard({ q, qIdx, total, sel, submitted, onSelect }) {
  return (
    <div style={{ background: 'var(--surface)', borderRadius: 'var(--radius-card)', padding: '20px 18px', border: '1px solid var(--line)', marginBottom: 16 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 12 }}>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.08em' }}>
          QUESTION {qIdx + 1} / {total}
        </div>
        {submitted && (
          <div style={{
            marginLeft: 'auto', fontSize: 11, fontWeight: 700,
            padding: '3px 8px', borderRadius: 2,
            background: sel === q.correct ? 'var(--correct-soft)' : 'var(--wrong-soft)',
            color: sel === q.correct ? 'var(--correct)' : 'var(--wrong)',
            fontFamily: 'var(--font-mono)', letterSpacing: '0.06em',
          }}>
            {sel === q.correct ? 'CORRECT' : 'INCORRECT'}
          </div>
        )}
      </div>
      <h3 style={{ fontFamily: 'var(--font-serif)', fontSize: 22, fontWeight: 600, color: 'var(--ink)', lineHeight: 1.3, margin: '0 0 18px', letterSpacing: '-0.01em' }}>
        {q.q}
      </h3>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {q.options.map((opt, i) => {
          const isSel = sel === i;
          const isCor = i === q.correct;
          let bg = 'var(--surface)', border = '1px solid var(--line)';
          let labelBg = 'var(--surface-2)', labelColor = 'var(--ink-2)';
          let trailingIcon = null;
          if (submitted) {
            if (isCor) { bg = 'var(--correct-soft)'; border = '1px solid var(--correct)'; labelBg = 'var(--correct)'; labelColor = '#fff'; trailingIcon = <Icon name="check" size={16} color="var(--correct)" stroke={2.6}/>; }
            else if (isSel) { bg = 'var(--wrong-soft)'; border = '1px solid var(--wrong)'; labelBg = 'var(--wrong)'; labelColor = '#fff'; trailingIcon = <Icon name="x" size={16} color="var(--wrong)" stroke={2.6}/>; }
          } else if (isSel) {
            bg = 'var(--primary-soft)'; border = '1px solid var(--primary)';
            labelBg = 'var(--primary)'; labelColor = 'var(--primary-ink)';
          }
          return (
            <button key={i} className="option-row" disabled={submitted} onClick={() => onSelect(i)}
              style={{ background: bg, border, cursor: submitted ? 'default' : 'pointer' }}>
              <div className="option-label" style={{ background: labelBg, color: labelColor }}>
                {String.fromCharCode(65 + i)}
              </div>
              <div style={{ fontSize: 15, fontWeight: 500, color: 'var(--ink)', flex: 1, lineHeight: 1.35 }}>{opt}</div>
              {trailingIcon}
            </button>
          );
        })}
      </div>
    </div>
  );
}

function ReviewBlock({ q, transcript }) {
  return (
    <>
      <div style={{ background: 'var(--primary-soft)', borderRadius: 'var(--radius-card)', padding: '14px 18px', marginTop: 16 }}>
        <div style={{ fontSize: 11, fontWeight: 700, color: 'var(--primary)', letterSpacing: '0.08em', fontFamily: 'var(--font-mono)', marginBottom: 6 }}>
          EXPLANATION
        </div>
        <div style={{ fontSize: 14, color: 'var(--ink)', lineHeight: 1.6 }}>{q.explain}</div>
      </div>
      <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: 'var(--ink-3)', letterSpacing: '0.08em', margin: '20px 4px 10px' }}>
        TRANSCRIPT · 對話逐字稿
      </div>
      <div style={{ background: 'var(--surface)', borderRadius: 'var(--radius-card)', padding: '16px 14px', border: '1px solid var(--line)', marginBottom: 16 }}>
        {transcript.map((t, i) => {
          const isF = t.who === 'F';
          return (
            <div key={i} style={{ display: 'flex', flexDirection: isF ? 'row-reverse' : 'row', gap: 8, marginBottom: 12, alignItems: 'flex-end' }}>
              <div style={{ width: 28, height: 28, borderRadius: 999, background: isF ? 'var(--primary-soft)' : 'var(--surface-2)', color: isF ? 'var(--primary)' : 'var(--ink-2)', display: 'grid', placeItems: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0, fontFamily: 'var(--font-serif)' }}>
                {t.who === 'F' ? 'S' : 'D'}
              </div>
              <div style={{ maxWidth: '78%', background: isF ? 'var(--primary-soft)' : 'var(--surface-2)', color: 'var(--ink)', padding: '10px 13px', borderRadius: 4, fontSize: 14, lineHeight: 1.55 }}>
                <div style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 2, fontWeight: 700, letterSpacing: '0.06em', fontFamily: 'var(--font-mono)' }}>
                  {t.name.toUpperCase()}
                </div>
                {t.text}
              </div>
            </div>
          );
        })}
      </div>
    </>
  );
}

function PlayerDock({ paused, onPlay, onBack5, onFwd5, rightAction }) {
  const seekBtnStyle = { ...dockSecBtn(), display: 'grid', placeItems: 'center', width: 52, height: 52 };
  return (
    <div className="bottom-dock">
      <div className="dock-inner">
        <button onClick={onBack5} style={seekBtnStyle}>
          <Icon name="back5" size={28}/>
        </button>
        <button onClick={onPlay} style={{
          width: 60, height: 60, borderRadius: 999,
          background: 'var(--primary)', color: 'var(--primary-ink)',
          border: 'none', display: 'grid', placeItems: 'center', cursor: 'pointer', flexShrink: 0,
        }}>
          <Icon name={paused ? 'play' : 'pause'} size={32}/>
        </button>
        <button onClick={onFwd5} style={seekBtnStyle}>
          <Icon name="fwd5" size={28}/>
        </button>
        <div style={{ flex: 1, display: 'flex', justifyContent: 'flex-end' }}>{rightAction}</div>
      </div>
    </div>
  );
}

// ── RESULT PAGE ───────────────────────────────────────────────────────────────
function ResultPage({ go, quiz, answers = [], uid }) {
  const questions = (quiz?.quiz_items || SAMPLE_QUESTIONS).map(item =>
    item.question ? { q: item.question, options: item.options, correct: item.answer_index, explain: item.explanation } : item
  );
  const score = answers.reduce((acc, a, i) => acc + (a === questions[i]?.correct ? 1 : 0), 0);
  const total = questions.length;

  const completedMap = useCompletedMap(uid);
  const saved = useRef(false);
  useEffect(() => {
    if (saved.current || !uid || !quiz?.id) return;
    saved.current = true;
    saveProgress(uid, quiz.id, score, total, answers).catch(console.error);
  }, [uid]);

  // 2-column stats — no streak
  const stats = [
    { v: `${Math.round((score / total) * 100)}%`, l: 'ACCURACY' },
    { v: completedMap.size || '—', l: 'DONE' },
  ];

  return (
    <>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px 8px', flexShrink: 0 }}>
        <button className="chrome-btn" onClick={() => go('list')}>
          <Icon name="chevL" size={18}/>
        </button>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.1em' }}>RESULT</div>
        <button className="chrome-btn" onClick={() => handleShare(quiz)}>
          <Icon name="share" size={16}/>
        </button>
      </div>

      <div className="page-scroll" style={{ padding: '20px 24px 48px' }}>
        <div style={{ textAlign: 'center', padding: '16px 0 8px' }}>
          <div style={{ fontFamily: 'var(--font-serif)', fontSize: 110, fontWeight: 700, lineHeight: 1, letterSpacing: '-0.05em', color: score === total ? 'var(--correct)' : 'var(--ink)' }}>
            {score}<span style={{ color: 'var(--ink-3)', fontWeight: 400 }}>/{total}</span>
          </div>
          <div style={{ fontFamily: 'var(--font-serif)', fontSize: 22, fontWeight: 600, color: 'var(--ink)', marginTop: 8, letterSpacing: '-0.01em' }}>
            {score === total ? '全對！太厲害了。' : score >= 2 ? '不錯，再練一次。' : '再聽一次試試。'}
          </div>
          <div style={{ fontSize: 13, color: 'var(--ink-2)', marginTop: 6, fontFamily: 'var(--font-mono)' }}>
            {quiz?.title || 'Shipment Delay'} · {quiz?.mins || 1}:23
          </div>
        </div>

        <div style={{ display: 'flex', gap: 10, justifyContent: 'center', margin: '24px 0 28px' }}>
          {questions.map((q, i) => {
            const c = answers[i] === q.correct;
            return (
              <div key={i} style={{ width: 60, padding: '12px 0', borderRadius: 'var(--radius-card)', background: c ? 'var(--correct-soft)' : 'var(--wrong-soft)', color: c ? 'var(--correct)' : 'var(--wrong)', textAlign: 'center', border: `1px solid ${c ? 'var(--correct)' : 'var(--wrong)'}` }}>
                <div style={{ fontSize: 10, fontWeight: 700, opacity: .85, marginBottom: 3, fontFamily: 'var(--font-mono)', letterSpacing: '0.06em' }}>Q{i + 1}</div>
                <Icon name={c ? 'check_circle' : 'cancel'} size={22}/>
              </div>
            );
          })}
        </div>

        {/* 2-column stats (no streak) */}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', border: '1px solid var(--line)', marginBottom: 24 }}>
          {stats.map((s, i) => (
            <div key={i} style={{ padding: '14px 10px', textAlign: 'center', borderRight: i === 0 ? '1px solid var(--line)' : 'none', background: 'var(--surface)' }}>
              <div style={{ fontFamily: 'var(--font-serif)', fontSize: 24, fontWeight: 700, color: 'var(--ink)', letterSpacing: '-0.02em' }}>{s.v}</div>
              <div style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 2, fontFamily: 'var(--font-mono)', letterSpacing: '0.08em' }}>{s.l}</div>
            </div>
          ))}
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
          <button className="btn-pill primary" onClick={() => go('review', { quiz, userAnswers: answers })} style={{ padding: '14px', fontSize: 15 }}>
            <Icon name="doc" size={15}/> 查看複習
          </button>
          <div style={{ display: 'flex', gap: 10 }}>
            <button className="btn-pill ghost" onClick={() => handleShare(quiz)} style={{ flex: 1, padding: '12px', fontSize: 14 }}>
              <Icon name="share" size={14}/> 分享
            </button>
            <button className="btn-pill ghost" onClick={() => go('player', { quiz, answers: Array(total).fill(null) })} style={{ flex: 1, padding: '12px', fontSize: 14 }}>
              <Icon name="repeat" size={14}/> 重做
            </button>
          </div>
        </div>
      </div>
    </>
  );
}

// ── REVIEW PAGE ───────────────────────────────────────────────────────────────
function ReviewPage({ go, quiz, userAnswers = null }) {
  const [details, setDetails] = useState(null);

  useEffect(() => {
    if (!quiz) return;
    if (quiz.quiz_items) {
      setDetails(quiz);
      return;
    }
    const unsub = db.collection('quizzes').doc(quiz.id).collection('details').doc('content').onSnapshot(
      snap => setDetails(snap.exists ? snap.data() : null),
      console.error
    );
    return () => unsub();
  }, [quiz]);

  const questions = (details?.quiz_items || SAMPLE_QUESTIONS).map(item =>
    item.question ? {
      q: item.question, options: item.options,
      correct: item.answer_index, explain: item.explanation,
    } : item
  );
  const transcript = (details?.transcript || SAMPLE_TRANSCRIPT).map(t =>
    t.speaker ? {
      who: t.speaker === 'Woman' ? 'F' : 'M',
      name: t.speaker === 'Woman' ? 'Sarah' : 'Daniel',
      text: t.text,
    } : t
  );
  const audioUrl = details?.audio_url || null;

  const audioRef = useRef(null);
  const [paused, setPaused] = useState(true);
  const [currentT, setCurrentT] = useState(0);
  const [duration, setDuration] = useState(83);
  const [showTranscript, setShowTranscript] = useState(false);
  const [expandedExplains, setExpandedExplains] = useState({});

  useEffect(() => {
    if (!audioUrl) return;
    const a = new Audio(audioUrl);
    audioRef.current = a;
    a.addEventListener('loadedmetadata', () => setDuration(a.duration));
    a.addEventListener('timeupdate', () => setCurrentT(a.currentTime));
    a.addEventListener('ended', () => setPaused(true));
    return () => { a.pause(); };
  }, [audioUrl]);

  useEffect(() => {
    if (audioUrl || paused) return;
    const id = setInterval(() => {
      setCurrentT(t => {
        if (t >= duration) { setPaused(true); return duration; }
        return t + 0.5;
      });
    }, 500);
    return () => clearInterval(id);
  }, [paused, audioUrl, duration]);

  if (!details) {
    return <div style={{ textAlign: 'center', padding: '100px 20px', color: 'var(--ink-3)' }}><Spinner size={32}/></div>;
  }

  const togglePlay = () => {
    if (audioRef.current) { if (paused) audioRef.current.play(); else audioRef.current.pause(); }
    setPaused(p => !p);
  };
  const seek = (delta) => {
    const next = Math.max(0, Math.min(duration, currentT + delta));
    if (audioRef.current) audioRef.current.currentTime = next;
    setCurrentT(next);
  };
  const seekTo = (ratio) => {
    const next = ratio * duration;
    if (audioRef.current) audioRef.current.currentTime = next;
    setCurrentT(next);
  };
  const fmt = s => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
  const progress = duration > 0 ? currentT / duration : 0;

  const chevron = (open) => (
    <span style={{ display: 'flex', transform: open ? 'rotate(270deg)' : 'rotate(90deg)', transition: 'transform .2s' }}>
      <Icon name="chevR" size={15} color={open ? 'var(--primary)' : 'var(--ink-3)'}/>
    </span>
  );

  return (
    <>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px 8px', flexShrink: 0 }}>
        <button className="chrome-btn" onClick={() => go('list')}>
          <Icon name="chevL" size={18}/>
        </button>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.1em' }}>REVIEW</div>
        <button className="chrome-btn" onClick={() => handleShare(quiz)}>
          <Icon name="share" size={16}/>
        </button>
      </div>

      <div className="page-scroll" style={{ padding: '4px 16px 16px' }}>
        <div style={{ position: 'relative', marginBottom: 14 }}>
          <div className="img-box" style={{ aspectRatio: '4 / 3', borderRadius: 'var(--radius-card)' }}>
            {quiz?.image_url && <img src={quiz.image_url} alt={quiz.title} loading="eager"/>}
          </div>
          <div style={{
            position: 'absolute', top: 14, left: 14,
            background: 'var(--surface)', border: '1px solid var(--line)',
            padding: '6px 10px', borderRadius: 'var(--radius-card)',
            fontSize: 11, fontWeight: 700, color: 'var(--ink)',
            fontFamily: 'var(--font-mono)', letterSpacing: '0.04em',
          }}>
            {quiz ? quiz.title.toUpperCase() : 'REVIEW'} · {quiz ? (CAT_MAP[quiz.cat]?.label || '') : ''}
          </div>
        </div>

        <AudioBar progress={progress} currentT={currentT} duration={duration} fmt={fmt} onSeek={seekTo}/>

        {/* Transcript section (single toggle) */}
        <div style={{ marginBottom: 16 }}>
          <button
            onClick={() => setShowTranscript(v => !v)}
            style={{
              width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
              padding: '12px 16px',
              background: showTranscript ? 'var(--surface-2)' : 'var(--surface)',
              border: '1px solid var(--line)',
              borderRadius: showTranscript ? 'var(--radius-card) var(--radius-card) 0 0' : 'var(--radius-card)',
              cursor: 'pointer',
            }}
          >
            <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: 'var(--ink-2)', letterSpacing: '0.08em' }}>
              TRANSCRIPT · 對話逐字稿
            </span>
            {chevron(showTranscript)}
          </button>
          {showTranscript && (
            <div style={{ background: 'var(--surface)', border: '1px solid var(--line)', borderTop: 'none', borderRadius: '0 0 var(--radius-card) var(--radius-card)', padding: '16px 14px' }}>
              {transcript.map((t, i) => {
                const isF = t.who === 'F';
                return (
                  <div key={i} style={{ display: 'flex', flexDirection: isF ? 'row-reverse' : 'row', gap: 8, marginBottom: i < transcript.length - 1 ? 12 : 0, alignItems: 'flex-end' }}>
                    <div style={{ width: 28, height: 28, borderRadius: 999, background: isF ? 'var(--primary-soft)' : 'var(--surface-2)', color: isF ? 'var(--primary)' : 'var(--ink-2)', display: 'grid', placeItems: 'center', fontSize: 11, fontWeight: 700, flexShrink: 0, fontFamily: 'var(--font-serif)' }}>
                      {isF ? 'S' : 'D'}
                    </div>
                    <div style={{ maxWidth: '78%', background: isF ? 'var(--primary-soft)' : 'var(--surface-2)', color: 'var(--ink)', padding: '10px 13px', borderRadius: 4, fontSize: 14, lineHeight: 1.55 }}>
                      <div style={{ fontSize: 10, color: 'var(--ink-3)', marginBottom: 2, fontWeight: 700, letterSpacing: '0.06em', fontFamily: 'var(--font-mono)' }}>
                        {t.name.toUpperCase()}
                      </div>
                      {t.text}
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </div>

        {/* Per-question review blocks */}
        {questions.map((q, qi) => (
          <div key={qi} style={{ background: 'var(--surface)', borderRadius: 'var(--radius-card)', border: '1px solid var(--line)', marginBottom: 16, overflow: 'hidden' }}>
            <div style={{ padding: '16px 18px 14px' }}>
              <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.08em', marginBottom: 8 }}>
                QUESTION {qi + 1} / {questions.length}
              </div>
              <h3 style={{ fontFamily: 'var(--font-serif)', fontSize: 20, fontWeight: 600, color: 'var(--ink)', lineHeight: 1.3, margin: '0 0 14px', letterSpacing: '-0.01em' }}>
                {q.q}
              </h3>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                {q.options.map((opt, oi) => {
                  const isCor = oi === q.correct;
                  const isSel = userAnswers && userAnswers[qi] === oi;
                  let bg = 'var(--bg)';
                  let border = '1px solid var(--line)';
                  let iconBg = 'var(--surface-2)';
                  let iconColor = 'var(--ink-3)';
                  let iconContent = String.fromCharCode(65 + oi);
                  let textColor = 'var(--ink-2)';
                  let textWeight = 400;

                  if (isCor) {
                    bg = 'var(--correct-soft)'; border = '1px solid var(--correct)';
                    iconBg = 'var(--correct)'; iconColor = '#fff'; textColor = 'var(--correct)'; textWeight = 600;
                    iconContent = <Icon name="check" size={13} stroke={2.8}/>;
                  } else if (isSel) {
                    bg = 'var(--wrong-soft)'; border = '1px solid var(--wrong)';
                    iconBg = 'var(--wrong)'; iconColor = '#fff'; textColor = 'var(--wrong)'; textWeight = 600;
                    iconContent = <Icon name="x" size={13} stroke={2.8}/>;
                  }

                  return (
                    <div key={oi} style={{
                      display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px',
                      borderRadius: 'var(--radius-btn)',
                      background: bg,
                      border: border,
                    }}>
                      <div style={{
                        width: 24, height: 24, borderRadius: 2, flexShrink: 0,
                        background: iconBg,
                        color: iconColor,
                        display: 'grid', placeItems: 'center',
                        fontSize: 11, fontWeight: 700, fontFamily: 'var(--font-mono)',
                      }}>
                        {iconContent}
                      </div>
                      <div style={{ fontSize: 14, fontWeight: textWeight, color: textColor, flex: 1, lineHeight: 1.35 }}>
                        {opt}
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>

            <button
              onClick={() => setExpandedExplains(prev => ({ ...prev, [qi]: !prev[qi] }))}
              style={{
                width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                padding: '10px 18px',
                background: expandedExplains[qi] ? 'var(--primary-soft)' : 'var(--surface)',
                borderTop: '1px solid var(--line)', borderRight: 'none', borderBottom: 'none', borderLeft: 'none',
                cursor: 'pointer', transition: 'background .15s',
              }}
            >
              <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: expandedExplains[qi] ? 'var(--primary)' : 'var(--ink-3)', letterSpacing: '0.08em' }}>
                EXPLANATION
              </span>
              {chevron(expandedExplains[qi])}
            </button>
            {expandedExplains[qi] && (
              <div style={{ padding: '12px 18px 16px', background: 'var(--primary-soft)' }}>
                <div style={{ fontSize: 14, color: 'var(--ink)', lineHeight: 1.6 }}>{q.explain}</div>
              </div>
            )}
          </div>
        ))}

        <div style={{ height: 130 }}/>
      </div>

      <PlayerDock
        paused={paused} onPlay={togglePlay}
        onBack5={() => seek(-5)} onFwd5={() => seek(5)}
        rightAction={
          <button className="btn-pill ghost" onClick={() => go('player', { quiz, answers: Array(questions.length).fill(null) })} style={{ padding: '12px 16px', fontSize: 14 }}>
            <Icon name="repeat" size={14}/> 重做
          </button>
        }
      />
    </>
  );
}

// ── ADMIN PAGE ────────────────────────────────────────────────────────────────
function AdminPage({ go }) {
  const [tab, setTab] = useState('upload');
  const [stage, setStage] = useState('idle');
  const [activeStep, setActiveStep] = useState(0);
  const [file, setFile] = useState(null);
  const [previewUrl, setPreviewUrl] = useState(null);
  const [errorMsg, setErrorMsg] = useState('');
  const fileInputRef = useRef();

  const steps = [
    { key: 'analyze', label: '分析場景',  icon: 'cam'      },
    { key: 'write',   label: '撰寫對話',  icon: 'doc'      },
    { key: 'image',   label: '生成情境圖', icon: 'sparkles' },
    { key: 'voice',   label: '合成語音',  icon: 'sound'    },
    { key: 'save',    label: '儲存測驗',  icon: 'check'    },
  ];

  const handleFile = async (f) => {
    if (!f) return;
    const resized = await resizeImage(f, 1024);
    setFile(resized);
    setPreviewUrl(URL.createObjectURL(resized));
    setStage('preview');
    setErrorMsg('');
  };

  const handleDrop = (e) => {
    e.preventDefault();
    const f = e.dataTransfer.files[0];
    if (f && (f.type === 'image/jpeg' || f.type === 'image/png')) handleFile(f);
  };

  const generate = async () => {
    if (stage !== 'preview' || !file) return;
    setStage('generating');
    setActiveStep(0);
    const adminKey = sessionStorage.getItem('vl-admin-key') || '';
    if (!BACKEND_URL) {
      [1500, 3000, 4500, 6000].forEach((ms, i) => setTimeout(() => setActiveStep(i + 1), ms));
      setTimeout(() => setStage('done'), 6500);
      return;
    }
    try {
      const form = new FormData();
      form.append('image', file);
      const res = await fetch(`${BACKEND_URL}/generate`, {
        method: 'POST',
        headers: { 'Admin-Key': adminKey },
        body: form,
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      setStage('done');
    } catch (err) {
      setStage('error');
      setErrorMsg(err.message || '生成失敗，請重試');
    }
  };

  return (
    <>
      {/* AppBar */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px 8px', flexShrink: 0 }}>
        <button className="chrome-btn" onClick={() => go('list')}>
          <Icon name="chevL" size={18}/>
        </button>
        <div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, color: 'var(--ink-2)', letterSpacing: '0.1em', display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ width: 6, height: 6, borderRadius: 999, background: 'var(--primary)', boxShadow: '0 0 0 4px var(--primary-soft)' }}/>
          ADMIN
        </div>
        <div style={{ width: 38 }}/>
      </div>

      {/* Tab bar */}
      <div style={{ display: 'flex', borderBottom: '2px solid var(--line)', padding: '0 20px', flexShrink: 0 }}>
        {[['upload', '上傳新測驗'], ['manage', '管理測驗']].map(([k, label]) => (
          <button
            key={k}
            onClick={() => setTab(k)}
            style={{
              flex: 1, padding: '10px 0', border: 'none',
              background: 'transparent', cursor: 'pointer',
              fontSize: 13, fontWeight: 700, fontFamily: 'inherit',
              color: tab === k ? 'var(--primary)' : 'var(--ink-3)',
              borderBottom: tab === k ? '2px solid var(--primary)' : '2px solid transparent',
              marginBottom: -2, transition: 'color .15s',
            }}
          >
            {label}
          </button>
        ))}
      </div>

      {/* Upload tab */}
      {tab === 'upload' && (
        <div className="page-scroll" style={{ padding: '8px 24px 48px' }}>
          <h1 style={{ fontFamily: 'var(--font-serif)', fontSize: 32, fontWeight: 700, letterSpacing: '-0.025em', margin: '12px 0 6px', lineHeight: 1.15, color: 'var(--ink)' }}>
            上傳一張<br/>情境照片。
          </h1>
          <p style={{ fontSize: 14, color: 'var(--ink-2)', lineHeight: 1.55, margin: '0 0 24px' }}>
            AI 會生成符合場景的多益對話與三題聽力題目
          </p>

          {stage === 'idle' ? (
            <div onDrop={handleDrop} onDragOver={e => e.preventDefault()} onClick={() => fileInputRef.current?.click()} style={{ width: '100%', border: '1.5px dashed var(--line)', background: 'var(--surface)', padding: '36px 20px', textAlign: 'center', cursor: 'pointer', borderRadius: 'var(--radius-card)' }}>
              <div style={{ width: 60, height: 60, borderRadius: 999, background: 'var(--primary-soft)', color: 'var(--primary)', margin: '0 auto 14px', display: 'grid', placeItems: 'center' }}>
                <Icon name="cam" size={26} stroke={1.6}/>
              </div>
              <div style={{ fontFamily: 'var(--font-serif)', fontSize: 18, fontWeight: 600, marginBottom: 4, color: 'var(--ink)' }}>點擊上傳 / 拍照</div>
              <div style={{ fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.55 }}>支援 JPG · PNG · 最大 8MB</div>
            </div>
          ) : (
            <div style={{ border: '1px solid var(--line)', background: 'var(--surface)', borderRadius: 'var(--radius-card)', overflow: 'hidden' }}>
              <div className="img-box" style={{ aspectRatio: '4 / 3', position: 'relative' }}>
                {previewUrl && <img src={previewUrl} alt="preview" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}/>}
                {stage === 'preview' && (
                  <button onClick={() => { setStage('idle'); setFile(null); setPreviewUrl(null); }} style={{ position: 'absolute', top: 12, right: 12, background: 'var(--ink)', color: 'var(--bg)', padding: '6px 10px', borderRadius: 2, fontSize: 12, fontWeight: 600, border: 'none', cursor: 'pointer', fontFamily: 'var(--font-mono)' }}>重選</button>
                )}
              </div>
              <div style={{ padding: '12px 14px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid var(--line)' }}>
                <div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--ink-3)' }}>
                  {file?.name || 'image.jpg'} · {file ? (file.size / 1024 / 1024).toFixed(1) : '?'}MB
                </div>
                <div style={{ fontSize: 11, color: 'var(--ink-2)', fontFamily: 'var(--font-mono)', fontWeight: 700, letterSpacing: '0.08em' }}>
                  {stage === 'preview' ? 'READY' : stage === 'generating' ? 'PROCESSING' : stage === 'done' ? 'COMPLETE' : 'ERROR'}
                </div>
              </div>
            </div>
          )}

          <input ref={fileInputRef} type="file" accept="image/jpeg,image/png" style={{ display: 'none' }} onChange={e => handleFile(e.target.files[0])}/>

          <div style={{ marginTop: 16 }}>
            <button className={`btn-pill ${stage === 'preview' ? 'primary' : 'disabled'}`} onClick={generate} disabled={stage !== 'preview'} style={{ width: '100%', padding: '16px 20px', fontSize: 15 }}>
              {stage === 'generating' ? <><Spinner size={16}/> 生成中…</> : stage === 'done' ? <><Icon name="check" size={16} stroke={2.4}/> 已生成</> : stage === 'error' ? <><Icon name="x" size={16}/> 失敗，請重試</> : <><Icon name="sparkles" size={16} stroke={2.2}/> 生成測驗</>}
            </button>
          </div>

          {stage === 'error' && errorMsg && (
            <div style={{ marginTop: 12, padding: '10px 14px', background: 'var(--wrong-soft)', border: '1px solid var(--wrong)', borderRadius: 'var(--radius-card)', fontSize: 13, color: 'var(--wrong)' }}>
              {errorMsg}
            </div>
          )}

          {(stage === 'generating' || stage === 'done') && (
            <div style={{ marginTop: 24, background: 'var(--surface)', border: '1px solid var(--line)' }}>
              {steps.map((s, i) => {
                const done   = stage === 'done' || i < activeStep;
                const active = stage === 'generating' && i === activeStep;
                return (
                  <div key={s.key} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px', borderBottom: i < steps.length - 1 ? '1px solid var(--line)' : 'none' }}>
                    <div style={{ width: 28, height: 28, borderRadius: 999, background: done ? 'var(--primary)' : active ? 'var(--primary-soft)' : 'var(--surface-2)', color: done ? 'var(--primary-ink)' : active ? 'var(--primary)' : 'var(--ink-3)', display: 'grid', placeItems: 'center', flexShrink: 0 }}>
                      {done ? <Icon name="check" size={14} stroke={2.6}/> : active ? <Spinner size={14}/> : <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700 }}>{i + 1}</span>}
                    </div>
                    <div style={{ fontSize: 14, fontWeight: 500, color: 'var(--ink)', flex: 1, fontFamily: 'var(--font-serif)', opacity: !done && !active ? 0.45 : 1 }}>{s.label}</div>
                    {done   && <span style={{ fontSize: 11, color: 'var(--correct)', fontFamily: 'var(--font-mono)', fontWeight: 700 }}>OK</span>}
                    {active && <span style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>…</span>}
                  </div>
                );
              })}
            </div>
          )}

          {stage === 'done' && (
            <button className="btn-pill primary" onClick={() => { setTab('manage'); setStage('idle'); setActiveStep(0); }} style={{ width: '100%', marginTop: 16, padding: '14px' }}>
              前往管理測驗 <Icon name="arrow" size={14} stroke={2.4}/>
            </button>
          )}
        </div>
      )}

      {/* Manage tab */}
      {tab === 'manage' && <AdminQuizList/>}
    </>
  );
}

// ── ADMIN QUIZ LIST ───────────────────────────────────────────────────────────
function AdminQuizList() {
  const [quizzes, setQuizzes] = useState(null);
  const [deleteId, setDeleteId] = useState(null);
  const [deleting, setDeleting] = useState(false);
  const [replaceQuiz, setReplaceQuiz] = useState(null);

  useEffect(() => {
    return db.collection('quizzes').orderBy('created_at', 'desc').onSnapshot(
      snap => setQuizzes(snap.docs.map(d => ({ id: d.id, ...d.data() }))),
      () => setQuizzes([])
    );
  }, []);

  const handleDelete = async (quiz) => {
    setDeleting(true);
    const adminKey = sessionStorage.getItem('vl-admin-key') || '';
    try {
      if (BACKEND_URL) {
        const res = await fetch(`${BACKEND_URL}/quiz/${quiz.id}`, {
          method: 'DELETE',
          headers: { 'Admin-Key': adminKey },
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
      }
      setDeleteId(null);
    } catch (e) {
      console.error(e);
    } finally {
      setDeleting(false);
    }
  };

  if (quizzes === null) {
    return <div style={{ textAlign: 'center', padding: '48px', color: 'var(--ink-3)' }}><Spinner size={24}/></div>;
  }

  return (
    <div className="page-scroll" style={{ padding: '4px 20px 48px' }}>
      <div style={{ fontSize: 11, color: 'var(--ink-3)', letterSpacing: '0.08em', fontFamily: 'var(--font-mono)', padding: '10px 2px 12px' }}>
        {quizzes.length} 份測驗
      </div>

      {quizzes.length === 0 && (
        <div style={{ textAlign: 'center', padding: '40px 0', color: 'var(--ink-3)' }}>
          <Icon name="headphones" size={32}/>
          <div style={{ marginTop: 12, fontSize: 14 }}>尚無測驗</div>
        </div>
      )}

      {quizzes.map(q => (
        <AdminQuizRow
          key={q.id}
          q={q}
          isDeleteConfirm={deleteId === q.id}
          deleting={deleting}
          onDeleteRequest={() => { setDeleteId(q.id); setReplaceQuiz(null); }}
          onDeleteConfirm={() => handleDelete(q)}
          onDeleteCancel={() => setDeleteId(null)}
          onReplaceImage={() => { setReplaceQuiz(q); setDeleteId(null); }}
        />
      ))}

      {replaceQuiz && (
        <ReplaceImageModal quiz={replaceQuiz} onClose={() => setReplaceQuiz(null)}/>
      )}
    </div>
  );
}

function AdminQuizRow({ q, isDeleteConfirm, deleting, onDeleteRequest, onDeleteConfirm, onDeleteCancel, onReplaceImage }) {
  return (
    <div style={{
      background: 'var(--surface)',
      border: `1px solid ${isDeleteConfirm ? 'var(--wrong)' : 'var(--line)'}`,
      borderRadius: 'var(--radius-card)',
      marginBottom: 12, overflow: 'hidden', transition: 'border-color .2s',
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px' }}>
        <div className="img-box" style={{ width: 64, height: 48, borderRadius: 2, flexShrink: 0 }}>
          {q.image_url && <img src={q.image_url} alt={q.title} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}/>}
        </div>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontFamily: 'var(--font-serif)', fontSize: 15, fontWeight: 600, color: 'var(--ink)', lineHeight: 1.2, marginBottom: 5, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
            {q.title}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <CatTag cat={q.cat}/>
            <span style={{ fontSize: 10, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>{q.date}</span>
          </div>
        </div>
        <div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
          <button onClick={onReplaceImage} title="換圖" style={{
            width: 34, height: 34, borderRadius: 'var(--radius-card)',
            border: '1px solid var(--line)', background: 'var(--surface)',
            color: 'var(--ink-2)', display: 'grid', placeItems: 'center', cursor: 'pointer',
          }}>
            <Icon name="image" size={15}/>
          </button>
          <button
            onClick={isDeleteConfirm ? onDeleteCancel : onDeleteRequest}
            title={isDeleteConfirm ? '取消' : '刪除'}
            style={{
              width: 34, height: 34, borderRadius: 'var(--radius-card)',
              border: '1px solid var(--wrong)',
              background: isDeleteConfirm ? 'var(--wrong)' : 'var(--wrong-soft)',
              color: isDeleteConfirm ? '#fff' : 'var(--wrong)',
              display: 'grid', placeItems: 'center', cursor: 'pointer', transition: 'all .15s',
            }}
          >
            <Icon name="trash" size={15}/>
          </button>
        </div>
      </div>

      {isDeleteConfirm && (
        <div style={{ borderTop: '1px solid var(--wrong)', padding: '10px 14px', background: 'var(--wrong-soft)', display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ flex: 1, fontSize: 12, color: 'var(--wrong)', fontWeight: 600, lineHeight: 1.4 }}>
            確定刪除「{q.title}」？<br/>
            <span style={{ fontWeight: 400, opacity: 0.8 }}>連同圖片與音檔一併刪除，無法復原。</span>
          </span>
          <button onClick={onDeleteCancel} style={{ padding: '6px 12px', borderRadius: 2, border: '1px solid var(--line)', background: 'var(--surface)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--ink-2)', whiteSpace: 'nowrap' }}>取消</button>
          <button onClick={onDeleteConfirm} disabled={deleting} style={{ padding: '6px 12px', borderRadius: 2, border: 'none', background: 'var(--wrong)', color: '#fff', fontSize: 12, fontWeight: 700, cursor: 'pointer', whiteSpace: 'nowrap' }}>
            {deleting ? <Spinner size={12}/> : '確認刪除'}
          </button>
        </div>
      )}
    </div>
  );
}

// ── REPLACE IMAGE MODAL ───────────────────────────────────────────────────────
function ReplaceImageModal({ quiz, onClose }) {
  const [file, setFile] = useState(null);
  const [previewUrl, setPreviewUrl] = useState(null);
  const [uploading, setUploading] = useState(false);
  const [done, setDone] = useState(false);
  const fileInputRef = useRef();

  const handleFile = async (f) => {
    if (!f) return;
    const resized = await resizeImage(f, 1024);
    setFile(resized);
    setPreviewUrl(URL.createObjectURL(resized));
  };

  const handleConfirm = async () => {
    if (!file) return;
    setUploading(true);
    const adminKey = sessionStorage.getItem('vl-admin-key') || '';
    try {
      if (BACKEND_URL) {
        const form = new FormData();
        form.append('image', file);
        const res = await fetch(`${BACKEND_URL}/quiz/${quiz.id}/image`, {
          method: 'PUT',
          headers: { 'Admin-Key': adminKey },
          body: form,
        });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
      }
      setDone(true);
      setTimeout(onClose, 900);
    } catch (e) {
      console.error(e);
      setUploading(false);
    }
  };

  return (
    <div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(20,17,13,0.55)', zIndex: 30, display: 'flex', alignItems: 'flex-end' }}>
      <div onClick={e => e.stopPropagation()} style={{ background: 'var(--surface)', width: '100%', padding: '20px 20px 32px', borderTopLeftRadius: 10, borderTopRightRadius: 10 }}>
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 18 }}>
          <div>
            <div style={{ fontFamily: 'var(--font-serif)', fontSize: 20, fontWeight: 600, color: 'var(--ink)' }}>更換測驗圖片</div>
            <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 3, fontFamily: 'var(--font-mono)' }}>{quiz.title}</div>
          </div>
          <button onClick={onClose} style={{ width: 32, height: 32, borderRadius: 999, border: '1px solid var(--line)', background: 'transparent', color: 'var(--ink-2)', display: 'grid', placeItems: 'center', cursor: 'pointer', flexShrink: 0 }}>
            <Icon name="x" size={16}/>
          </button>
        </div>

        {!previewUrl ? (
          <div onClick={() => fileInputRef.current?.click()} onDrop={e => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); }} onDragOver={e => e.preventDefault()}
            style={{ border: '1.5px dashed var(--line)', borderRadius: 4, padding: '28px 20px', textAlign: 'center', cursor: 'pointer', background: 'var(--bg)' }}>
            <div style={{ width: 48, height: 48, borderRadius: 999, background: 'var(--primary-soft)', color: 'var(--primary)', margin: '0 auto 12px', display: 'grid', placeItems: 'center' }}>
              <Icon name="upload" size={22} stroke={1.6}/>
            </div>
            <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--ink)', marginBottom: 4 }}>點擊上傳 / 拖曳圖片</div>
            <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>JPG · PNG · 最大 8MB</div>
          </div>
        ) : (
          <div style={{ border: '1px solid var(--line)', borderRadius: 4, overflow: 'hidden' }}>
            <img src={previewUrl} alt="preview" style={{ width: '100%', aspectRatio: '4/3', objectFit: 'cover', display: 'block' }}/>
            <div style={{ padding: '10px 12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid var(--line)' }}>
              <span style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>
                {file?.name} · {file ? (file.size / 1024 / 1024).toFixed(1) : '?'}MB
              </span>
              {!uploading && !done && (
                <button onClick={() => { setFile(null); setPreviewUrl(null); }} style={{ fontSize: 11, color: 'var(--ink-2)', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline', padding: 0 }}>重選</button>
              )}
              {uploading && <span style={{ fontSize: 11, color: 'var(--ink-3)', fontFamily: 'var(--font-mono)' }}>上傳中…</span>}
              {done && <span style={{ fontSize: 11, color: 'var(--correct)', fontFamily: 'var(--font-mono)', fontWeight: 700 }}>✓ 已更新</span>}
            </div>
          </div>
        )}

        <input ref={fileInputRef} type="file" accept="image/jpeg,image/png" style={{ display: 'none' }} onChange={e => handleFile(e.target.files[0])}/>

        <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
          <button onClick={onClose} className="btn-pill ghost" style={{ flex: 1, padding: '12px', fontSize: 13 }}>取消</button>
          <button onClick={handleConfirm} disabled={!file || uploading || done}
            className={`btn-pill ${file && !uploading && !done ? 'primary' : 'disabled'}`}
            style={{ flex: 1, padding: '12px', fontSize: 13 }}>
            {uploading ? '上傳中…' : done ? '✓ 完成' : '確認更換'}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Helpers ───────────────────────────────────────────────────────────────────
function dockSecBtn() {
  return {
    width: 44, height: 44, borderRadius: 999,
    background: 'var(--surface-2)', color: 'var(--ink-2)',
    border: 'none', display: 'grid', placeItems: 'center',
    flexShrink: 0, cursor: 'pointer',
  };
}

function handleShare(quiz) {
  const base = window.location.origin + window.location.pathname;
  const url = quiz?.id ? `${base}#quiz=${quiz.id}` : base;
  if (navigator.share) {
    navigator.share({ title: quiz?.title || 'VisiListen', url }).catch(() => {});
  } else {
    navigator.clipboard?.writeText(url).catch(() => {});
  }
}

// ── Boot ──────────────────────────────────────────────────────────────────────
async function resizeImage(file, maxDim = 1024) {
  return new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      const img = new Image();
      img.onload = () => {
        let { width, height } = img;
        if (width > maxDim || height > maxDim) {
          const ratio = Math.min(maxDim / width, maxDim / height);
          width = Math.round(width * ratio);
          height = Math.round(height * ratio);
        } else {
          return resolve(file);
        }
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, width, height);
        canvas.toBlob((blob) => {
          if (blob) {
            resolve(new File([blob], file.name, { type: 'image/jpeg', lastModified: Date.now() }));
          } else {
            resolve(file);
          }
        }, 'image/jpeg', 0.85);
      };
      img.onerror = () => resolve(file);
      img.src = e.target.result;
    };
    reader.onerror = () => resolve(file);
    reader.readAsDataURL(file);
  });
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
