const { useState, useEffect, useMemo, useRef } = React;

// ─── Tweak defaults (editable via Tweaks panel) ──────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "brandName": "GradeCheck",
  "brandAccent": "Check",
  "logoColor": "#16a34a",
  "accentColor": "#16a34a",
  "freeLimit": 1,
  "featuredPlan": "student"
} /*EDITMODE-END*/;

// ─── Grading systems ──────────────────────────────────────────────────────
const SYSTEMS = {
  usa: {
    id: "usa", name: "USA Letter", flag: "US", scale: "A–F",
    bands: [
    { grade: "A+", min: 97 }, { grade: "A", min: 93 }, { grade: "A-", min: 90 },
    { grade: "B+", min: 87 }, { grade: "B", min: 83 }, { grade: "B-", min: 80 },
    { grade: "C+", min: 77 }, { grade: "C", min: 73 }, { grade: "C-", min: 70 },
    { grade: "D+", min: 67 }, { grade: "D", min: 63 }, { grade: "D-", min: 60 },
    { grade: "F", min: 0 }],

    instruction: "Return one of A+, A, A-, B+, B, B-, C+, C, C-, D+, D, D-, F."
  },
  danish: {
    id: "danish", name: "Danish 7-trins", flag: "DK", scale: "12 → −3",
    bands: [
    { grade: "12", min: 95 }, { grade: "10", min: 85 }, { grade: "7", min: 70 },
    { grade: "4", min: 55 }, { grade: "02", min: 40 }, { grade: "00", min: 20 },
    { grade: "−3", min: 0 }],

    instruction: "Return one of: 12, 10, 7, 4, 02, 00, −3."
  },
  uk: {
    id: "uk", name: "UK Honours", flag: "UK", scale: "1st → Fail",
    bands: [
    { grade: "1st", min: 70 }, { grade: "2:1", min: 60 },
    { grade: "2:2", min: 50 }, { grade: "3rd", min: 40 }, { grade: "Fail", min: 0 }],

    instruction: "Return one of: 1st, 2:1, 2:2, 3rd, Fail."
  },
  ib: {
    id: "ib", name: "IB Diploma", flag: "IB", scale: "7 → 1",
    bands: [
    { grade: "7", min: 85 }, { grade: "6", min: 75 }, { grade: "5", min: 65 },
    { grade: "4", min: 50 }, { grade: "3", min: 35 }, { grade: "2", min: 20 }, { grade: "1", min: 0 }],

    instruction: "Return a single digit from 1 to 7."
  },
  german: {
    id: "german", name: "German Noten", flag: "DE", scale: "1 → 6",
    bands: [
    { grade: "1", min: 90 }, { grade: "2", min: 75 }, { grade: "3", min: 60 },
    { grade: "4", min: 45 }, { grade: "5", min: 20 }, { grade: "6", min: 0 }],

    instruction: "Return a single digit from 1 to 6."
  },
  percent: {
    id: "percent", name: "Percent", flag: "%", scale: "0–100",
    bands: [
    { grade: "90+", min: 90 }, { grade: "75+", min: 75 },
    { grade: "60+", min: 60 }, { grade: "50+", min: 50 }, { grade: "<50", min: 0 }],

    instruction: "Return a percentage from 0 to 100, e.g. '78'."
  }
};

const SUBJECTS = ["Own Language", "English", "History", "Math", "Science", "Programming", "Other Language", "General"];

const PLANS = [
{ id: "free", name: "Free", price: "$0", per: "forever", limit: 1, features: ["1 grade / week", "All grading systems", "No saved history"] },
{ id: "student", name: "Student", price: "$4.99", per: "month", limit: 10, badge: "Most popular", features: ["10 grades / week", "Full dashboard & history", "Final-grade projection", "Cancel anytime"] },
{ id: "pro", name: "Pro", price: "$9.99", per: "month", limit: 30, features: ["30 grades / week", "Detailed analytics", "Export grades to CSV", "Rubric library"] }];


// ─── helpers ─────────────────────────────────────────────────────────────
const LS_USER = "mr:user";
const LS_ENTRIES = "mr:entries";
const LS_FREE_USED = "mr:freeUsed";
const LS_DAILY = "mr:daily"; // { date: 'YYYY-MM-DD', count: N }

// Backend endpoints are served from the same origin (Vercel /api/*).
const DAILY_LIMIT = 1; // free tier: 1/week

// Weekly key: year + ISO week number
const weekKey = () => {
  const d = new Date();
  const jan1 = new Date(d.getFullYear(), 0, 1);
  const week = Math.ceil(((d - jan1) / 86400000 + jan1.getDay() + 1) / 7);
  return `${d.getFullYear()}-W${week}`;
};
const todayKey = weekKey; // alias so existing code still works
const loadDaily = () => {
  try {
    const d = JSON.parse(localStorage.getItem(LS_DAILY) || "null");
    if (!d || d.date !== weekKey()) return { date: weekKey(), count: 0 };
    return d;
  } catch {return { date: weekKey(), count: 0 };}
};
const saveDaily = (d) => localStorage.setItem(LS_DAILY, JSON.stringify(d));

const loadUser = () => {try {return JSON.parse(localStorage.getItem(LS_USER) || "null");} catch {return null;}};
const saveUser = (u) => localStorage.setItem(LS_USER, JSON.stringify(u));
const clearUser = () => localStorage.removeItem(LS_USER);
const loadEntries = () => {try {return JSON.parse(localStorage.getItem(LS_ENTRIES) || "[]");} catch {return [];}};
const saveEntries = (e) => localStorage.setItem(LS_ENTRIES, JSON.stringify(e));
const loadFreeUsed = () => Number(localStorage.getItem(LS_FREE_USED) || 0);
const saveFreeUsed = (n) => localStorage.setItem(LS_FREE_USED, String(n));

// ─── user prefs (defaults that follow the user across sessions) ─────────
const LS_PREFS = "mr:prefs";
const DEFAULT_PREFS = {
  defaultSystem: "usa",
  defaultStrictness: "Fair",
  defaultSubject: "Own Language",
  autoSave: true,        // signed-in users: auto-save graded work to dashboard
  showAiTips: true,      // show inline coaching hints after a grade
  reduceMotion: false,   // damp transitions
  compactMode: false,    // tighter spacing throughout the app
};
const loadPrefs = () => {
  try {
    return { ...DEFAULT_PREFS, ...(JSON.parse(localStorage.getItem(LS_PREFS) || "{}") || {}) };
  } catch { return { ...DEFAULT_PREFS }; }
};
const savePrefs = (p) => localStorage.setItem(LS_PREFS, JSON.stringify(p));

const initials = (name) => name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]).join("").toUpperCase() || "?";

const scoreToGrade = (score, sysId) => {
  const sys = SYSTEMS[sysId];
  if (sysId === "percent") return `${Math.round(score)}%`;
  for (const b of sys.bands) if (score >= b.min) return b.grade;
  return sys.bands[sys.bands.length - 1].grade;
};

// ─── Logo ────────────────────────────────────────────────────────────────
function Logo({ size = 28, color = "#16a34a" }) {
  // Black paper + green check — black & green brand match
  return (
    <div className="logo" style={{ width: size, height: size }}>
      <svg viewBox="0 0 32 32" width={size} height={size}>
        {/* paper (black) */}
        <rect x="6" y="4" width="17" height="22" rx="2" fill="#0f0f0f" stroke="#0f0f0f" strokeWidth="1.5" />
        {/* text lines on paper (green, subtle) */}
        <line x1="9" y1="9" x2="19" y2="9" stroke={color} strokeWidth="1" strokeLinecap="round" opacity="0.55" />
        <line x1="9" y1="12.5" x2="17" y2="12.5" stroke={color} strokeWidth="1" strokeLinecap="round" opacity="0.55" />
        <line x1="9" y1="16" x2="19" y2="16" stroke={color} strokeWidth="1" strokeLinecap="round" opacity="0.55" />
        {/* green check stamp */}
        <circle cx="22" cy="22" r="7.5" fill={color} />
        <path
          d="M18.5 22 L21 24.5 L25.5 19.5"
          stroke="#0f0f0f"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
          fill="none" />
      </svg>
    </div>);

}

function WordMark({ size = 18, name = "GradeCheck", accent = "Check", color = "#16a34a" }) {
  // split the name into [pre, accent, post] and color the accent portion
  const i = accent ? name.indexOf(accent) : -1;
  if (i < 0) {
    return <span className="wordmark" style={{ fontSize: size }}>{name}</span>;
  }
  return (
    <span className="wordmark" style={{ fontSize: size }}>
      {name.slice(0, i)}
      <span className="wordmark-r" style={{ color }}>{name.slice(i, i + accent.length)}</span>
      {name.slice(i + accent.length)}
    </span>);

}

// ─── Auth Modal (gated, not full screen) ─────────────────────────────────
function AuthModal({ reason, onAuth, onClose, tweaks }) {
  const [mode, setMode] = useState("signup");
  // step: "credentials" | "code" | "forgot" | "forgot-code"
  const [step, setStep] = useState("credentials");
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [code, setCode] = useState("");
  const [newPassword, setNewPassword] = useState("");

  const [authErr, setAuthErr] = useState("");
  const [authBusy, setAuthBusy] = useState(false);

  const finalizeAuth = (user) => {
    saveUser(user);
    if (mode === "signup") {
      try {
        const k = "mr:daily";
        const d = JSON.parse(localStorage.getItem(k) || "{}");
        if (d && typeof d.count === "number" && d.count > 0) {
          d.count = Math.max(0, d.count - 1);
          localStorage.setItem(k, JSON.stringify(d));
        }
      } catch {}
      fetch("/api/welcome", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email, name: user.name })
      }).catch(() => {});
    }
    onAuth(user);
  };

  const submitCredentials = async (e) => {
    e.preventDefault();
    if (!email.trim() || !password) return;
    setAuthErr("");
    setAuthBusy(true);
    try {
      const endpoint = mode === "signup" ? "/api/signup" : "/api/login";
      const r = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: email.trim(), password, name: name.trim() }),
      });
      const data = await r.json().catch(() => ({}));
      setAuthBusy(false);
      if (!r.ok) {
        setAuthErr(data.error || "Auth failed");
        return;
      }
      if (data.needsCode) {
        setStep("code");
        setCode("");
        return;
      }
      if (data.user) finalizeAuth(data.user);
    } catch {
      setAuthBusy(false);
      setAuthErr("Could not reach server. Check your connection and try again.");
    }
  };

  const submitCode = async (e) => {
    e.preventDefault();
    if (!code.trim()) return;
    setAuthErr("");
    setAuthBusy(true);
    try {
      const r = await fetch("/api/verify", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: email.trim().toLowerCase(), code: code.trim() }),
      });
      const data = await r.json().catch(() => ({}));
      setAuthBusy(false);
      if (!r.ok) {
        setAuthErr(data.error || "Verification failed");
        return;
      }
      if (data.user) finalizeAuth(data.user);
    } catch {
      setAuthBusy(false);
      setAuthErr("Could not reach server. Try again.");
    }
  };

  const switchMode = (m) => {
    setMode(m);
    setStep("credentials");
    setAuthErr("");
    setCode("");
  };

  const requestReset = async () => {
    if (!email.trim()) { setAuthErr("Enter your email first"); return; }
    setAuthErr("");
    setAuthBusy(true);
    try {
      const r = await fetch("/api/forgot", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: email.trim() }),
      });
      const data = await r.json().catch(() => ({}));
      setAuthBusy(false);
      if (!r.ok) { setAuthErr(data.error || "Could not send code"); return; }
      setStep("forgot-code");
      setCode("");
    } catch {
      setAuthBusy(false);
      setAuthErr("Could not reach server. Try again.");
    }
  };

  const submitReset = async (e) => {
    e.preventDefault();
    if (!code.trim() || !newPassword) return;
    setAuthErr("");
    setAuthBusy(true);
    try {
      const r = await fetch("/api/reset", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: email.trim().toLowerCase(), code: code.trim(), password: newPassword }),
      });
      const data = await r.json().catch(() => ({}));
      setAuthBusy(false);
      if (!r.ok) { setAuthErr(data.error || "Reset failed"); return; }
      if (data.user) finalizeAuth(data.user);
    } catch {
      setAuthBusy(false);
      setAuthErr("Could not reach server. Try again.");
    }
  };

  return (
    <div className="modal-scrim" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <button className="modal-x" onClick={onClose} aria-label="Close">×</button>
        <div className="modal-logo">
          <Logo size={32} color={tweaks.logoColor} />
          <WordMark size={20} name={tweaks.brandName} accent={tweaks.brandAccent} color={tweaks.logoColor} />
        </div>
        <h2 className="modal-h">{
          step === "code" ? "Check your email" :
          step === "forgot" ? "Reset password" :
          step === "forgot-code" ? "Check your email" :
          (mode === "signup" ? "Get 1 free grade per week" : (reason?.title || "Welcome back"))
        }</h2>
        <p className="modal-d">{
          step === "code" ? `We sent a 6-digit code to ${email}. Enter it below.` :
          step === "forgot" ? "Enter your email — we'll send a 6-digit code to reset your password." :
          step === "forgot-code" ? `If an account exists for ${email}, a code is on the way.` :
          (mode === "signup" ? "Create a free account to get 1 free grade per week, forever." : (reason?.detail || "Log in to get 1 free grade per week, forever."))
        }</p>

        {step === "credentials" && (
          <div className="auth-tabs">
            <button className={"auth-tab" + (mode === "signup" ? " auth-tab--on" : "")} onClick={() => switchMode("signup")}>Sign up</button>
            <button className={"auth-tab" + (mode === "login" ? " auth-tab--on" : "")} onClick={() => switchMode("login")}>Log in</button>
          </div>
        )}

        {step === "credentials" ? (
          <form className="auth-form" onSubmit={submitCredentials}>
            {mode === "signup" &&
            <label>
                Name
                <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Alex Doe" />
              </label>
            }
            <label>
              Email
              <input type="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@school.edu" />
            </label>
            <label>
              Password
              <input type="password" required value={password} onChange={(e) => setPassword(e.target.value)} placeholder="••••••••" />
            </label>
            <button type="submit" className="auth-submit" disabled={authBusy}>
              {authBusy ? "Sending code…" : mode === "signup" ? "Create account — it's free" : "Log in"}
            </button>
            {mode === "login" && (
              <button
                type="button"
                onClick={() => { setStep("forgot"); setAuthErr(""); setCode(""); setNewPassword(""); }}
                style={{background:"none",border:"none",color:"#666",fontSize:12,cursor:"pointer",marginTop:2,textAlign:"center"}}
              >
                Forgot password?
              </button>
            )}
            {authErr && <div style={{color:"#c00",fontSize:13,marginTop:6}}>{authErr}</div>}
          </form>
        ) : step === "code" ? (
          <form className="auth-form" onSubmit={submitCode}>
            <label>
              Verification code
              <input
                type="text"
                required
                inputMode="numeric"
                pattern="[0-9]*"
                maxLength={6}
                autoFocus
                value={code}
                onChange={(e) => setCode(e.target.value.replace(/\D/g,"").slice(0,6))}
                placeholder="123456"
                style={{letterSpacing:"6px",fontSize:"20px",textAlign:"center"}}
              />
            </label>
            <button type="submit" className="auth-submit" disabled={authBusy || code.length < 6}>
              {authBusy ? "Verifying…" : "Verify & continue"}
            </button>
            <button
              type="button"
              onClick={() => { setStep("credentials"); setAuthErr(""); }}
              style={{background:"none",border:"none",color:"#666",fontSize:13,cursor:"pointer",marginTop:4}}
            >
              ← Use a different email
            </button>
            {authErr && <div style={{color:"#c00",fontSize:13,marginTop:6}}>{authErr}</div>}
          </form>
        ) : step === "forgot" ? (
          <form className="auth-form" onSubmit={(e) => { e.preventDefault(); requestReset(); }}>
            <label>
              Email
              <input type="email" required autoFocus value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@school.edu" />
            </label>
            <button type="submit" className="auth-submit" disabled={authBusy}>
              {authBusy ? "Sending…" : "Send reset code"}
            </button>
            <button
              type="button"
              onClick={() => { setStep("credentials"); setAuthErr(""); }}
              style={{background:"none",border:"none",color:"#666",fontSize:13,cursor:"pointer",marginTop:4}}
            >
              ← Back to log in
            </button>
            {authErr && <div style={{color:"#c00",fontSize:13,marginTop:6}}>{authErr}</div>}
          </form>
        ) : (
          /* forgot-code: enter code + new password */
          <form className="auth-form" onSubmit={submitReset}>
            <label>
              Verification code
              <input
                type="text"
                required
                inputMode="numeric"
                pattern="[0-9]*"
                maxLength={6}
                autoFocus
                value={code}
                onChange={(e) => setCode(e.target.value.replace(/\D/g,"").slice(0,6))}
                placeholder="123456"
                style={{letterSpacing:"6px",fontSize:"20px",textAlign:"center"}}
              />
            </label>
            <label>
              New password
              <input type="password" required value={newPassword} onChange={(e) => setNewPassword(e.target.value)} placeholder="••••••••" />
            </label>
            <button type="submit" className="auth-submit" disabled={authBusy || code.length < 6 || newPassword.length < 6}>
              {authBusy ? "Resetting…" : "Set new password"}
            </button>
            <button
              type="button"
              onClick={() => { setStep("forgot"); setAuthErr(""); }}
              style={{background:"none",border:"none",color:"#666",fontSize:13,cursor:"pointer",marginTop:4}}
            >
              ← Use a different email
            </button>
            {authErr && <div style={{color:"#c00",fontSize:13,marginTop:6}}>{authErr}</div>}
          </form>
        )}
        <p className="modal-foot">No card required · 1 free grade every week</p>
      </div>
    </div>);

}

// ─── Pricing Modal ──────────────────────────────────────────────────────
function PricingModal({ user, onClose, onUpgrade, onManageBilling, tweaks }) {
  const featuredId = tweaks?.featuredPlan || "student";
  const isPaid = user?.plan === "student" || user?.plan === "pro";
  return (
    <div className="modal-scrim" onClick={onClose}>
      <div className="modal modal--wide" onClick={(e) => e.stopPropagation()}>
        <button className="modal-x" onClick={onClose} aria-label="Close">×</button>
        <h2 className="modal-h" style={{ textAlign: "center" }}>Pick a plan</h2>
        <p className="modal-d" style={{ textAlign: "center" }}>
          {isPaid
            ? "Manage your subscription in the billing portal — change card, see invoices, or cancel."
            : "Cancel anytime. Cheap as a coffee."}
        </p>

        <div className="plans">
          {PLANS.map((p) => {
            const current = user?.plan === p.id;
            const rank = { free: 0, student: 1, pro: 2 };
            const userRank = rank[user?.plan || "free"];
            const isUpgrade = rank[p.id] > userRank;
            const isDowngrade = rank[p.id] < userRank && user;
            return (
              <div key={p.id} className={"plan" + (p.id === featuredId ? " plan--featured" : "") + (current ? " plan--current" : "")}>
                {current
                  ? <span className="plan-badge plan-badge--current">Your plan</span>
                  : p.id === featuredId && <span className="plan-badge">Most popular</span>}
                <div className="plan-name">{p.name}</div>
                <div className="plan-price">
                  <span className="plan-price-n">{p.price}</span>
                  <span className="plan-price-p">/ {p.per}</span>
                </div>
                <ul className="plan-features">
                  {p.features.map((f, i) =>
                  <li key={i}><span className="plan-check">✓</span> {f}</li>
                  )}
                </ul>
                {current ? (
                  <button className="plan-btn" disabled>Current plan</button>
                ) : isDowngrade ? (
                  <button className="plan-btn plan-btn--quiet" onClick={onManageBilling}>
                    Manage in billing →
                  </button>
                ) : (
                  <button
                    className={"plan-btn" + (p.id === featuredId ? " plan-btn--primary" : "")}
                    onClick={() => isPaid && isUpgrade ? onManageBilling() : onUpgrade(p.id)}>
                    {isPaid && isUpgrade ? "Switch to " + p.name + " →" : ("Choose " + p.name)}
                  </button>
                )}
              </div>);
          })}
        </div>
      </div>
    </div>);
}

// ─── Header ──────────────────────────────────────────────────────────────
function Header({ user, view, onView, onLogout, onShowAuth, onShowPricing, onManageBilling, freeLeft, tweaks }) {
  const [open, setOpen] = useState(false);
  return (
    <header className="hdr">
      <div className="hdr-l">
        <button className="brand" onClick={() => onView("grade")}>
          <Logo size={28} color={tweaks.logoColor} />
          <WordMark size={17} name={tweaks.brandName} accent={tweaks.brandAccent} color={tweaks.logoColor} />
        </button>
        {user &&
        <div className="tabs">
            <button className={"tab" + (view === "grade" ? " tab--on" : "")} onClick={() => onView("grade")}>Grade</button>
            <button className={"tab" + (view === "dash" ? " tab--on" : "")} onClick={() => onView("dash")}>
              Dashboard{user.plan === "free" && <span style={{ marginLeft: 6, opacity: 0.7 }}>🔒</span>}
            </button>
          </div>
        }
      </div>
      <div className="hdr-r">
        {!user &&
        <span className="free-tag">Free plan</span>
        }
        {user && user.plan === "free" &&
        <button className="upgrade-btn" onClick={onShowPricing}>
            <span className="upgrade-spark">✦</span> Upgrade
          </button>
        }
        {!user ?
        <button className="login-btn" onClick={() => onShowAuth({ title: "Welcome back", detail: "Log in to get 1 free grade per week, forever." })}>Log in</button> :

        <div style={{ position: "relative" }}>
            <button className="user-btn" onClick={() => setOpen((o) => !o)}>
              <span className="user-avatar">{initials(user.name)}</span>
              <span className="user-name">{user.name.split(" ")[0]}</span>
            </button>
            {open &&
          <>
                <div className="modal-scrim modal-scrim--silent" onClick={() => setOpen(false)} />
                <div className="user-menu">
                  <div className="user-menu-h">
                    <div className="user-menu-h-em">{user.name}</div>
                    <div>{user.email}</div>
                    <div className="user-menu-plan">
                      <span className={"plan-pill plan-pill--" + user.plan}>{user.plan === "free" ? "Free" : user.plan === "student" ? "Student" : "Pro"}</span>
                    </div>
                  </div>
                  <button onClick={() => {setOpen(false);onView("settings");}}>Settings</button>
                  <button onClick={() => {setOpen(false);onManageBilling();}}>
                    {user.plan === "free" ? "Plans & billing" : "Billing & invoices"}
                  </button>
                  <button onClick={onLogout}>Log out</button>
                </div>
              </>
          }
          </div>
        }
      </div>
    </header>);

}

// ─── System picker ───────────────────────────────────────────────────────
function SystemPicker({ system, onPick, hideLabel }) {
  const [open, setOpen] = useState(false);
  return (
    <div className="sys-row" style={{ position: "relative" }}>
      {!hideLabel && <span className="sys-label">Grading template</span>}
      <div style={{ position: "relative", display: "inline-block" }}>
        <button className="sys-btn" onClick={() => setOpen((o) => !o)}>
          <span className="sys-btn-flag">{system.flag}</span>
          <span>{system.name}</span>
          <span style={{ color: "var(--ink-3)", fontSize: 11 }}>{system.scale}</span>
          <svg width="11" height="11" viewBox="0 0 12 12">
            <path d="M3 4.5 L6 7.5 L9 4.5" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </button>
        {/* Native select overlaid on mobile only — invisible but tappable */}
        <select
          value={system.id}
          onChange={(e) => onPick(e.target.value)}
          style={{
            position: "absolute", inset: 0, opacity: 0,
            width: "100%", height: "100%", cursor: "pointer",
            display: "none"
          }}
          className="sys-native-select">
          {Object.values(SYSTEMS).map((s) =>
          <option key={s.id} value={s.id}>{s.flag} {s.name} ({s.scale})</option>
          )}
        </select>
      </div>
      {open &&
      <>
        <div className="modal-scrim modal-scrim--silent" onClick={() => setOpen(false)} />
        <div className="sys-menu">
          {Object.values(SYSTEMS).map((s) =>
          <button
            key={s.id}
            className="sys-item"
            onClick={() => {onPick(s.id);setOpen(false);}}>
              <span className="sys-item-flag">{s.flag}</span>
              <span className="sys-item-name">{s.name}</span>
              <span className="sys-item-scale">{s.scale}</span>
              {s.id === system.id && <span className="sys-item-check">✓</span>}
            </button>
          )}
        </div>
      </>
      }
    </div>);
}

// ─── Subject picker (matches SystemPicker style) ─────────────────────────
const SUBJECT_ICONS = {
  "Own Language": "✍︎",
  "English": "En",
  "History": "⏱ ",
  "Math": "π",
  "Science": "⚛︎",
  "Programming": "{ }",
  "Other Language": "A Å",
  "General": "•"
};

function SubjectPicker({ subject, onPick, disabled }) {
  const [open, setOpen] = useState(false);
  return (
    <div style={{ position: "relative", display: "inline-block", alignSelf: "flex-start" }}>
      <button className="sys-btn" onClick={() => !disabled && setOpen((o) => !o)} disabled={disabled}>
        <span className="sys-btn-flag">{SUBJECT_ICONS[subject] || "•"}</span>
        <span>{subject}</span>
        <svg width="11" height="11" viewBox="0 0 12 12">
          <path d="M3 4.5 L6 7.5 L9 4.5" stroke="currentColor" strokeWidth="1.4" fill="none" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </button>
      <select
        value={subject}
        onChange={(e) => onPick(e.target.value)}
        disabled={disabled}
        style={{
          position: "absolute", inset: 0, opacity: 0,
          width: "100%", height: "100%", cursor: "pointer",
          display: "none"
        }}
        className="sys-native-select">
        {SUBJECTS.map((s) => <option key={s} value={s}>{s}</option>)}
      </select>
      {open &&
      <>
        <div className="modal-scrim modal-scrim--silent" onClick={() => setOpen(false)} />
        <div className="sys-menu sys-menu--subject">
          {SUBJECTS.map((s) =>
          <button
            key={s}
            className="sys-item"
            onClick={() => {onPick(s);setOpen(false);}}>
              <span className="sys-item-flag">{SUBJECT_ICONS[s] || "•"}</span>
              <span className="sys-item-name">{s}</span>
              {s === subject && <span className="sys-item-check">✓</span>}
            </button>
          )}
        </div>
      </>
      }
    </div>);
}

// ─── Grade view ──────────────────────────────────────────────────────────
function GradeView({ system, setSystemId, user, freeLeft, weeklyLeft, onGradeUsed, onSaveAttempt, onShowPricing, prefs }) {
  const [text, setText] = useState("");
  const [subject, setSubject] = useState(prefs?.defaultSubject || "Own Language");
  const [strictness, setStrictness] = useState(prefs?.defaultStrictness || "Fair");
  const [rubric, setRubric] = useState("");
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);
  const [saved, setSaved] = useState(false);
  const [saveName, setSaveName] = useState("");

  const wordCount = useMemo(() => text.trim() ? text.trim().split(/\s+/).length : 0, [text]);
  const ready = text.trim().length >= 20 && !loading;
  const blocked = !user && freeLeft <= 0;

  const grade = async () => {
    if (blocked) {
      onGradeUsed("blocked");
      return;
    }
    // Limit check: free users (signed-out OR plan=free) gated by SERVER (per IP/network)
    const isFreeTier = !user || user.plan === "free";
    if (isFreeTier) {
      try {
        const qs = user ? "?email=" + encodeURIComponent(user.email) : "";
        const r = await fetch("/api/usage" + qs);
        const u = await r.json();
        if (u.remaining <= 0) {
          setError(`Weekly limit reached (${u.limit} grade/week). Upgrade for more.`);
          return;
        }
      } catch {}
    } else {
      // Paid users: keep client-side per-plan cap
      const planLimit = user.plan === "pro" ? 30 : 10;
      const daily = loadDaily();
      if (daily.count >= planLimit) {
        setError(`Weekly limit reached (${planLimit} grades/week). Resets next week.`);
        return;
      }
    }
    setLoading(true);setError(null);setResult(null);setSaved(false);

    try {
      const work = text.trim();
      const words = work.split(/\s+/).filter(Boolean);
      const wc = words.length;

      // ── Call our Vercel serverless function (key stays server-side) ──
      const r = await fetch("/api/grade", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          text: work,
          subject,
          strictness,
          rubric: rubric.trim(),
          system: { name: system.name, instruction: system.instruction, bands: system.bands }
        })
      });
      if (!r.ok) {
        const err = await r.json().catch(() => ({}));
        throw new Error(err.error || "Backend error " + r.status);
      }
      const data = await r.json();

      const result = {
        grade: String(data.grade ?? "?"),
        label: String(data.label ?? ""),
        score: typeof data.score === "number" ? Math.round(data.score) : 50,
        summary: String(data.summary ?? ""),
        strengths: Array.isArray(data.strengths) ? data.strengths.slice(0, 4) : [],
        weaknesses: Array.isArray(data.weaknesses) ? data.weaknesses.slice(0, 4) : [],
        feedback: String(data.feedback ?? "")
      };
      setResult(result);

      // Increment usage: server-side for free tier, local for paid users
      if (isFreeTier) {
        fetch("/api/usage", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(user ? { email: user.email } : {}),
        }).catch(() => {});
      } else {
        const d = loadDaily();
        saveDaily({ date: d.date, count: d.count + 1 });
      }

      const preview = words.slice(0, 5).join(" ");
      setSaveName(`${subject} — ${preview}${wc > 5 ? "…" : ""}`);
      onGradeUsed("success");
    } catch (err) {
      console.error(err);
      setError("Couldn't grade — " + (err.message || "try again."));
    } finally {
      setLoading(false);
    }
  };

  const save = () => {
    if (!result) return;
    if (!user) {
      onSaveAttempt(result, saveName, subject, system.id);
      return;
    }
    const entries = loadEntries();
    const entry = {
      id: Date.now().toString(36),
      name: saveName.trim() || `${subject} — untitled`,
      subject,
      systemId: system.id,
      grade: result.grade,
      label: result.label,
      score: result.score,
      // Persist the original submitted text so the dashboard can show it back.
      // Capped to keep entry records reasonably small.
      text: (text || "").slice(0, 8000),
      // Persist the rubric (if any) — useful context when you revisit the entry.
      rubric: (rubric || "").slice(0, 2000),
      // Persist feedback so the dashboard can show context per entry.
      summary: result.summary || "",
      feedback: result.feedback || "",
      strengths: Array.isArray(result.strengths) ? result.strengths : [],
      weaknesses: Array.isArray(result.weaknesses) ? result.weaknesses : [],
      // actualScore/actualGrade: user-entered override after they get the real grade back.
      actualScore: null,
      actualGrade: null,
      weight: 1,
      created: new Date().toISOString()
    };
    entries.unshift(entry);
    saveEntries(entries);
    // Mirror to KV (best-effort)
    if (user?.email) {
      fetch("/api/save", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email, entry: { ...entry, result } }),
      }).catch(() => {});
    }
    setSaved(true);
  };

  return (
    <div className="main-grade">
      <section className="panel">
        <SystemPicker system={system} onPick={setSystemId} />

        {/* Ad slot — only for logged-in free users */}
        {user && user.plan === "free" &&
        <button
          type="button"
          onClick={onShowPricing}
          style={{
            margin: "0 0 12px",
            padding: "10px 12px",
            border: "1px dashed #2a2a2a",
            borderRadius: 8,
            background: "#0c0c0c",
            color: "#888",
            fontSize: 12,
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            width: "100%",
            cursor: "pointer",
            font: "inherit",
            textAlign: "left"
          }}>
            <span>Ad · Go ad-free with Student or Pro</span>
            <span style={{ color: "#16a34a" }}>Upgrade →</span>
          </button>
        }

        <textarea
          className="ta"
          placeholder="Paste your essay, answer, or test response here…"
          value={text}
          onChange={(e) => setText(e.target.value)}
          disabled={loading} />
        
        <div className="ta-meta">
          <span>{wordCount} words</span>
          <span className="ta-hint">{
            ["Own Language", "English"].includes(subject)
              ? "Scored on length, vocabulary, sentence rhythm and structure."
              : "Scored on correctness, method and explanation."
          }</span>
        </div>

        <div className="field-row">
          <div className="field">
            <span className="field-l">Subject</span>
            <SubjectPicker subject={subject} onPick={setSubject} disabled={loading} />
          </div>
          <label className="field">
            <span className="field-l">Strictness</span>
            <div className="strict-row">
              {["Lenient", "Fair", "Harsh"].map((s) =>
              <button
                key={s}
                className={"chip" + (strictness === s ? " chip--on" : "")}
                onClick={() => setStrictness(s)}
                disabled={loading} style={{ backgroundColor: "rgb(255, 255, 255)", color: "rgb(0, 0, 0)" }}>
                {s}</button>
              )}
            </div>
          </label>
        </div>

        <details className="rubric">
          <summary>+ Add rubric (optional)</summary>
          <textarea
            className="ta ta--small"
            style={{ marginTop: 8 }}
            placeholder="Assignment prompt or grading criteria…"
            value={rubric}
            onChange={(e) => setRubric(e.target.value)}
            disabled={loading} />
          
        </details>

        <button className="go" onClick={grade} disabled={!ready}>
          {loading ? <><span className="go-spin" /> Grading…</> : blocked ? "Sign up to keep grading →" : "Grade my work"}
        </button>
        {!loading &&
        <div className="free-hint">
            {text.trim().length < 20 ?
            "Type or paste your work above to grade." :
          !user ? (weeklyLeft > 0 ?
            `${weeklyLeft} free ${weeklyLeft === 1 ? "grade" : "grades"} · no card needed` :
            "Free grade used up — sign up to keep going.") :
          null}
          </div>
        }
      </section>

      <section className="panel">
        {!result && !loading &&
        <div className="result-empty">
            <div>
              <div className="result-empty-t">No grade yet</div>
              <div className="result-empty-d">Paste your work and tap Grade.</div>
            </div>
          </div>
        }

        {loading &&
        <div className="loading-wrap">
            <div className="sk sk-num" />
            <div className="sk sk-bar" />
            <div className="sk sk-bar sk-bar--med" />
            <div className="sk sk-bar sk-bar--short" />
          </div>
        }

        {result && !loading &&
        <>
            <div className="grade-big">
              <div className="grade-big-num">{scoreToGrade(result.score, system.id)}</div>
              <div className="grade-big-meta">
                <div className="grade-big-lbl">{result.label}</div>
                <div className="grade-big-sub">{system.name}</div>
              </div>
            </div>

            <div className="score-row">
              <span className="score-row-l">Score</span>
              <div className="score-bar">
                <div className="score-bar-fill" style={{ width: `${Math.max(2, Math.min(100, result.score))}%` }} />
              </div>
              <span className="score-row-n">{result.score}</span>
            </div>

            <div className="block">
              <div className="block-h">Summary</div>
              <p className="block-p">{result.summary}</p>
            </div>

            <div style={{ position: "relative" }}>
              <div style={{ filter: !user ? "blur(5px)" : "none", userSelect: !user ? "none" : "auto", pointerEvents: !user ? "none" : "auto" }}>
                <div className="block-grid">
                  {result.strengths.length > 0 &&
                <div className="block">
                    <div className="block-h">Strengths</div>
                    <ul className="bullets bullets--good">
                      {result.strengths.map((s, i) => <li key={i}>{s}</li>)}
                    </ul>
                  </div>
                }
                  {result.weaknesses.length > 0 &&
                <div className="block">
                    <div className="block-h">To improve</div>
                    <ul className="bullets bullets--bad">
                      {result.weaknesses.map((s, i) => <li key={i}>{s}</li>)}
                    </ul>
                  </div>
                }
                </div>
                {result.feedback &&
              <div className="block">
                  <div className="block-h">Feedback</div>
                  <p className="block-p">{result.feedback}</p>
                </div>
              }
              </div>
            </div>

            <div className="save-row">
              {!saved ?
            <>
                  <input
                className="save-name-input"
                value={saveName}
                onChange={(e) => setSaveName(e.target.value)}
                placeholder="Name this grade…" />
              
                  <button className="save-btn" onClick={save}>
                    ＋ Save to dashboard
                  </button>
                </> :

            <button className="save-btn save-btn--saved" disabled>✓ Saved</button>
            }
            </div>
          </>
        }
      </section>

      {error &&
      <div className="toast toast--err" onClick={() => setError(null)}>{error}</div>
      }
    </div>);

}

// ─── Dashboard ───────────────────────────────────────────────────────────
// Effective score honors the user's actualScore override (the grade they
// actually got handed back). Falls back to the AI-estimated score.
const effScore = (e) => (e.actualScore != null ? e.actualScore : (e.score || 0));
// Display grade in the currently-selected dashboard system, translated from
// the underlying 0-100 score. The entry's original system is only used inside
// the editor (where the chips come from its bands).
const effGradeIn = (e, sysId) => scoreToGrade(effScore(e), sysId);

// Tone (color band) derived from 0-100 score — used for the ring stroke.
const scoreTone = (score) => {
  if (score == null) return { from: "#cbd5e1", to: "#94a3b8" };
  if (score >= 90) return { from: "#22c55e", to: "#15803d" }; // green
  if (score >= 75) return { from: "#84cc16", to: "#4d7c0f" }; // lime
  if (score >= 60) return { from: "#eab308", to: "#a16207" }; // amber
  if (score >= 45) return { from: "#f97316", to: "#c2410c" }; // orange
  return { from: "#ef4444", to: "#b91c1c" };                  // red
};

// Greeting line tuned to the score band. Works for any grading system because
// it keys off the 0-100 score, not the localized letter.
const scoreGreeting = (score, firstName) => {
  const name = firstName ? `, ${firstName}` : "";
  if (score == null) return firstName ? `Welcome back${name}` : "Your projection";
  if (score >= 95) return `Outstanding${name}`;
  if (score >= 88) return `Excellent work${name}`;
  if (score >= 80) return `Strong run${name}`;
  if (score >= 72) return `Solid going${name}`;
  if (score >= 62) return `Steady progress${name}`;
  if (score >= 50) return `Room to grow${name}`;
  if (score >= 35) return `Time to dig in${name}`;
  return `Let's turn it around${name}`;
};

function Ring({ score, size = 180, stroke = 12, label, sub, mono = false }) {
  const r = (size - stroke) / 2;
  const C = 2 * Math.PI * r;
  const pct = Math.max(0, Math.min(100, score == null ? 0 : score));
  const offset = C - (pct / 100) * C;
  const id = React.useId();
  const tone = scoreTone(score);
  const stroker = mono ? "var(--ink)" : `url(#${id})`;
  return (
    <div className="ring" style={{ width: size, height: size }}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        <defs>
          <linearGradient id={id} x1="0" y1="0" x2="1" y2="1">
            <stop offset="0%" stopColor={tone.from} />
            <stop offset="100%" stopColor={tone.to} />
          </linearGradient>
        </defs>
        <circle cx={size / 2} cy={size / 2} r={r}
          stroke="var(--bg-3)" strokeWidth={stroke} fill="none" />
        <circle cx={size / 2} cy={size / 2} r={r}
          stroke={stroker} strokeWidth={stroke} fill="none"
          strokeDasharray={C} strokeDashoffset={score == null ? C : offset}
          strokeLinecap="round"
          transform={`rotate(-90 ${size / 2} ${size / 2})`}
          style={{ transition: "stroke-dashoffset .8s cubic-bezier(.2,.8,.2,1)" }} />
      </svg>
      <div className="ring-mid">
        <div className="ring-label" style={{ fontSize: size * 0.34 }}>{label}</div>
        {sub && <div className="ring-sub">{sub}</div>}
      </div>
    </div>
  );
}

function EntryTile({ entry, system, dragging, onDragStart, onDragEnd, onClick }) {
  const date = new Date(entry.created).toLocaleDateString(undefined, { month: "short", day: "numeric" });
  const overridden = entry.actualScore != null;
  // Translate the underlying 0-100 score into the currently-active system.
  const displayGrade = effGradeIn(entry, system.id);
  return (
    <div
      className={"tile" + (dragging ? " tile--drag" : "") + (overridden ? " tile--actual" : "")}
      draggable
      onDragStart={onDragStart}
      onDragEnd={onDragEnd}
      onClick={onClick}
      tabIndex={0}
      onKeyDown={(e) => { if (e.key === "Enter") onClick(); }}>
      <div className="tile-grade">
        <span className="tile-grade-v">{displayGrade}</span>
        {overridden && <span className="tile-grade-tag">actual</span>}
      </div>
      <div className="tile-body">
        <div className="tile-name">{entry.name}</div>
        <div className="tile-meta">
          <span>{date}</span>
          <span className="tile-meta-dot">·</span>
          <span>{system.flag}</span>
          {Number(entry.weight) !== 1 && (
            <>
              <span className="tile-meta-dot">·</span>
              <span>{entry.weight}× weight</span>
            </>
          )}
        </div>
      </div>
      <div className="tile-score">{Math.round(effScore(entry))}</div>
    </div>
  );
}

// Pick a representative score for a grade label inside a system. Uses the
// midpoint of the grade band so the projection averages stay sensible when
// the user enters a grade rather than a raw percent.
const gradeToScore = (gradeLabel, sysId) => {
  const sys = SYSTEMS[sysId];
  if (!sys) return null;
  for (let i = 0; i < sys.bands.length; i++) {
    if (sys.bands[i].grade === gradeLabel) {
      const min = sys.bands[i].min;
      const max = i === 0 ? 100 : Math.max(min, sys.bands[i - 1].min - 1);
      return Math.round((min + max) / 2);
    }
  }
  return null;
};

function EntryEditor({ entry, system, onSave, onClose, onDelete }) {
  const [name, setName] = useState(entry.name);
  const [subject, setSubject] = useState(entry.subject);
  const [weight, setWeight] = useState(entry.weight);
  const [actualGrade, setActualGrade] = useState(entry.actualGrade || null);
  const [actualScore, setActualScore] = useState(entry.actualScore != null ? entry.actualScore : "");

  // The editor follows the CURRENT dashboard system, not the entry's original
  // one. Chips, derived labels, and the AI-grade hint are all translated via
  // the underlying 0-100 score. Picking a chip locks actualScore to its band
  // midpoint in this system.
  const sys = system;
  const aiScore = Math.round(entry.score || 0);
  const aiGradeInCurrent = scoreToGrade(aiScore, sys.id);

  // If a chip is selected, derive score from it. Otherwise use the typed score.
  const resolved = useMemo(() => {
    if (actualGrade) return { grade: actualGrade, score: gradeToScore(actualGrade, sys.id) };
    if (actualScore === "" || actualScore == null) return { grade: null, score: null };
    const n = Math.max(0, Math.min(100, Number(actualScore)));
    return { grade: scoreToGrade(n, sys.id), score: n };
  }, [actualGrade, actualScore, sys.id]);

  // If the entry already has an actualScore, show what that grade is in the
  // current system (might differ from stored actualGrade if system changed).
  const storedActualGradeInCurrent = entry.actualScore != null
    ? scoreToGrade(entry.actualScore, sys.id) : null;

  const pickChip = (g) => {
    setActualGrade(g === actualGrade ? null : g);
    setActualScore(""); // chip is now the source of truth
  };
  const typeScore = (v) => {
    setActualScore(v);
    setActualGrade(null); // typing overrides the chip
  };

  const save = () => {
    onSave({
      name: name.trim() || entry.name,
      subject,
      weight: Math.max(0, Number(weight) || 0),
      actualScore: resolved.score,
      actualGrade: resolved.grade,
    });
    onClose();
  };

  const dateStr = new Date(entry.created).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" });
  const wordCount = entry.text ? entry.text.split(/\s+/).filter(Boolean).length : 0;
  const hasValue = resolved.grade != null;

  return (
    <div className="editor-backdrop" onClick={onClose}>
      <div className="editor" onClick={(e) => e.stopPropagation()}>
        <div className="editor-head">
          <input
            className="editor-title-input"
            value={name}
            onChange={(e) => setName(e.target.value)}
            spellCheck={false} />
          <button className="editor-close" onClick={onClose} aria-label="Close">×</button>
        </div>
        <div className="editor-sub">
          <span>{dateStr}</span>
          <span className="editor-sub-dot">·</span>
          <span className="editor-sub-sys">{sys.flag} {sys.name}</span>
          <span className="editor-sub-dot">·</span>
          {storedActualGradeInCurrent ? (
            <>
              <span className="editor-sub-actual">
                Your grade <strong>{storedActualGradeInCurrent}</strong>
              </span>
              <span className="editor-sub-dot">·</span>
              <span className="editor-sub-ai">AI {aiGradeInCurrent}</span>
            </>
          ) : (
            <span>AI graded <strong>{aiGradeInCurrent}</strong> · {aiScore}/100</span>
          )}
        </div>

        <div className="editor-body">
          {/* Subject + weight, single quiet row. */}
          <div className="editor-line">
            <span className="editor-line-l">Subject</span>
            <SubjectPicker subject={subject} onPick={setSubject} />
            <span className="editor-line-sep" />
            <span className="editor-line-l">Weight</span>
            <input
              className="editor-num"
              type="number" min="0" step="0.5"
              value={weight}
              onChange={(e) => setWeight(e.target.value)} />
            <span className="editor-line-l-mute">×</span>
          </div>

          {/* Actual grade — chips + score, no section card. */}
          <div className="editor-grade">
            <div className="editor-grade-h">
              <span className="editor-grade-t">Your grade</span>
              {!hasValue && <span className="editor-grade-hint">tap a chip or type a score</span>}
            </div>
            <div
              className="grade-chips"
              style={{ gridTemplateColumns: `repeat(${sys.bands.length}, minmax(0, 1fr))` }}>
              {sys.bands.map((b) => (
                <button
                  key={b.grade}
                  type="button"
                  className={"grade-chip" + (actualGrade === b.grade ? " grade-chip--on" : "")}
                  onClick={() => pickChip(b.grade)}>
                  {b.grade}
                </button>
              ))}
            </div>
            <div className="editor-grade-score">
              <input
                className="editor-num editor-num--wide"
                type="number" min="0" max="100" step="0.5"
                placeholder={`or score (AI ${aiScore})`}
                value={actualScore}
                onChange={(e) => typeScore(e.target.value)} />
              <span className="editor-num-suf">/ 100</span>
              {!actualGrade && resolved.grade && (
                <span className="editor-num-grade">= {resolved.grade}</span>
              )}
            </div>
          </div>

          {/* Collapsibles — quiet by default. */}
          {entry.text && (
            <details className="editor-disc">
              <summary>
                <span>Submitted work</span>
                <span className="editor-disc-meta">{wordCount} words</span>
              </summary>
              <div className="editor-text">{entry.text}</div>
            </details>
          )}

          {entry.rubric && (
            <details className="editor-disc">
              <summary>
                <span>Rubric</span>
                <span className="editor-disc-meta">{entry.rubric.split(/\s+/).filter(Boolean).length} words</span>
              </summary>
              <div className="editor-text editor-text--rubric">{entry.rubric}</div>
            </details>
          )}

          {(entry.summary || entry.feedback) && (
            <details className="editor-disc">
              <summary>
                <span>AI feedback</span>
                <span className="editor-disc-meta">{entry.label || ""}</span>
              </summary>
              <div className="editor-fb">
                {entry.summary && <p className="editor-fb-sum">{entry.summary}</p>}
                {entry.feedback && <p className="editor-fb-body">{entry.feedback}</p>}
              </div>
            </details>
          )}
        </div>

        <div className="editor-foot">
          <button className="editor-del" onClick={() => { onDelete(); onClose(); }}>Delete</button>
          <div style={{ flex: 1 }} />
          <button className="editor-cancel" onClick={onClose}>Cancel</button>
          <button className="editor-save" onClick={save}>Save</button>
        </div>
      </div>
    </div>
  );
}

function SubjectColumn({ subject, entries, system, isOver, onDragOver, onDragLeave, onDrop, onTileClick, draggingId, onDragStart, onDragEnd }) {
  const avg = useMemo(() => {
    if (!entries.length) return null;
    let t = 0, w = 0;
    for (const e of entries) {
      const ew = Math.max(0, Number(e.weight) || 0);
      t += effScore(e) * ew; w += ew;
    }
    return w === 0 ? 0 : t / w;
  }, [entries]);
  const sysForGrade = SYSTEMS[entries[0]?.systemId] || system;
  // Subject ring label is rendered in the currently-active dashboard system.
  const gradeLabel = avg == null ? "—" : scoreToGrade(avg, system.id);

  return (
    <div
      className={"col" + (isOver ? " col--over" : "")}
      onDragOver={onDragOver}
      onDragLeave={onDragLeave}
      onDrop={onDrop}>
      <div className="col-head">
        <div className="col-head-l">
          <span className="col-icon">{SUBJECT_ICONS[subject] || "•"}</span>
          <div>
            <div className="col-name">{subject}</div>
            <div className="col-meta">{entries.length} {entries.length === 1 ? "entry" : "entries"}</div>
          </div>
        </div>
        <Ring size={56} stroke={5} score={avg} label={gradeLabel} />
      </div>
      <div className="col-tiles">
        {entries.length === 0 ? (
          <div className="col-empty">Drop here</div>
        ) : entries.map((e) => (
          <EntryTile
            key={e.id}
            entry={e}
            system={system}
            dragging={draggingId === e.id}
            onDragStart={(ev) => onDragStart(ev, e.id)}
            onDragEnd={onDragEnd}
            onClick={() => onTileClick(e.id)} />
        ))}
      </div>
    </div>
  );
}

function SmartInsight({ entries, user }) {
  const [text, setText] = useState(() => localStorage.getItem("mr:insight") || "");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const generate = async () => {
    if (entries.length === 0) return;
    setLoading(true); setError(null);
    try {
      // Privacy-safe: ONLY send subject + score + weight + grade label.
      // No original text, no AI feedback body, no email — pure aggregate signal.
      const compact = entries.map((e) => ({
        subject: e.subject,
        score: Math.round(effScore(e)),
        weight: e.weight,
        label: e.label || "",
      }));
      // Try the server-side Groq endpoint first (works on Vercel).
      // Fall back to window.claude.complete if we're in a preview env that
      // doesn't have /api/insight wired up.
      let out = "";
      try {
        const r = await fetch("/api/insight", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ entries: compact }),
        });
        if (r.ok) {
          const data = await r.json();
          out = data.text || "";
        } else if (window.claude?.complete) {
          out = await window.claude.complete(
            `You are a warm study coach. Write 2-3 short sentences (max 60 words) about this student's grades — which subjects are strongest, which need work, and one concrete suggestion. Plain prose only.\n\n${JSON.stringify(compact)}`
          );
        } else {
          const err = await r.json().catch(() => ({}));
          throw new Error(err.error || "insight failed");
        }
      } catch (innerErr) {
        if (window.claude?.complete) {
          out = await window.claude.complete(
            `You are a warm study coach. Write 2-3 short sentences (max 60 words) about this student's grades — which subjects are strongest, which need work, and one concrete suggestion. Plain prose only.\n\n${JSON.stringify(compact)}`
          );
        } else {
          throw innerErr;
        }
      }
      const trimmed = (out || "").trim();
      setText(trimmed);
      localStorage.setItem("mr:insight", trimmed);
    } catch (e) {
      setError("Couldn't generate insight right now");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="insight">
      <div className="insight-head">
        <div>
          <div className="insight-eyebrow">
            <svg width="11" height="11" viewBox="0 0 12 12" aria-hidden="true">
              <path d="M6 1.5 L7.2 4.8 L10.5 6 L7.2 7.2 L6 10.5 L4.8 7.2 L1.5 6 L4.8 4.8 Z" fill="currentColor" />
            </svg>
            Smart insight
          </div>
          <div className="insight-sub">
            Personalized analysis · only subject + score is sent, never your work
          </div>
        </div>
        <button className="insight-btn" onClick={generate} disabled={loading || entries.length === 0}>
          {loading ? "Thinking…" : (text ? "Refresh" : "Generate")}
        </button>
      </div>
      <div className="insight-body">
        {error ? <span className="insight-err">{error}</span>
        : text ? <p>{text}</p>
        : <p className="insight-empty">Save a few grades and tap <em>Generate</em> for a quick read on where you stand.</p>}
      </div>
    </div>
  );
}

function DashLocked({ user, onShowPricing }) {
  return (
    <div className="dash-lock">
      <div className="dash-lock-card">
        <div className="dash-lock-ring">
          <svg width="120" height="120" viewBox="0 0 120 120">
            <circle cx="60" cy="60" r="52" stroke="var(--bg-3)" strokeWidth="10" fill="none" />
            <circle cx="60" cy="60" r="52" stroke="var(--accent)" strokeWidth="10" fill="none"
              strokeDasharray="326.7" strokeDashoffset="245"
              strokeLinecap="round"
              transform="rotate(-90 60 60)" />
          </svg>
          <div className="dash-lock-ring-mid">
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
              <rect x="5" y="11" width="14" height="9" rx="2" stroke="var(--ink)" strokeWidth="1.6" />
              <path d="M8 11 V8 a4 4 0 0 1 8 0 V11" stroke="var(--ink)" strokeWidth="1.6" fill="none" />
            </svg>
          </div>
        </div>
        <h2 className="dash-lock-t">Your dashboard is part of Student & Pro</h2>
        <p className="dash-lock-d">
          Save every grade, drag entries between subjects, override with the grade
          you actually got, and let an AI coach point out where to focus next.
          {user ? "" : " Free accounts can grade once a week without an account."}
        </p>
        <ul className="dash-lock-list">
          <li>Persistent grade history, synced across devices</li>
          <li>Subject columns with per-subject averages</li>
          <li>Drag & drop to re-organize</li>
          <li>Edit any grade with your real score</li>
          <li>AI-personalized study insights</li>
        </ul>
        <button className="dash-lock-cta" onClick={onShowPricing}>See plans →</button>
      </div>
    </div>
  );
}

function Dashboard({ entries, setEntries, system, user, onShowPricing }) {
  // Gate to paid plans. Free users see a soft upsell.
  if (!user || user.plan === "free") {
    return <DashLocked user={user} onShowPricing={onShowPricing} />;
  }

  const [draggingId, setDraggingId] = useState(null);
  const [overSubject, setOverSubject] = useState(null);
  const [editingId, setEditingId] = useState(null);

  const syncEntries = (next) => {
    setEntries(next);
    saveEntries(next);
    if (user?.email) {
      fetch("/api/save", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email, entries: next }),
      }).catch(() => {});
    }
  };
  const updateEntry = (id, patch) => syncEntries(entries.map((e) => e.id === id ? { ...e, ...patch } : e));
  const removeEntry = (id) => syncEntries(entries.filter((e) => e.id !== id));

  // Group entries by subject. Only show subjects that have at least one entry.
  const grouped = useMemo(() => {
    const m = {};
    for (const e of entries) {
      const s = SUBJECTS.includes(e.subject) ? e.subject : "General";
      (m[s] = m[s] || []).push(e);
    }
    // Order subjects by SUBJECTS list, then keep any unknown at the end.
    const ordered = {};
    for (const s of SUBJECTS) if (m[s]) ordered[s] = m[s];
    for (const s of Object.keys(m)) if (!ordered[s]) ordered[s] = m[s];
    return ordered;
  }, [entries]);

  // Overall weighted average.
  const overall = useMemo(() => {
    if (!entries.length) return null;
    let t = 0, w = 0;
    for (const e of entries) {
      const ew = Math.max(0, Number(e.weight) || 0);
      t += effScore(e) * ew; w += ew;
    }
    return w === 0 ? 0 : t / w;
  }, [entries]);
  const overallGrade = overall != null ? scoreToGrade(overall, system.id) : "—";
  const totalWeight = entries.reduce((a, e) => a + (Number(e.weight) || 0), 0);

  // Drag handlers ────────────────────────────────────────────────────────
  const onDragStart = (ev, id) => {
    setDraggingId(id);
    try { ev.dataTransfer.effectAllowed = "move"; ev.dataTransfer.setData("text/plain", id); } catch {}
  };
  const onDragEnd = () => { setDraggingId(null); setOverSubject(null); };
  const onDragOver = (subject) => (ev) => { ev.preventDefault(); setOverSubject(subject); };
  const onDragLeave = (subject) => () => { setOverSubject((s) => s === subject ? null : s); };
  const onDrop = (subject) => (ev) => {
    ev.preventDefault();
    const id = draggingId || ev.dataTransfer?.getData("text/plain");
    if (!id) return;
    const entry = entries.find((e) => e.id === id);
    if (entry && entry.subject !== subject) updateEntry(id, { subject });
    setDraggingId(null); setOverSubject(null);
  };

  const editing = editingId ? entries.find((e) => e.id === editingId) : null;
  const subjectsShown = Object.keys(grouped);
  // Always show every SUBJECT as a drop target if there are entries — easier to organize.
  const allSubjects = entries.length > 0 ? SUBJECTS.filter((s) => grouped[s] || true) : subjectsShown;

  return (
    <div className="dash">
      <header className="dash-hero">
        <div className="dash-hero-l">
          <p className="dash-eyebrow">Projected final</p>
          <h2 className="dash-hero-t">
            {entries.length === 0
              ? (user.name ? `Welcome back, ${user.name.split(" ")[0]}` : "Your projection")
              : scoreGreeting(overall, user.name ? user.name.split(" ")[0] : "")}
          </h2>
          <p className="dash-hero-d">
            {entries.length === 0
              ? "Save your first grade to start building your dashboard."
              : `${entries.length} ${entries.length === 1 ? "entry" : "entries"} across ${subjectsShown.length} ${subjectsShown.length === 1 ? "subject" : "subjects"} · ${system.name}`}
          </p>
          <div className="dash-hero-stats">
            <div className="dash-stat">
              <div className="dash-stat-n">{overall == null ? "—" : overall.toFixed(1)}</div>
              <div className="dash-stat-l">Avg score</div>
            </div>
            <div className="dash-stat">
              <div className="dash-stat-n">{totalWeight.toFixed(1)}</div>
              <div className="dash-stat-l">Total weight</div>
            </div>
            <div className="dash-stat">
              <div className="dash-stat-n">
                {entries.length > 0 ? Math.round(Math.max(...entries.map(effScore))) : "—"}
              </div>
              <div className="dash-stat-l">Best</div>
            </div>
            <div className="dash-stat">
              <div className="dash-stat-n">
                {entries.length > 0 ? Math.round(Math.min(...entries.map(effScore))) : "—"}
              </div>
              <div className="dash-stat-l">Lowest</div>
            </div>
          </div>
        </div>
        <div className="dash-hero-r">
          <Ring size={220} stroke={14} score={overall} label={overallGrade} sub={overall == null ? "" : `${Math.round(overall)} / 100`} />
        </div>
      </header>

      <SmartInsight entries={entries} user={user} />

      <section className="cols-wrap">
        <div className="cols-head">
          <h3>Subjects</h3>
          <span className="cols-hint">Drag entries between subjects · click to edit</span>
        </div>
        {entries.length === 0 ? (
          <div className="cols-empty">
            <div className="cols-empty-t">No saved grades yet</div>
            <div className="cols-empty-d">Head to <strong>Grade</strong>, paste your work, and save the result here.</div>
          </div>
        ) : (
          <div className="cols">
            {allSubjects.map((s) => (
              <SubjectColumn
                key={s}
                subject={s}
                entries={grouped[s] || []}
                system={system}
                isOver={overSubject === s}
                onDragOver={onDragOver(s)}
                onDragLeave={onDragLeave(s)}
                onDrop={onDrop(s)}
                onTileClick={setEditingId}
                draggingId={draggingId}
                onDragStart={onDragStart}
                onDragEnd={onDragEnd} />
            ))}
          </div>
        )}
      </section>

      {editing && (
        <EntryEditor
          entry={editing}
          system={system}
          onSave={(patch) => updateEntry(editing.id, patch)}
          onDelete={() => removeEntry(editing.id)}
          onClose={() => setEditingId(null)} />
      )}
    </div>
  );
}

// ─── Settings view ─────────────────────────────────────────────────────
function SettingsView({ user, setUser, prefs, setPrefs, systemId, setSystemId, entries, setEntries, onShowPricing, onManageBilling, onClose, setToast }) {
  const [name, setName] = useState(user?.name || "");
  const [confirm, setConfirm] = useState(null); // "clear" | "logout-everywhere"

  const dirtyName = name.trim() && name.trim() !== user?.name;
  const planLabel = user?.plan === "pro" ? "Pro" : user?.plan === "student" ? "Student" : "Free";

  const saveName = () => {
    if (!dirtyName) return;
    const updated = { ...user, name: name.trim() };
    setUser(updated);
    setToast && setToast("Name updated");
  };

  const exportData = () => {
    const payload = {
      exported: new Date().toISOString(),
      user: { name: user?.name, email: user?.email, plan: user?.plan },
      prefs,
      entries,
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `grades-${new Date().toISOString().slice(0,10)}.json`;
    document.body.appendChild(a); a.click(); a.remove();
    URL.revokeObjectURL(url);
    setToast && setToast("Export downloaded");
  };

  const clearEntries = () => {
    setEntries([]);
    if (user) {
      fetch("/api/save", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email, entries: [] }),
      }).catch(() => {});
    }
    setConfirm(null);
    setToast && setToast("All entries cleared");
  };

  return (
    <div className="settings">
      <header className="settings-hero">
        <div>
          <div className="settings-eyebrow">Account</div>
          <h2 className="settings-t">Settings</h2>
          <p className="settings-d">Defaults, data, and the boring bits.</p>
        </div>
        <button className="settings-back" onClick={onClose} aria-label="Back">
          ← Back to grading
        </button>
      </header>

      {/* Account */}
      <section className="settings-card">
        <div className="settings-card-h">
          <h3>Profile</h3>
          <span className="settings-card-h-d">Who you are in the app.</span>
        </div>
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Display name</div>
            <div className="settings-row-d">Shown in greetings and the header.</div>
          </div>
          <div className="settings-row-r settings-row-r--inline">
            <input
              className="settings-input"
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="Your name" />
            <button
              className={"settings-btn" + (dirtyName ? " settings-btn--primary" : " settings-btn--ghost")}
              onClick={saveName}
              disabled={!dirtyName}>Save</button>
          </div>
        </div>
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Email</div>
            <div className="settings-row-d">Used to sign in. Contact support to change.</div>
          </div>
          <div className="settings-row-r">
            <span className="settings-static">{user?.email}</span>
          </div>
        </div>
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Plan</div>
            <div className="settings-row-d">
              {user?.plan === "free"
                ? "1 grade per week. Upgrade for the dashboard, history, and AI coach."
                : user?.plan === "student"
                ? "10 grades per week, dashboard, and AI coach. Manage card, invoices, or cancel in the billing portal."
                : "30 grades per week, all features, priority queue. Manage card, invoices, or cancel in the billing portal."}
            </div>
          </div>
          <div className="settings-row-r settings-row-r--inline">
            <span className={"plan-pill plan-pill--" + (user?.plan || "free")}>{planLabel}</span>
            {user?.plan === "free" ? (
              <button className="settings-btn settings-btn--ghost" onClick={onShowPricing}>Upgrade →</button>
            ) : (
              <button className="settings-btn settings-btn--ghost" onClick={onManageBilling}>Manage billing →</button>
            )}
          </div>
        </div>
      </section>

      {/* Password */}
      <PasswordCard user={user} setToast={setToast} />

      {/* Grading defaults */}
      <section className="settings-card">
        <div className="settings-card-h">
          <h3>Grading defaults</h3>
          <span className="settings-card-h-d">What loads when you start a new grade.</span>
        </div>
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Default template</div>
            <div className="settings-row-d">The grading scale the picker starts on.</div>
          </div>
          <div className="settings-row-r">
            <SystemPicker
              hideLabel
              system={SYSTEMS[prefs.defaultSystem] || SYSTEMS.usa}
              onPick={(id) => { setPrefs({ defaultSystem: id }); setSystemId(id); }} />
          </div>
        </div>
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Default strictness</div>
            <div className="settings-row-d">How harshly the AI grades, until you change it.</div>
          </div>
          <div className="settings-row-r">
            <div className="settings-seg">
              {["Lenient", "Fair", "Harsh"].map((s) =>
                <button
                  key={s}
                  className={"settings-seg-btn" + (prefs.defaultStrictness === s ? " settings-seg-btn--on" : "")}
                  onClick={() => setPrefs({ defaultStrictness: s })}>{s}</button>
              )}
            </div>
          </div>
        </div>
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Default subject</div>
            <div className="settings-row-d">Pre-selected on the grade screen.</div>
          </div>
          <div className="settings-row-r">
            <SubjectPicker
              subject={prefs.defaultSubject}
              onPick={(s) => setPrefs({ defaultSubject: s })} />
          </div>
        </div>
      </section>

      {/* Behaviour */}
      <section className="settings-card">
        <div className="settings-card-h">
          <h3>App behaviour</h3>
          <span className="settings-card-h-d">Small quality-of-life things.</span>
        </div>
        <SettingsToggle
          label="Auto-save graded work"
          desc="Drop every grade into your dashboard automatically."
          on={prefs.autoSave}
          onChange={(v) => setPrefs({ autoSave: v })} />
        <SettingsToggle
          label="Show AI tips inline"
          desc="A short coach note appears below each result."
          on={prefs.showAiTips}
          onChange={(v) => setPrefs({ showAiTips: v })} />
        <SettingsToggle
          label="Reduce motion"
          desc="Damp transitions and hover lifts."
          on={prefs.reduceMotion}
          onChange={(v) => setPrefs({ reduceMotion: v })} />
        <SettingsToggle
          label="Compact mode"
          desc="Tighter spacing — more on screen at once."
          on={prefs.compactMode}
          onChange={(v) => setPrefs({ compactMode: v })} />
      </section>

      {/* Data & privacy */}
      <section className="settings-card">
        <div className="settings-card-h">
          <h3>Your data</h3>
          <span className="settings-card-h-d">You own it — take it or wipe it any time.</span>
        </div>
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Export everything</div>
            <div className="settings-row-d">Download a JSON file with your account, prefs, and {entries.length} {entries.length === 1 ? "entry" : "entries"}.</div>
          </div>
          <div className="settings-row-r">
            <button className="settings-btn settings-btn--ghost" onClick={exportData}>Download .json</button>
          </div>
        </div>
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Clear all entries</div>
            <div className="settings-row-d">Wipes your saved grades from this account. Cannot be undone.</div>
          </div>
          <div className="settings-row-r">
            {confirm === "clear" ? (
              <div className="settings-confirm">
                <span>Sure?</span>
                <button className="settings-btn settings-btn--danger" onClick={clearEntries}>Yes, clear {entries.length}</button>
                <button className="settings-btn settings-btn--ghost" onClick={() => setConfirm(null)}>Cancel</button>
              </div>
            ) : (
              <button
                className="settings-btn settings-btn--ghost-danger"
                onClick={() => setConfirm("clear")}
                disabled={entries.length === 0}>
                Clear {entries.length || ""}
              </button>
            )}
          </div>
        </div>
      </section>

      <FeedbackCard user={user} setToast={setToast} />

      <p className="settings-foot">
        Preferences are stored on this device.
        {" "}<a href="/privacy.html">Privacy</a> · <a href="/terms.html">Terms</a>
      </p>
    </div>
  );
}

function SettingsToggle({ label, desc, on, onChange }) {
  return (
    <div className="settings-row">
      <div className="settings-row-l">
        <div className="settings-row-t">{label}</div>
        <div className="settings-row-d">{desc}</div>
      </div>
      <div className="settings-row-r">
        <button
          type="button"
          role="switch"
          aria-checked={on}
          className={"settings-switch" + (on ? " settings-switch--on" : "")}
          onClick={() => onChange(!on)}>
          <span className="settings-switch-knob" />
        </button>
      </div>
    </div>
  );
}

// Password change — collapsed by default; expands to 3 inputs, then a code step.
// "Forgot current password?" link uses the existing /api/forgot → /api/reset flow.
function PasswordCard({ user, setToast }) {
  const [open, setOpen] = useState(false);
  const [step, setStep] = useState("form"); // "form" | "code" | "forgot-code"
  const [cur, setCur] = useState("");
  const [next, setNext] = useState("");
  const [conf, setConf] = useState("");
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");

  const reset = () => {
    setStep("form");
    setCur(""); setNext(""); setConf(""); setCode("");
    setErr(""); setBusy(false);
  };
  const close = () => { setOpen(false); reset(); };

  const canSubmitForm = cur && next.length >= 6 && next === conf && !busy;
  const canSubmitCode = /^\d{6}$/.test(code) && !busy;
  const canSubmitForgot = /^\d{6}$/.test(code) && next.length >= 6 && next === conf && !busy;

  const submitForm = async (e) => {
    e.preventDefault();
    setErr("");
    if (next.length < 6) return setErr("New password must be at least 6 characters.");
    if (next !== conf) return setErr("New passwords don't match.");
    if (next === cur) return setErr("New password must be different.");
    setBusy(true);
    try {
      const r = await fetch("/api/change-password", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email, currentPassword: cur, newPassword: next }),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok || !data.needsCode) {
        setErr(data.error || "Couldn't start password change.");
        setBusy(false);
        return;
      }
      setStep("code");
      setBusy(false);
    } catch {
      setErr("Network error — try again.");
      setBusy(false);
    }
  };

  const submitCode = async (e) => {
    e.preventDefault();
    setErr("");
    setBusy(true);
    try {
      const r = await fetch("/api/change-password", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email, code }),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErr(data.error || "Couldn't verify code.");
        setBusy(false);
        return;
      }
      setToast && setToast("Password updated");
      close();
    } catch {
      setErr("Network error — try again.");
      setBusy(false);
    }
  };

  // Forgot flow: kick off /api/forgot, jump to code+newpw step.
  const requestReset = async () => {
    setErr(""); setBusy(true);
    try {
      const r = await fetch("/api/forgot", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email }),
      });
      // Endpoint is intentionally silent about whether an account exists, so
      // we just advance the UI either way — same as the login modal does.
      if (r.status === 429) {
        const data = await r.json().catch(() => ({}));
        setErr(data.error || "Too many requests — wait a bit.");
        setBusy(false);
        return;
      }
      setCur(""); setNext(""); setConf(""); setCode(""); setErr("");
      setStep("forgot-code");
      setBusy(false);
    } catch {
      setErr("Network error — try again.");
      setBusy(false);
    }
  };

  const submitForgot = async (e) => {
    e.preventDefault();
    setErr("");
    if (next.length < 6) return setErr("New password must be at least 6 characters.");
    if (next !== conf) return setErr("New passwords don't match.");
    setBusy(true);
    try {
      const r = await fetch("/api/reset", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email, code, password: next }),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErr(data.error || "Couldn't reset password.");
        setBusy(false);
        return;
      }
      setToast && setToast("Password updated");
      close();
    } catch {
      setErr("Network error — try again.");
      setBusy(false);
    }
  };

  return (
    <section className="settings-card">
      <div className="settings-card-h">
        <h3>Password</h3>
        <span className="settings-card-h-d">Keep your account secure. We'll email a code to confirm.</span>
      </div>
      {!open ? (
        <div className="settings-row">
          <div className="settings-row-l">
            <div className="settings-row-t">Change password</div>
            <div className="settings-row-d">Confirms with your current password and a code sent to {user?.email}.</div>
          </div>
          <div className="settings-row-r">
            <button className="settings-btn settings-btn--ghost" onClick={() => setOpen(true)}>
              Change →
            </button>
          </div>
        </div>
      ) : step === "form" ? (
        <form className="settings-pw" onSubmit={submitForm}>
          <label className="settings-pw-field">
            <span>Current password</span>
            <input
              type="password"
              autoComplete="current-password"
              className="settings-input settings-input--wide"
              value={cur}
              onChange={(e) => setCur(e.target.value)}
              autoFocus />
          </label>
          <label className="settings-pw-field">
            <span>New password</span>
            <input
              type="password"
              autoComplete="new-password"
              className="settings-input settings-input--wide"
              value={next}
              onChange={(e) => setNext(e.target.value)}
              placeholder="At least 6 characters" />
          </label>
          <label className="settings-pw-field">
            <span>Confirm new password</span>
            <input
              type="password"
              autoComplete="new-password"
              className="settings-input settings-input--wide"
              value={conf}
              onChange={(e) => setConf(e.target.value)} />
          </label>
          {err && <div className="settings-pw-err">{err}</div>}
          <div className="settings-pw-row">
            <button type="button" className="settings-pw-link" onClick={requestReset} disabled={busy}>
              Forgot current password?
            </button>
            <div className="settings-pw-actions">
              <button type="button" className="settings-btn settings-btn--ghost" onClick={close} disabled={busy}>
                Cancel
              </button>
              <button type="submit" className="settings-btn settings-btn--primary" disabled={!canSubmitForm}>
                {busy ? "Sending code…" : "Send confirmation code"}
              </button>
            </div>
          </div>
        </form>
      ) : step === "code" ? (
        <form className="settings-pw" onSubmit={submitCode}>
          <div className="settings-pw-note">
            We sent a 6-digit code to <strong>{user.email}</strong>. It expires in 10 minutes.
          </div>
          <label className="settings-pw-field">
            <span>Confirmation code</span>
            <input
              type="text"
              inputMode="numeric"
              autoComplete="one-time-code"
              maxLength={6}
              className="settings-input settings-input--wide settings-input--code"
              value={code}
              onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
              placeholder="123456"
              autoFocus />
          </label>
          {err && <div className="settings-pw-err">{err}</div>}
          <div className="settings-pw-actions">
            <button type="button" className="settings-btn settings-btn--ghost" onClick={close} disabled={busy}>
              Cancel
            </button>
            <button type="submit" className="settings-btn settings-btn--primary" disabled={!canSubmitCode}>
              {busy ? "Verifying…" : "Confirm change"}
            </button>
          </div>
        </form>
      ) : (
        // forgot-code: enter code + new password (we never knew the old one)
        <form className="settings-pw" onSubmit={submitForgot}>
          <div className="settings-pw-note">
            If an account exists for <strong>{user.email}</strong>, a reset code is on its way. Enter it below with your new password.
          </div>
          <label className="settings-pw-field">
            <span>Reset code</span>
            <input
              type="text"
              inputMode="numeric"
              autoComplete="one-time-code"
              maxLength={6}
              className="settings-input settings-input--wide settings-input--code"
              value={code}
              onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
              placeholder="123456"
              autoFocus />
          </label>
          <label className="settings-pw-field">
            <span>New password</span>
            <input
              type="password"
              autoComplete="new-password"
              className="settings-input settings-input--wide"
              value={next}
              onChange={(e) => setNext(e.target.value)}
              placeholder="At least 6 characters" />
          </label>
          <label className="settings-pw-field">
            <span>Confirm new password</span>
            <input
              type="password"
              autoComplete="new-password"
              className="settings-input settings-input--wide"
              value={conf}
              onChange={(e) => setConf(e.target.value)} />
          </label>
          {err && <div className="settings-pw-err">{err}</div>}
          <div className="settings-pw-actions">
            <button type="button" className="settings-btn settings-btn--ghost" onClick={close} disabled={busy}>
              Cancel
            </button>
            <button type="submit" className="settings-btn settings-btn--primary" disabled={!canSubmitForgot}>
              {busy ? "Updating…" : "Reset password"}
            </button>
          </div>
        </form>
      )}
    </section>
  );
}

// ─── Feedback card ─────────────────────────────────────────────────────
// Sends the message to the team inbox via /api/feedback (Resend).
function FeedbackCard({ user, setToast }) {
  const CATS = [
    { id: "bug",     label: "Bug" },
    { id: "feature", label: "Idea" },
    { id: "general", label: "General" },
    { id: "praise",  label: "Praise" },
  ];
  const [cat, setCat] = useState("general");
  const [msg, setMsg] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  const [sent, setSent] = useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setErr("");
    const t = msg.trim();
    if (t.length < 5) return setErr("Tell us a little more — at least a sentence.");
    if (t.length > 4000) return setErr("That's a lot — keep it under 4000 characters.");
    setBusy(true);
    try {
      const r = await fetch("/api/feedback", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ message: t, category: cat, email: user?.email || "" }),
      });
      const data = await r.json().catch(() => ({}));
      if (!r.ok) {
        setErr(data.error || "Couldn't send — try again.");
        setBusy(false);
        return;
      }
      setSent(true);
      setMsg("");
      setBusy(false);
      setToast && setToast("Thanks — feedback sent");
      setTimeout(() => setSent(false), 4000);
    } catch {
      setErr("Network error — try again.");
      setBusy(false);
    }
  };

  return (
    <section className="settings-card">
      <div className="settings-card-h">
        <h3>Send feedback</h3>
        <span className="settings-card-h-d">Found a bug, got an idea, or just want to say hi? The team reads everything.</span>
      </div>
      <form className="settings-fb" onSubmit={submit}>
        <div className="settings-fb-cats">
          {CATS.map((c) => (
            <button
              key={c.id}
              type="button"
              className={"settings-fb-cat" + (cat === c.id ? " settings-fb-cat--on" : "")}
              onClick={() => setCat(c.id)}>
              {c.label}
            </button>
          ))}
        </div>
        <textarea
          className="settings-fb-text"
          placeholder={
            cat === "bug" ? "What happened? What did you expect to happen?" :
            cat === "feature" ? "Describe what would help, and what problem it solves." :
            cat === "praise" ? "Tell us what you like!" :
            "What's on your mind?"
          }
          value={msg}
          onChange={(e) => setMsg(e.target.value)}
          rows={4}
          maxLength={4000} />
        <div className="settings-fb-foot">
          <span className="settings-fb-meta">
            {user
              ? <>Signed in as <strong>{user.email}</strong></>
              : "Sent anonymously — we can't reply"}
            <span className="settings-fb-dot">·</span>
            <span>{msg.length}/4000</span>
          </span>
          <div className="settings-pw-actions">
            <button
              type="submit"
              className="settings-btn settings-btn--primary"
              disabled={busy || msg.trim().length < 5}>
              {busy ? "Sending…" : sent ? "✓ Sent" : "Send feedback"}
            </button>
          </div>
        </div>
        {err && <div className="settings-pw-err">{err}</div>}
      </form>
    </section>
  );
}

// ─── Root ────────────────────────────────────────────────────────────────
function App() {
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [user, setUser] = useState(loadUser);
  const [prefs, setPrefsState] = useState(loadPrefs);
  const [view, setView] = useState("grade");
  const [systemId, setSystemId] = useState(() => {
    const p = loadPrefs();
    return SYSTEMS[p.defaultSystem] ? p.defaultSystem : "usa";
  });
  const [entries, setEntries] = useState(loadEntries);
  const [freeUsed, setFreeUsed] = useState(loadFreeUsed);
  const [authReason, setAuthReason] = useState(null);
  const [showPricing, setShowPricing] = useState(false);
  const [pendingSave, setPendingSave] = useState(null);
  const [toast, setToast] = useState(null);

  // ─── Persistent session: auto-save user to localStorage on any change ──
  // Restored on mount via useState(loadUser). Cleared on logout via clearUser().
  useEffect(() => {
    if (user) saveUser(user);
    else clearUser();
  }, [user]);

  // Persist prefs whenever they change. Also keep --reduce-motion etc on the app root.
  useEffect(() => { savePrefs(prefs); }, [prefs]);
  const setPrefs = (patch) => setPrefsState((p) => ({ ...p, ...patch }));

  // ─── Hydrate entries from server when a user logs in / is restored ────
  // Replaces local entries with the KV record so grades follow the user
  // across browsers and devices.
  useEffect(() => {
    if (!user?.email) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await fetch("/api/save?email=" + encodeURIComponent(user.email));
        if (!r.ok) return;
        const data = await r.json();
        if (cancelled || !Array.isArray(data.entries)) return;
        setEntries(data.entries);
        saveEntries(data.entries);
      } catch {}
    })();
    return () => { cancelled = true; };
  }, [user?.email]);

  const FREE_LIMIT = Number(tweaks.freeLimit) || 1;
  const weeklyUsed = loadDaily().count;
  const planLimit = !user ? FREE_LIMIT : user.plan === "pro" ? 30 : user.plan === "student" ? 10 : FREE_LIMIT;
  const weeklyLeft = Math.max(0, planLimit - weeklyUsed);
  const freeLeft = !user ? weeklyLeft : 0;

  useEffect(() => {
    if (toast) {
      const t = setTimeout(() => setToast(null), 2400);
      return () => clearTimeout(t);
    }
  }, [toast]);

  // Stripe checkout return handler
  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const c = params.get("checkout");
    if (c === "success") {
      const plan = params.get("plan") === "pro" ? "pro" : "student";
      const u = loadUser();
      if (u) {
        const updated = { ...u, plan };
        saveUser(updated);
        setUser(updated);
        // Persist plan server-side so it sticks across devices
        fetch("/api/plan", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ email: u.email, plan })
        }).catch(() => {});
      }
      setToast("Subscription active — thanks!");
      window.history.replaceState({}, "", window.location.pathname);
    } else if (c === "cancel") {
      setToast("Checkout canceled");
      window.history.replaceState({}, "", window.location.pathname);
    }
  }, []);

  // On load, sync plan from server for already-logged-in users
  useEffect(() => {
    if (!user) return;
    fetch("/api/plan?email=" + encodeURIComponent(user.email)).
    then((r) => r.json()).
    then((data) => {
      if (data.plan && data.plan !== user.plan) {
        const updated = { ...user, plan: data.plan };
        saveUser(updated);
        setUser(updated);
      }
    }).
    catch(() => {});
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // After user signs up while a save is pending, finish that save.
  useEffect(() => {
    if (user && pendingSave) {
      const entries0 = loadEntries();
      entries0.unshift({
        id: Date.now().toString(36),
        name: pendingSave.name.trim() || `${pendingSave.subject} — untitled`,
        subject: pendingSave.subject,
        systemId: pendingSave.systemId,
        grade: pendingSave.result.grade,
        label: pendingSave.result.label,
        score: pendingSave.result.score,
        weight: 1,
        created: new Date().toISOString()
      });
      saveEntries(entries0);
      setEntries(entries0);
      setPendingSave(null);
      setToast("Account created · grade saved");
      setView("dash");
    }
  }, [user, pendingSave]);

  const onView = (v) => {
    if (v === "dash" && !user) {
      setAuthReason({
        title: "Unlock your dashboard",
        detail: "Sign up free to get 1 free grade per week, forever."
      });
      return;
    }
    if (v === "dash" && user && user.plan === "free") {
      setShowPricing(true);
      return;
    }
    if (v === "settings" && !user) {
      setAuthReason({
        title: "Settings need an account",
        detail: "Log in to save preferences across devices."
      });
      return;
    }
    setView(v);
  };

  const handleGradeUsed = (kind) => {
    if (user) return;
    if (kind === "success") {
      const next = freeUsed + 1;
      setFreeUsed(next);
      saveFreeUsed(next);
      // Hit the wall after their 3rd grade — show value first, then ask
      if (next >= FREE_LIMIT) {
        setTimeout(() => {
          setAuthReason({
            title: "You've used your free grade",
            detail: "Sign up free to get 1 free grade per week, forever."
          });
        }, 1500);
      }
    } else if (kind === "blocked") {
      setAuthReason({
        title: "Free grades used up",
        detail: "Sign up free to keep grading. No card required."
      });
    }
  };

  const handleSaveAttempt = (result, name, subject, systemId) => {
    setPendingSave({ result, name, subject, systemId });
    setAuthReason({
      title: "Save your grade",
      detail: "Sign up free to get 1 free grade per week, forever."
    });
  };

  const handleUpgrade = async (planId) => {
    if (!user) {
      setShowPricing(false);
      setAuthReason({ title: "Almost there", detail: "Create your account to choose a plan." });
      return;
    }
    if (planId === "free") {
      const updated = { ...user, plan: "free" };
      saveUser(updated);
      setUser(updated);
      setShowPricing(false);
      setToast("Plan: Free");
      return;
    }
    // Paid plans → Stripe Checkout
    try {
      const r = await fetch("/api/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ plan: planId, email: user.email })
      });
      const data = await r.json();
      if (!r.ok || !data.url) throw new Error(data.error || "Checkout failed");
      window.location.href = data.url;
    } catch (e) {
      setShowPricing(false);
      setToast("Couldn't start checkout — try again");
    }
  };

  // ─── Stripe Customer Portal: real plan management, invoices, cancel. ──
  // Free users get the pricing modal instead.
  const openBillingPortal = async () => {
    if (!user) {
      setAuthReason({ title: "Sign in to manage billing", detail: "Log in to access your subscription." });
      return;
    }
    if (user.plan === "free") {
      setShowPricing(true);
      return;
    }
    setToast("Opening billing portal…");
    try {
      const r = await fetch("/api/portal", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ email: user.email })
      });
      const data = await r.json();
      if (!r.ok || !data.url) throw new Error(data.error || "Portal unavailable");
      window.location.href = data.url;
    } catch (e) {
      setToast("Couldn't open billing portal — try again");
    }
  };

  const system = SYSTEMS[systemId];

  return (
    <div
      className={"app" + (prefs.reduceMotion ? " app--reduce-motion" : "") + (prefs.compactMode ? " app--compact" : "")}
      style={{ "--accent": tweaks.accentColor, "--accent-2": tweaks.accentColor, "--accent-soft": tweaks.accentColor + "22" }}>
      <Header
        user={user}
        view={view}
        onView={onView}
        freeLeft={freeLeft}
        weeklyLeft={weeklyLeft}
        planLimit={planLimit}
        tweaks={tweaks}
        onShowAuth={(reason) => setAuthReason(reason)}
        onShowPricing={() => setShowPricing(true)}
        onManageBilling={openBillingPortal}
        onLogout={() => {clearUser();setUser(null);setView("grade");}} />
      
      {view === "grade" ?
      <GradeView
        system={system}
        setSystemId={setSystemId}
        user={user}
        prefs={prefs}
        freeLeft={freeLeft}
        weeklyLeft={weeklyLeft}
        onGradeUsed={handleGradeUsed}
        onSaveAttempt={handleSaveAttempt}
        onShowPricing={() => setShowPricing(true)} /> :
      view === "settings" ?
      <SettingsView
        user={user}
        setUser={setUser}
        prefs={prefs}
        setPrefs={setPrefs}
        systemId={systemId}
        setSystemId={setSystemId}
        entries={entries}
        setEntries={setEntries}
        onShowPricing={() => setShowPricing(true)}
        onManageBilling={openBillingPortal}
        onClose={() => setView("grade")}
        setToast={setToast} /> :

      <Dashboard entries={entries} setEntries={setEntries} system={system} user={user} onShowPricing={() => setShowPricing(true)} />}
      <footer className="ftr">
        <span><WordMark size={12} name={tweaks.brandName} accent={tweaks.brandAccent} color={tweaks.logoColor} /> · automated grading</span>
        <span style={{ display: "flex", gap: 12, alignItems: "center" }}>
          <span>{Object.keys(SYSTEMS).length} grading templates · estimates only</span>
          <a href="/privacy.html" style={{ color: "var(--ink-3)", textDecoration: "none" }}>Privacy</a>
          <a href="/terms.html" style={{ color: "var(--ink-3)", textDecoration: "none" }}>Terms</a>
        </span>
      </footer>

      {authReason &&
      <AuthModal
        reason={authReason}
        tweaks={tweaks}
        onAuth={async (u) => {
          setUser(u);
          setAuthReason(null);
          // Pull the user's saved plan from the server (sticks across devices)
          try {
            const r = await fetch("/api/plan?email=" + encodeURIComponent(u.email));
            const data = await r.json();
            if (data.plan && data.plan !== u.plan) {
              const updated = { ...u, plan: data.plan };
              saveUser(updated);
              setUser(updated);
            }
          } catch {}
        }}
        onClose={() => setAuthReason(null)} />

      }
      {showPricing &&
      <PricingModal
        user={user}
        tweaks={tweaks}
        onClose={() => setShowPricing(false)}
        onUpgrade={handleUpgrade}
        onManageBilling={() => { setShowPricing(false); openBillingPortal(); }} />

      }
      {toast && <div className="toast">{toast}</div>}

      <TweaksPanel>
        <TweakSection label="Brand" />
        <TweakText label="Brand name" value={tweaks.brandName}
        onChange={(v) => setTweak('brandName', v)} />
        <TweakText label="Accent part of name" value={tweaks.brandAccent}
        onChange={(v) => setTweak('brandAccent', v)} />
        <TweakColor label="Logo color" value={tweaks.logoColor}
        onChange={(v) => setTweak('logoColor', v)} />
        <TweakColor label="App accent" value={tweaks.accentColor}
        onChange={(v) => setTweak('accentColor', v)} />
        <TweakSection label="Pricing" />
        <TweakSlider label="Free grade limit" value={tweaks.freeLimit}
        min={1} max={10} step={1}
        onChange={(v) => setTweak('freeLimit', v)} />
        <TweakRadio label="Featured plan" value={tweaks.featuredPlan}
        options={['free', 'student', 'pro']}
        onChange={(v) => setTweak('featuredPlan', v)} />
      </TweaksPanel>
    </div>);

}

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