/* Waffle — deployed build. React + ReactDOM load globally from index.html. */
const { useState, useEffect, useMemo, useRef } = React;

/* AI model. If AI calls ever fail with a "model" error, change THIS one line
   to a current name from console.anthropic.com -> Docs -> Models. */
const MODEL = "claude-haiku-4-5-20251001";

/* ================================================================== */
/*  WAFFLE — variable income, subcategories, subtle game feel          */
/* ================================================================== */
const SF = '-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif';
const T = {
  bg: "#F2F2F7", card: "#FFFFFF", text: "#1D1D1F", dim: "#6E6E73", faint: "#A1A1A6",
  sep: "rgba(60,60,67,0.10)", fill: "#F2F2F7", fill2: "#E9E9EE",
  blue: "#007AFF", green: "#34C759", red: "#FF3B30", orange: "#FF9500", ink: "#1D1D1F",
  waffle: "#F2A93C",
};
const CATS = [
  { name: "Groceries", color: "#34C759" }, { name: "Dining Out", color: "#FF9500" },
  { name: "Shopping", color: "#FF2D55" }, { name: "Electronics", color: "#007AFF" },
  { name: "Home", color: "#5856D6" }, { name: "Transport", color: "#5AC8FA" },
  { name: "Entertainment", color: "#AF52DE" }, { name: "Health", color: "#00C7BE" },
  { name: "Bills", color: "#8E8E93" }, { name: "Subscriptions", color: "#FFCC00" },
  { name: "Other", color: "#C7C7CC" },
];
const COLOR = Object.fromEntries(CATS.map((c) => [c.name, c.color]));
const SUBS = {
  Transport: ["Gas", "Car payment", "Insurance", "Parking", "Maintenance"],
  Bills: ["Rent", "Electric", "Water", "Internet", "Phone"],
  "Dining Out": ["Restaurants", "Coffee", "Takeout", "Bars"],
  Shopping: ["Clothes", "Gifts", "Misc"],
  Groceries: ["Store"], Health: ["Gym", "Pharmacy", "Doctor"],
  Entertainment: ["Streaming", "Events", "Games"], Home: ["Furniture", "Decor", "Supplies"],
  Electronics: [], Subscriptions: [], Other: [],
};

const DEFAULT_CONFIG = {
  period: "monthly", goal: { name: "House down payment", target: 40000, saved: 6500 },
  budgets: { Groceries: 450, "Dining Out": 300, Shopping: 200, Electronics: 80, Home: 150, Transport: 650, Entertainment: 100, Health: 80, Bills: 1700, Subscriptions: 60, Other: 100 },
};
function daysAgo(d) { const t = new Date(); t.setDate(t.getDate() - d); return t.toISOString().slice(0, 10); }
const SEED_TX = [
  { id: "1", who: "Trader Joe's", amount: 84, date: daysAgo(2), category: "Groceries", sub: "Store" },
  { id: "2", who: "Sushi Mori", amount: 96, date: daysAgo(2), category: "Dining Out", sub: "Restaurants" },
  { id: "3", who: "Target", amount: 132, date: daysAgo(4), category: "Shopping", sub: "Misc" },
  { id: "4", who: "Chipotle", amount: 23, date: daysAgo(5), category: "Dining Out", sub: "Takeout" },
  { id: "5", who: "Uber Eats", amount: 41, date: daysAgo(6), category: "Dining Out", sub: "Takeout" },
  { id: "6", who: "Zara", amount: 118, date: daysAgo(7), category: "Shopping", sub: "Clothes" },
  { id: "7", who: "Shell", amount: 58, date: daysAgo(8), category: "Transport", sub: "Gas" },
  { id: "8", who: "Toyota Financial", amount: 340, date: daysAgo(10), category: "Transport", sub: "Car payment" },
  { id: "9", who: "Geico", amount: 145, date: daysAgo(11), category: "Transport", sub: "Insurance" },
  { id: "10", who: "Netflix", amount: 18, date: daysAgo(10), category: "Subscriptions" },
  { id: "11", who: "Whole Foods", amount: 61, date: daysAgo(12), category: "Groceries", sub: "Store" },
  { id: "12", who: "Rent", amount: 1450, date: daysAgo(12), category: "Bills", sub: "Rent" },
  { id: "13", who: "FPL", amount: 120, date: daysAgo(9), category: "Bills", sub: "Electric" },
  { id: "14", who: "Ramen Bar", amount: 38, date: daysAgo(13), category: "Dining Out", sub: "Restaurants" },
  { id: "15", who: "Shell", amount: 54, date: daysAgo(3), category: "Transport", sub: "Gas" },
];
const SEED_INCOME = [
  { id: "i1", source: "Fire Dept", amount: 1850, date: daysAgo(3) },
  { id: "i2", source: "Overtime", amount: 430, date: daysAgo(9) },
  { id: "i3", source: "Fire Dept", amount: 1850, date: daysAgo(17) },
  { id: "i4", source: "Web design — Healing Vine", amount: 600, date: daysAgo(20) },
  { id: "i5", source: "Fire Dept", amount: 1850, date: daysAgo(31) },
  { id: "i6", source: "Overtime", amount: 680, date: daysAgo(38) },
  { id: "i7", source: "Fire Dept", amount: 1850, date: daysAgo(45) },
  { id: "i8", source: "Web design — Westside FD", amount: 1100, date: daysAgo(50) },
  { id: "i9", source: "Fire Dept", amount: 1850, date: daysAgo(59) },
  { id: "i10", source: "Overtime", amount: 250, date: daysAgo(66) },
  { id: "i11", source: "Fire Dept", amount: 1850, date: daysAgo(73) },
  { id: "i12", source: "Fire Dept", amount: 1850, date: daysAgo(87) },
];

/* ---- helpers ---- */
const fmt = (n) => "$" + Math.round(n).toLocaleString("en-US");
const fmtK = (n) => (n >= 1000 ? "$" + (n / 1000).toFixed(n >= 10000 ? 0 : 1) + "k" : "$" + Math.round(n));
const monthKey = (iso) => iso.slice(0, 7);
const thisMonth = () => new Date().toISOString().slice(0, 7);
const prettyDate = (iso) => new Date(iso + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric" });
const ageDays = (iso) => (Date.now() - new Date(iso + "T00:00:00").getTime()) / 86400000;
function scoreFor(spent, budget) { if (!budget) return null; const r = spent / budget; return Math.max(0, Math.min(100, Math.round(100 - Math.max(0, r - 0.6) * 140))); }
const scoreColor = (s) => (s >= 80 ? T.green : s >= 50 ? T.orange : T.red);
function localGuess(who = "") {
  const w = who.toLowerCase(); const has = (...k) => k.some((x) => w.includes(x));
  if (has("whole foods", "trader", "kroger", "aldi", "publix", "grocer")) return "Groceries";
  if (has("restaurant", "cafe", "coffee", "starbucks", "chipotle", "uber eats", "doordash", "bar", "bistro", "sushi", "ramen", "pizza")) return "Dining Out";
  if (has("netflix", "spotify", "hulu", "disney", "icloud", "prime")) return "Subscriptions";
  if (has("uber", "lyft", "shell", "chevron", "gas", "geico", "toyota", "parking")) return "Transport";
  if (has("cvs", "walgreens", "pharmacy", "gym", "fitness")) return "Health";
  if (has("best buy", "apple store")) return "Electronics";
  if (has("ikea", "home depot", "lowes", "wayfair")) return "Home";
  if (has("fpl", "at&t", "verizon", "comcast", "electric", "rent", "insurance")) return "Bills";
  if (has("movie", "cinema", "steam", "concert")) return "Entertainment";
  if (has("zara", "h&m", "nike", "target", "walmart", "amazon")) return "Shopping";
  return "Other";
}

/* ---- AI (preview) ---- */
async function callClaude(content) {
  const res = await fetch("/api/claude", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: MODEL, max_tokens: 1000, messages: [{ role: "user", content }] }) });
  const data = await res.json(); if (data.error) throw new Error(data.error.message); return (data.content || []).map((b) => (b.type === "text" ? b.text : "")).join("").trim();
}
const parseJSON = (t) => JSON.parse(t.replace(/```json|```/g, "").trim());
async function aiCategorize(who, amount) { return parseJSON(await callClaude(`Finance categorizer. Categories: ${CATS.map((c) => c.name).join(", ")}. Merchant "${who}", $${amount}. Return ONLY JSON {"category":"","sub":"","confidence":0}`)); }
async function aiReadReceipt(b64, mt) { return parseJSON(await callClaude([{ type: "image", source: { type: "base64", media_type: mt, data: b64 } }, { type: "text", text: `Read receipt. Categories: ${CATS.map((c) => c.name).join(", ")}. Return ONLY JSON {"who":"","amount":0,"date":"","category":"","sub":"","items":[{"name":"","price":0}]}. Max 5 items.` }])); }
async function aiCoach(p) { return await callClaude(`Sharp, encouraging finance coach for someone with variable income (firefighter + side work). Data: ${JSON.stringify(p)}
Plain text, under 130 words, speak to "you": 1) one line on income — note it's variable and the projected range. 2) each over-budget category: how far over + what redirecting buys toward "${p.goal.name}". 3) one budget tweak for next period + goal impact.`); }

/* ---- count-up ---- */
function useCountUp(value, dur = 650) {
  const [n, setN] = useState(value); const from = useRef(value);
  useEffect(() => { let raf; const s = performance.now(), a = from.current, b = value; const tick = (t) => { const p = Math.min(1, (t - s) / dur), e = 1 - Math.pow(1 - p, 3); setN(a + (b - a) * e); if (p < 1) raf = requestAnimationFrame(tick); else from.current = b; }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [value]);
  return n;
}

/* ---- icons ---- */
const Svg = ({ children, size = 24, color = "currentColor", sw = 1.9, fill = "none", className }) => <svg className={className} width={size} height={size} viewBox="0 0 24 24" fill={fill} stroke={color} strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round">{children}</svg>;
const IHome = (p) => <Svg {...p}><path d="M4 11 12 4l8 7" /><path d="M6 10v9h12v-9" /></Svg>;
const IWallet = (p) => <Svg {...p}><rect x="3" y="6" width="18" height="13" rx="2.5" /><path d="M3 10h18" /><circle cx="16.5" cy="14" r="1.1" fill={p.color || "currentColor"} stroke="none" /></Svg>;
const IChart = (p) => <Svg {...p}><line x1="6" y1="20" x2="6" y2="11" /><line x1="12" y1="20" x2="12" y2="5" /><line x1="18" y1="20" x2="18" y2="14" /></Svg>;
const ISliders = (p) => <Svg {...p}><line x1="4" y1="8" x2="20" y2="8" /><circle cx="9" cy="8" r="2.2" /><line x1="4" y1="16" x2="20" y2="16" /><circle cx="15" cy="16" r="2.2" /></Svg>;
const IPlus = (p) => <Svg {...p}><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></Svg>;
const IX = (p) => <Svg {...p}><line x1="6" y1="6" x2="18" y2="18" /><line x1="18" y1="6" x2="6" y2="18" /></Svg>;
const ICam = (p) => <Svg {...p}><path d="M3 8.5h3.5L8 6.5h8L17.5 8.5H21v11H3z" /><circle cx="12" cy="13.5" r="3.2" /></Svg>;
const ISpark = (p) => <Svg {...p} fill={p.color || "currentColor"} sw={0}><path d="M12 3l1.6 5.4L19 10l-5.4 1.6L12 17l-1.6-5.4L5 10l5.4-1.6z" /></Svg>;
const ICheck = (p) => <Svg {...p}><path d="M5 12l4 4 10-10" /></Svg>;
const IChevR = (p) => <Svg {...p}><path d="M9 6l6 6-6 6" /></Svg>;
const IChevL = (p) => <Svg {...p}><path d="M15 6l-6 6 6 6" /></Svg>;
const ITrend = (p) => <Svg {...p}><path d="M3 17l6-6 4 4 7-7" /><path d="M17 8h4v4" /></Svg>;
const ISpin = (p) => <Svg {...p}><path d="M12 4a8 8 0 1 1-8 8" /></Svg>;

/* ================================================================== */
/*  APP                                                                */
/* ================================================================== */
function App() {
  const [config, setConfig] = useState(DEFAULT_CONFIG);
  const [tx, setTx] = useState([]); const [income, setIncome] = useState([]);
  const [loading, setLoading] = useState(true);
  const [view, setView] = useState("home"); const [detail, setDetail] = useState(null);
  const [adding, setAdding] = useState(false); const [confirm, setConfirm] = useState(null);

  useEffect(() => {
    (async () => {
      try { const v = localStorage.getItem("waffle"); if (v) { const d = JSON.parse(v); setConfig(d.config || DEFAULT_CONFIG); setTx(d.tx || []); setIncome(d.income || []); } else throw 0; }
      catch (e) { setConfig(DEFAULT_CONFIG); setTx(SEED_TX); setIncome(SEED_INCOME); try { localStorage.setItem("waffle", JSON.stringify({ config: DEFAULT_CONFIG, tx: SEED_TX, income: SEED_INCOME })); } catch (_) {} }
      finally { setLoading(false); }
    })();
  }, []);
  const persist = (next) => { try { localStorage.setItem("waffle", JSON.stringify(next)); } catch (_) {} };
  const saveTx = (t) => { setTx(t); persist({ config, tx: t, income }); };
  const saveIncome = (i) => { setIncome(i); persist({ config, tx, income: i }); };
  const saveConfig = (c) => { setConfig(c); persist({ config: c, tx, income }); };

  const addExpense = (rec) => {
    const full = { ...rec, id: Date.now().toString() }; const next = [full, ...tx]; saveTx(next);
    const spent = next.filter((x) => x.category === rec.category && monthKey(x.date) === thisMonth()).reduce((a, b) => a + b.amount, 0);
    setConfirm({ kind: "expense", rec: full, spent, budget: config.budgets[rec.category] || 0 }); setAdding(false);
  };
  const addIncome = (rec) => {
    const full = { ...rec, id: Date.now().toString() }; const next = [full, ...income]; saveIncome(next);
    const monthIncome = next.filter((x) => monthKey(x.date) === thisMonth()).reduce((a, b) => a + b.amount, 0);
    setConfirm({ kind: "income", rec: full, monthIncome }); setAdding(false);
  };
  const metrics = useMemo(() => computeMetrics(tx, income, config), [tx, income, config]);

  let title = "Summary";
  if (detail) title = detail; else title = { home: "Summary", income: "Income", insights: "Insights", setup: "Budgets" }[view];

  return (
    <div style={{ minHeight: "100vh", background: T.bg, color: T.text, fontFamily: SF, WebkitFontSmoothing: "antialiased" }}>
      <style>{`
        *{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
        ::-webkit-scrollbar{width:0;height:0;}
        input,select,button{font-family:inherit;} input:focus,select:focus{outline:none;}
        ::placeholder{color:${T.faint};}
        .press{transition:transform .15s cubic-bezier(.2,.8,.2,1),opacity .15s;} .press:active{transform:scale(.96);opacity:.85;}
        .vin{animation:vin .42s cubic-bezier(.22,.61,.36,1) both;}@keyframes vin{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
        .pushin{animation:pushin .34s cubic-bezier(.32,.72,0,1) both;}@keyframes pushin{from{opacity:0;transform:translateX(24px);}to{opacity:1;transform:none;}}
        .stagger>*{animation:vin .5s cubic-bezier(.22,.61,.36,1) both;}
        .stagger>*:nth-child(1){animation-delay:.02s}.stagger>*:nth-child(2){animation-delay:.07s}.stagger>*:nth-child(3){animation-delay:.12s}.stagger>*:nth-child(4){animation-delay:.17s}.stagger>*:nth-child(5){animation-delay:.22s}
        .back{animation:bk .3s ease both;}@keyframes bk{from{opacity:0}to{opacity:1}}
        .sheet{animation:sh .46s cubic-bezier(.32,.72,0,1) both;}@keyframes sh{from{transform:translateY(100%)}to{transform:none}}
        .pop{animation:pp .36s cubic-bezier(.32,.72,0,1) both;}@keyframes pp{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:none}}
        .spin{animation:spin 1s linear infinite;}@keyframes spin{to{transform:rotate(360deg)}}
        .bar{transition:width .7s cubic-bezier(.22,.61,.36,1);}
        .grow{transform-origin:bottom;animation:grow .6s cubic-bezier(.22,.61,.36,1) both;}@keyframes grow{from{transform:scaleY(0)}to{transform:scaleY(1)}}
        select option{color:#1D1D1F}
      `}</style>

      <div style={{ maxWidth: 480, margin: "0 auto", padding: "0 16px 122px" }}>
        <header style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "calc(env(safe-area-inset-top) + 20px) 2px 10px", minHeight: 64 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
            {detail && <button className="press" onClick={() => setDetail(null)} style={{ background: "none", border: "none", color: T.blue, cursor: "pointer", display: "flex", alignItems: "center", marginLeft: -6 }}><IChevL size={26} sw={2.4} /></button>}
            <span style={{ fontSize: detail ? 26 : 34, fontWeight: 800, letterSpacing: "-0.02em", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{title}</span>
          </div>
          <button onClick={() => setAdding(true)} className="press" aria-label="Add" style={{ width: 38, height: 38, borderRadius: 19, border: "none", cursor: "pointer", background: T.ink, color: "#fff", display: "grid", placeItems: "center", boxShadow: "0 4px 14px rgba(0,0,0,.18)", flexShrink: 0 }}><IPlus size={22} sw={2.2} /></button>
        </header>

        {loading ? <div style={{ textAlign: "center", color: T.faint, padding: 80 }}>Loading…</div>
          : detail ? <div key={detail} className="pushin"><CategoryDetail cat={detail} metrics={metrics} tx={tx} onDelete={(id) => saveTx(tx.filter((x) => x.id !== id))} /></div>
            : <div key={view} className="vin">
              {view === "home" ? <HomeView metrics={metrics} config={config} tx={tx} onDelete={(id) => saveTx(tx.filter((x) => x.id !== id))} onOpenCat={setDetail} />
                : view === "income" ? <IncomeView metrics={metrics} income={income} onDelete={(id) => saveIncome(income.filter((x) => x.id !== id))} />
                  : view === "insights" ? <InsightsView metrics={metrics} config={config} />
                    : <SetupView config={config} onSave={saveConfig} />}
            </div>}
      </div>

      <nav style={{ position: "fixed", bottom: 0, left: 0, right: 0, maxWidth: 480, margin: "0 auto", background: "rgba(249,249,251,.82)", backdropFilter: "saturate(180%) blur(20px)", WebkitBackdropFilter: "saturate(180%) blur(20px)", borderTop: `1px solid ${T.sep}`, display: "flex", justifyContent: "space-around", padding: "9px 6px calc(8px + env(safe-area-inset-bottom))" }}>
        <Tab icon={IHome} label="Summary" active={view === "home" && !detail} onClick={() => { setDetail(null); setView("home"); }} />
        <Tab icon={IWallet} label="Income" active={view === "income" && !detail} onClick={() => { setDetail(null); setView("income"); }} />
        <Tab icon={IChart} label="Insights" active={view === "insights" && !detail} onClick={() => { setDetail(null); setView("insights"); }} />
        <Tab icon={ISliders} label="Budgets" active={view === "setup" && !detail} onClick={() => { setDetail(null); setView("setup"); }} />
      </nav>

      {adding && <AddSheet config={config} metrics={metrics} income={income} onClose={() => setAdding(false)} onExpense={addExpense} onIncome={addIncome} />}
      {confirm && <ConfirmCard data={confirm} goal={config.goal} onClose={() => setConfirm(null)} />}
    </div>
  );
}

/* ================================================================== */
/*  METRICS                                                            */
/* ================================================================== */
function bucketSum(arr, b) { return arr.filter((x) => { const a = ageDays(x.date); return a >= b * 30 && a < (b + 1) * 30; }).reduce((s, x) => s + x.amount, 0); }
function computeMetrics(tx, income, config) {
  const mk = thisMonth();
  const thisTx = tx.filter((t) => monthKey(t.date) === mk);
  const byCat = {}; CATS.forEach((c) => (byCat[c.name] = 0));
  thisTx.forEach((t) => (byCat[t.category] += t.amount));
  const totalSpent = thisTx.reduce((a, b) => a + b.amount, 0);
  const totalBudget = Object.values(config.budgets).reduce((a, b) => a + b, 0);
  const cats = CATS.map((c) => { const spent = byCat[c.name] || 0, budget = config.budgets[c.name] || 0; return { ...c, spent, budget, remaining: budget - spent, score: scoreFor(spent, budget) }; });
  const scored = cats.filter((c) => c.score !== null); const wT = scored.reduce((a, b) => a + b.budget, 0) || 1;
  const overall = Math.round(scored.reduce((a, b) => a + b.score * b.budget, 0) / wT);

  // income projection from 3 trailing 30-day buckets
  const iB = [0, 1, 2].map((b) => bucketSum(income, b));
  const iNonzero = iB.filter((x) => x > 0);
  const projIncome = iNonzero.length ? iNonzero.reduce((a, b) => a + b, 0) / iNonzero.length : 0;
  const incLow = iNonzero.length ? Math.min(...iNonzero) : 0;
  const incHigh = iNonzero.length ? Math.max(...iNonzero) : 0;
  // spend projection
  const eB = [0, 1, 2].map((b) => bucketSum(tx, b)); const eNz = eB.filter((x) => x > 0);
  const projSpend = eNz.length ? eNz.reduce((a, b) => a + b, 0) / eNz.length : totalSpent;
  const saveRate = Math.max(0, projIncome - projSpend);
  const g = config.goal; const toGo = Math.max(0, g.target - g.saved);
  const monthsToGoal = saveRate > 0 ? Math.ceil(toGo / saveRate) : null;
  const monthIncome = income.filter((x) => monthKey(x.date) === mk).reduce((a, b) => a + b.amount, 0);

  // 6-month income trend
  const trend = []; const base = new Date(); base.setDate(1);
  for (let i = 5; i >= 0; i--) { const d = new Date(base.getFullYear(), base.getMonth() - i, 1); const key = d.toISOString().slice(0, 7); trend.push({ key, label: d.toLocaleDateString("en-US", { month: "short" }), amount: income.filter((x) => monthKey(x.date) === key).reduce((a, b) => a + b.amount, 0) }); }
  const trendMax = Math.max(1, ...trend.map((t) => t.amount));

  // sources
  const srcMap = {}; income.filter((x) => ageDays(x.date) < 90).forEach((x) => (srcMap[x.source] = (srcMap[x.source] || 0) + x.amount));
  const sources = Object.entries(srcMap).map(([name, amt]) => ({ name, amt })).sort((a, b) => b.amt - a.amt);

  return { thisTx, cats, totalSpent, totalBudget, overall, projIncome, incLow, incHigh, projSpend, saveRate, monthsToGoal, monthIncome, trend, trendMax, sources, leftToSpend: totalBudget - totalSpent };
}

/* ================================================================== */
/*  HOME                                                               */
/* ================================================================== */
const TIERS = [[0, "Getting started"], [25, "Building"], [50, "Momentum"], [75, "Closing in"], [95, "Almost there"]];
function tierFor(pct) { let t = TIERS[0][1]; TIERS.forEach(([p, n]) => { if (pct >= p) t = n; }); return t; }

function HomeView({ metrics, config, tx, onDelete, onOpenCat }) {
  const g = config.goal; const pct = Math.min(100, (g.saved / g.target) * 100);
  const saved = useCountUp(g.saved);
  const recent = [...tx].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 5);
  const used = metrics.cats.filter((c) => c.budget > 0);

  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      {/* Goal */}
      <div style={{ ...card, padding: "22px 22px 20px" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
          <span style={{ color: T.dim, fontSize: 14, fontWeight: 600 }}>{g.name}</span>
          <span style={{ fontSize: 12, fontWeight: 700, color: T.waffle, background: T.waffle + "1C", padding: "3px 9px", borderRadius: 20 }}>{tierFor(pct)}</span>
        </div>
        <div style={{ fontSize: 44, fontWeight: 800, letterSpacing: "-0.03em", marginTop: 6, fontVariantNumeric: "tabular-nums" }}>{fmt(saved)}</div>
        <div style={{ color: T.dim, fontSize: 14, marginTop: 1 }}>of {fmt(g.target)}</div>
        {/* bar with milestone ticks */}
        <div style={{ position: "relative", height: 10, marginTop: 16 }}>
          <div style={{ position: "absolute", inset: 0, borderRadius: 5, background: T.fill2, overflow: "hidden" }}>
            <div className="bar" style={{ height: "100%", width: `${pct}%`, background: `linear-gradient(90deg,${T.waffle},${T.orange})`, borderRadius: 5 }} />
          </div>
          {[25, 50, 75].map((m) => (
            <div key={m} style={{ position: "absolute", left: `${m}%`, top: -1, width: 2, height: 12, background: pct >= m ? "rgba(255,255,255,.85)" : T.faint, borderRadius: 1, transform: "translateX(-1px)" }} />
          ))}
        </div>
        {metrics.monthsToGoal && (
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 13 }}>
            <span style={{ fontSize: 11.5, fontWeight: 700, color: T.green, background: T.green + "18", padding: "3px 9px", borderRadius: 20 }}>ON PACE</span>
            <span style={{ fontSize: 13.5, color: T.dim }}>~{metrics.monthsToGoal} months to go at {fmt(metrics.saveRate)}/mo saved</span>
          </div>
        )}
      </div>

      {/* Projected income mini (variable) */}
      <div style={{ ...card, padding: "16px 20px", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
        <div>
          <div style={{ fontSize: 13, color: T.dim, fontWeight: 600 }}>Projected income</div>
          <div style={{ fontSize: 22, fontWeight: 700, marginTop: 2, fontVariantNumeric: "tabular-nums" }}>~{fmt(metrics.projIncome)}<span style={{ fontSize: 14, color: T.faint, fontWeight: 500 }}>/mo</span></div>
        </div>
        <div style={{ textAlign: "right" }}>
          <div style={{ fontSize: 12, color: T.faint }}>typical range</div>
          <div style={{ fontSize: 14, color: T.dim, fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{fmtK(metrics.incLow)} – {fmtK(metrics.incHigh)}</div>
        </div>
      </div>

      {/* This month */}
      <div style={{ ...card, padding: "18px 22px" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
          <span style={{ fontSize: 15, fontWeight: 600 }}>Spent this month</span>
          <span style={{ fontSize: 14, color: metrics.leftToSpend < 0 ? T.red : T.dim, fontVariantNumeric: "tabular-nums" }}>{fmt(metrics.totalSpent)} of {fmt(metrics.totalBudget)}</span>
        </div>
        <div style={{ height: 8, borderRadius: 4, background: T.fill2, overflow: "hidden", marginTop: 12 }}>
          <div className="bar" style={{ height: "100%", width: `${Math.min(100, (metrics.totalSpent / metrics.totalBudget) * 100)}%`, background: metrics.leftToSpend < 0 ? T.red : T.blue, borderRadius: 4 }} />
        </div>
      </div>

      {/* Budgets (tap → detail) */}
      <Group label="Budgets">
        {used.map((c, i) => {
          const over = c.remaining < 0;
          return (
            <Row key={c.name} last={i === used.length - 1} onClick={() => onOpenCat(c.name)}>
              <span style={{ width: 11, height: 11, borderRadius: 6, background: c.color, flexShrink: 0 }} />
              <div style={{ flex: 1 }}>
                <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
                  <span style={{ fontSize: 15.5, fontWeight: 500 }}>{c.name}</span>
                  <span style={{ fontSize: 13.5, color: over ? T.red : T.dim, fontVariantNumeric: "tabular-nums" }}>{over ? `${fmt(-c.remaining)} over` : `${fmt(c.remaining)} left`}</span>
                </div>
                <div style={{ height: 5, borderRadius: 3, background: T.fill2, overflow: "hidden" }}>
                  <div className="bar" style={{ height: "100%", width: `${Math.min(100, (c.spent / c.budget) * 100)}%`, background: over ? T.red : c.color, borderRadius: 3 }} />
                </div>
              </div>
              <IChevR size={18} color={T.faint} sw={2} />
            </Row>
          );
        })}
      </Group>

      {recent.length > 0 && (
        <Group label="Recent">
          {recent.map((r, i) => <TxRow key={r.id} r={r} last={i === recent.length - 1} onDelete={onDelete} />)}
        </Group>
      )}
    </div>
  );
}

function TxRow({ r, last, onDelete }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <Row last={last && !open} onClick={() => setOpen((o) => !o)}>
        <span style={{ width: 30, height: 30, borderRadius: 8, background: (COLOR[r.category] || T.faint) + "22", display: "grid", placeItems: "center", flexShrink: 0 }}><span style={{ width: 11, height: 11, borderRadius: 6, background: COLOR[r.category] || T.faint }} /></span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{r.who}</div>
          <div style={{ fontSize: 12.5, color: T.faint }}>{r.sub ? `${r.category} · ${r.sub}` : r.category} · {prettyDate(r.date)}</div>
        </div>
        <span style={{ fontSize: 15.5, fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{fmt(r.amount)}</span>
      </Row>
      {open && <div className="pop" style={{ display: "flex", justifyContent: "flex-end", padding: "8px 16px 12px", borderBottom: last ? "none" : `1px solid ${T.sep}` }}><button className="press" onClick={() => onDelete(r.id)} style={{ background: "none", border: "none", color: T.red, fontSize: 14, fontWeight: 500, cursor: "pointer" }}>Delete</button></div>}
    </div>
  );
}

/* ================================================================== */
/*  CATEGORY DETAIL (subcategories)                                    */
/* ================================================================== */
function CategoryDetail({ cat, metrics, tx, onDelete }) {
  const c = metrics.cats.find((x) => x.name === cat) || {};
  const rows = tx.filter((t) => t.category === cat && monthKey(t.date) === thisMonth()).sort((a, b) => b.date.localeCompare(a.date));
  const subMap = {}; rows.forEach((t) => { const k = t.sub || "Other"; subMap[k] = (subMap[k] || 0) + t.amount; });
  const subs = Object.entries(subMap).map(([name, amt]) => ({ name, amt })).sort((a, b) => b.amt - a.amt);
  const max = Math.max(1, ...subs.map((s) => s.amt));
  const over = (c.remaining || 0) < 0;
  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <div style={{ ...card, padding: "20px 22px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <span style={{ width: 13, height: 13, borderRadius: 7, background: c.color }} />
          <span style={{ fontSize: 16, fontWeight: 600 }}>{cat}</span>
        </div>
        <div style={{ fontSize: 34, fontWeight: 800, letterSpacing: "-0.02em", marginTop: 8, fontVariantNumeric: "tabular-nums" }}>{fmt(c.spent || 0)}<span style={{ fontSize: 18, color: T.faint, fontWeight: 500 }}> / {fmt(c.budget || 0)}</span></div>
        <div style={{ height: 8, borderRadius: 4, background: T.fill2, overflow: "hidden", marginTop: 12 }}>
          <div className="bar" style={{ height: "100%", width: `${Math.min(100, ((c.spent || 0) / (c.budget || 1)) * 100)}%`, background: over ? T.red : c.color, borderRadius: 4 }} />
        </div>
        <div style={{ fontSize: 13.5, color: over ? T.red : T.dim, marginTop: 9 }}>{over ? `${fmt(-(c.remaining))} over budget` : `${fmt(c.remaining)} left this month`}</div>
      </div>

      {subs.length > 0 && (
        <Group label="Breakdown">
          {subs.map((s, i) => (
            <Row key={s.name} last={i === subs.length - 1}>
              <span style={{ fontSize: 15.5, fontWeight: 500, width: 120 }}>{s.name}</span>
              <div style={{ flex: 1, height: 7, borderRadius: 4, background: T.fill2, overflow: "hidden" }}>
                <div className="bar" style={{ height: "100%", width: `${(s.amt / max) * 100}%`, background: c.color, borderRadius: 4 }} />
              </div>
              <span style={{ fontSize: 14, fontWeight: 600, fontVariantNumeric: "tabular-nums", width: 64, textAlign: "right" }}>{fmt(s.amt)}</span>
            </Row>
          ))}
        </Group>
      )}

      <Group label="Transactions">
        {rows.length ? rows.map((r, i) => <TxRow key={r.id} r={r} last={i === rows.length - 1} onDelete={onDelete} />)
          : <div style={{ padding: "18px 16px", color: T.faint, fontSize: 14 }}>Nothing logged here this month.</div>}
      </Group>
    </div>
  );
}

/* ================================================================== */
/*  INCOME                                                             */
/* ================================================================== */
function IncomeView({ metrics, income, onDelete }) {
  const proj = useCountUp(metrics.projIncome);
  const recent = [...income].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 8);
  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      {/* projection */}
      <div style={{ ...card, padding: "22px 22px 20px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 7, color: T.dim, fontSize: 14, fontWeight: 600 }}><ITrend size={15} color={T.dim} sw={2} /> Projected this month</div>
        <div style={{ fontSize: 44, fontWeight: 800, letterSpacing: "-0.03em", marginTop: 6, fontVariantNumeric: "tabular-nums" }}>{fmt(proj)}</div>
        <div style={{ color: T.dim, fontSize: 14, marginTop: 1 }}>based on your last 90 days · typically {fmtK(metrics.incLow)}–{fmtK(metrics.incHigh)}</div>
        <div style={{ marginTop: 14, padding: "10px 14px", borderRadius: 12, background: T.fill, fontSize: 13, color: T.dim, lineHeight: 1.45 }}>
          So far this month you've brought in <span style={{ color: T.text, fontWeight: 600 }}>{fmt(metrics.monthIncome)}</span>. Variable income — Waffle averages your recent pace so the goal math stays honest.
        </div>
      </div>

      {/* trend */}
      <div style={{ ...card, padding: "20px 22px 16px" }}>
        <div style={{ fontSize: 15, fontWeight: 600, marginBottom: 4 }}>Last 6 months</div>
        <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 8, height: 110, marginTop: 14 }}>
          {metrics.trend.map((m) => (
            <div key={m.key} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}>
              <div style={{ fontSize: 10.5, color: T.dim, fontVariantNumeric: "tabular-nums" }}>{m.amount > 0 ? fmtK(m.amount) : ""}</div>
              <div className="grow" style={{ width: "62%", height: `${Math.max(3, (m.amount / metrics.trendMax) * 80)}px`, background: m.amount > 0 ? `linear-gradient(180deg,${T.waffle},${T.orange})` : T.fill2, borderRadius: 6 }} />
              <div style={{ fontSize: 10.5, color: T.faint }}>{m.label}</div>
            </div>
          ))}
        </div>
      </div>

      {/* sources */}
      {metrics.sources.length > 0 && (
        <Group label="Sources · last 90 days">
          {metrics.sources.map((s, i) => (
            <Row key={s.name} last={i === metrics.sources.length - 1}>
              <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{s.name}</span>
              <span style={{ fontSize: 14, fontWeight: 600, color: T.green, fontVariantNumeric: "tabular-nums" }}>{fmt(s.amt)}</span>
            </Row>
          ))}
        </Group>
      )}

      <Group label="Recent income">
        {recent.length ? recent.map((r, i) => (
          <IncRow key={r.id} r={r} last={i === recent.length - 1} onDelete={onDelete} />
        )) : <div style={{ padding: "18px 16px", color: T.faint, fontSize: 14 }}>Add income with the + button.</div>}
      </Group>
    </div>
  );
}
function IncRow({ r, last, onDelete }) {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <Row last={last && !open} onClick={() => setOpen((o) => !o)}>
        <span style={{ width: 30, height: 30, borderRadius: 8, background: T.green + "1C", display: "grid", placeItems: "center", flexShrink: 0 }}><IPlus size={15} color={T.green} sw={2.4} /></span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{r.source}</div>
          <div style={{ fontSize: 12.5, color: T.faint }}>{prettyDate(r.date)}</div>
        </div>
        <span style={{ fontSize: 15.5, fontWeight: 600, color: T.green, fontVariantNumeric: "tabular-nums" }}>+{fmt(r.amount)}</span>
      </Row>
      {open && <div className="pop" style={{ display: "flex", justifyContent: "flex-end", padding: "8px 16px 12px", borderBottom: last ? "none" : `1px solid ${T.sep}` }}><button className="press" onClick={() => onDelete(r.id)} style={{ background: "none", border: "none", color: T.red, fontSize: 14, fontWeight: 500, cursor: "pointer" }}>Delete</button></div>}
    </div>
  );
}

/* ================================================================== */
/*  INSIGHTS                                                           */
/* ================================================================== */
function InsightsView({ metrics, config }) {
  const [coach, setCoach] = useState(""); const [st, setSt] = useState("");
  const ring = useCountUp(metrics.overall); const C = 2 * Math.PI * 54;
  const run = async () => { setSt("loading"); try { setCoach(await aiCoach({ projectedIncome: Math.round(metrics.projIncome), incomeRange: [Math.round(metrics.incLow), Math.round(metrics.incHigh)], goal: config.goal, monthsToGoal: metrics.monthsToGoal, categories: metrics.cats.filter((c) => c.budget > 0).map((c) => ({ name: c.name, spent: Math.round(c.spent), budget: c.budget, score: c.score })) })); setSt("done"); } catch (e) { setSt("error"); } };
  const used = [...metrics.cats].filter((c) => c.budget > 0).sort((a, b) => a.score - b.score);
  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <div style={{ ...card, padding: "26px 22px", display: "flex", flexDirection: "column", alignItems: "center" }}>
        <div style={{ position: "relative", width: 150, height: 150 }}>
          <svg width="150" height="150" style={{ transform: "rotate(-90deg)" }}>
            <circle cx="75" cy="75" r="54" fill="none" stroke={T.fill2} strokeWidth="13" />
            <circle cx="75" cy="75" r="54" fill="none" stroke={scoreColor(metrics.overall)} strokeWidth="13" strokeLinecap="round" strokeDasharray={C} strokeDashoffset={C - (C * Math.round(ring)) / 100} style={{ transition: "stroke-dashoffset .2s linear" }} />
          </svg>
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}>
            <div style={{ textAlign: "center" }}><div style={{ fontSize: 46, fontWeight: 800, letterSpacing: "-0.03em", color: scoreColor(metrics.overall), fontVariantNumeric: "tabular-nums" }}>{Math.round(ring)}</div><div style={{ fontSize: 12, color: T.faint, marginTop: -2 }}>SCORE</div></div>
          </div>
        </div>
        <div style={{ fontSize: 14.5, color: T.dim, marginTop: 14, fontWeight: 500 }}>{metrics.overall >= 80 ? "Dialed in this month." : metrics.overall >= 50 ? "A few leaks to plug." : "Some categories ran hot."}</div>
      </div>
      <Group label="By category">
        {used.map((c, i) => (
          <Row key={c.name} last={i === used.length - 1}>
            <span style={{ fontSize: 17, fontWeight: 700, color: scoreColor(c.score), width: 34, fontVariantNumeric: "tabular-nums" }}>{c.score}</span>
            <div style={{ flex: 1 }}>
              <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}><span style={{ fontSize: 15.5, fontWeight: 500 }}>{c.name}</span><span style={{ fontSize: 13, color: T.faint, fontVariantNumeric: "tabular-nums" }}>{fmt(c.spent)} / {fmt(c.budget)}</span></div>
              <div style={{ height: 5, borderRadius: 3, background: T.fill2, overflow: "hidden" }}><div className="bar" style={{ height: "100%", width: `${Math.min(100, (c.spent / c.budget) * 100)}%`, background: scoreColor(c.score), borderRadius: 3 }} /></div>
            </div>
          </Row>
        ))}
      </Group>
      <div style={{ ...card, padding: "20px 22px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 8 }}><ISpark size={17} color={T.orange} /><span style={{ fontSize: 17, fontWeight: 700 }}>Coach</span></div>
        {st === "done" ? <div style={{ fontSize: 15, lineHeight: 1.55, whiteSpace: "pre-wrap" }}>{coach}</div> : <div style={{ fontSize: 14, color: T.dim, lineHeight: 1.5 }}>A read on your month — income range, what ran hot, and what it means for {config.goal.name}.</div>}
        {st === "error" && <div style={{ color: T.red, fontSize: 13, marginTop: 8 }}>Couldn't reach the coach. Try again.</div>}
        <button className="press" onClick={run} disabled={st === "loading"} style={{ ...pill, width: "100%", marginTop: 16, display: "flex", alignItems: "center", justifyContent: "center", gap: 8 }}>{st === "loading" ? <><ISpin size={17} className="spin" color="#fff" /> Thinking…</> : st === "done" ? "Refresh" : "Get my read"}</button>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  ADD SHEET (expense + income)                                       */
/* ================================================================== */
function AddSheet({ config, metrics, income, onClose, onExpense, onIncome }) {
  const [type, setType] = useState("expense");
  const [mode, setMode] = useState("manual");
  const [who, setWho] = useState(""); const [amount, setAmount] = useState("");
  const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
  const [category, setCategory] = useState("Other"); const [sub, setSub] = useState("");
  const [source, setSource] = useState(""); const [items, setItems] = useState(null);
  const [ai, setAi] = useState(""); const [conf, setConf] = useState(null);
  const fileRef = useRef();
  useEffect(() => { if (type === "expense" && who && ai !== "done") setCategory(localGuess(who)); }, [who, type]);
  useEffect(() => { setSub(""); }, [category]);

  const knownSources = [...new Set(income.map((x) => x.source))].slice(0, 5);
  const askAI = async () => { if (!who) return; setAi("loading"); try { const g = await aiCategorize(who, amount || 0); if (g.category) setCategory(g.category); if (g.sub) setSub(g.sub); setConf(g.confidence != null ? Math.round(g.confidence * 100) : null); setAi("done"); } catch (e) { setCategory(localGuess(who)); setAi("error"); } };
  const onReceipt = async (e) => { const f = e.target.files && e.target.files[0]; if (!f) return; setMode("receipt"); setAi("loading"); try { const b64 = await new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(r.result.split(",")[1]); r.onerror = rej; r.readAsDataURL(f); }); const out = await aiReadReceipt(b64, f.type || "image/jpeg"); if (out.who) setWho(out.who); if (out.amount) setAmount(String(out.amount)); if (out.date) setDate(out.date); if (out.category) setCategory(out.category); if (out.sub) setSub(out.sub); if (out.items) setItems(out.items); setAi("done"); } catch (err) { setAi("error"); } };

  const budget = config.budgets[category] || 0;
  const spentInCat = (metrics.cats.find((c) => c.name === category) || {}).spent || 0;
  const remainAfter = budget - (spentInCat + (parseFloat(amount) || 0));
  const subList = SUBS[category] || [];

  const submit = () => {
    const amt = parseFloat(amount); if (!amt) return;
    if (type === "income") { if (!source.trim()) return; onIncome({ source: source.trim(), amount: amt, date }); }
    else { if (!who.trim()) return; onExpense({ who: who.trim(), amount: amt, date, category, sub: sub || undefined, items: items || undefined }); }
  };

  return (
    <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 50, background: "rgba(0,0,0,.28)", backdropFilter: "blur(2px)", display: "flex", alignItems: "flex-end", justifyContent: "center" }}>
      <div className="sheet" onClick={(e) => e.stopPropagation()} style={{ width: "100%", maxWidth: 480, background: T.bg, borderRadius: "28px 28px 0 0", padding: "10px 18px calc(22px + env(safe-area-inset-bottom))", maxHeight: "94vh", overflowY: "auto" }}>
        <div style={{ width: 38, height: 5, borderRadius: 3, background: "#C7C7CC", margin: "0 auto 12px" }} />
        {/* type toggle */}
        <div style={{ display: "flex", background: T.fill2, borderRadius: 10, padding: 2, marginBottom: 16 }}>
          <Seg active={type === "expense"} onClick={() => setType("expense")} label="Expense" />
          <Seg active={type === "income"} onClick={() => setType("income")} label="Income" />
        </div>

        {type === "income" ? (
          <>
            <Field label="Source">
              <input value={source} onChange={(e) => setSource(e.target.value)} placeholder="Fire Dept, Overtime, web client…" style={inp} />
              {knownSources.length > 0 && (
                <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginTop: 9 }}>
                  {knownSources.map((s) => <Chip key={s} active={source === s} onClick={() => setSource(s)} label={s} />)}
                </div>
              )}
            </Field>
            <div style={{ display: "flex", gap: 12 }}>
              <Field label="Amount" flex><div style={{ position: "relative" }}><span style={{ position: "absolute", left: 14, top: 13, color: T.faint, fontWeight: 600 }}>$</span><input value={amount} onChange={(e) => setAmount(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="0" inputMode="decimal" style={{ ...inp, paddingLeft: 26, fontVariantNumeric: "tabular-nums" }} /></div></Field>
              <Field label="Date" flex><input type="date" value={date} onChange={(e) => setDate(e.target.value)} style={inp} /></Field>
            </div>
            <button className="press" onClick={submit} style={{ ...pill, width: "100%", background: T.green }}>Add income</button>
          </>
        ) : (
          <>
            <div style={{ display: "flex", background: T.fill2, borderRadius: 10, padding: 2, marginBottom: 16 }}>
              <Seg active={mode === "manual"} onClick={() => setMode("manual")} label="Quick add" />
              <Seg active={mode === "receipt"} onClick={() => fileRef.current && fileRef.current.click()} label="Snap receipt" />
            </div>
            <input ref={fileRef} type="file" accept="image/*" capture="environment" onChange={onReceipt} style={{ display: "none" }} />
            {mode === "receipt" && (
              <div style={{ ...card, padding: 14, marginBottom: 14, display: "flex", alignItems: "center", gap: 11 }}>
                {ai === "loading" ? <ISpin size={18} className="spin" color={T.blue} /> : <ICam size={18} color={T.blue} />}
                <span style={{ fontSize: 13.5, color: T.dim, flex: 1 }}>{ai === "loading" ? "Reading your receipt…" : ai === "done" ? "Got it — check & save." : ai === "error" ? "Couldn't read it. Enter manually." : "Tap to photograph your receipt."}</span>
                <button className="press" onClick={() => fileRef.current && fileRef.current.click()} style={{ ...pillSm }}>Photo</button>
              </div>
            )}
            <Field label="Merchant"><input value={who} onChange={(e) => { setWho(e.target.value); setAi(""); }} placeholder="Shell, Target, Geico…" style={inp} /></Field>
            <div style={{ display: "flex", gap: 12 }}>
              <Field label="Amount" flex><div style={{ position: "relative" }}><span style={{ position: "absolute", left: 14, top: 13, color: T.faint, fontWeight: 600 }}>$</span><input value={amount} onChange={(e) => setAmount(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="0" inputMode="decimal" style={{ ...inp, paddingLeft: 26, fontVariantNumeric: "tabular-nums" }} /></div></Field>
              <Field label="Date" flex><input type="date" value={date} onChange={(e) => setDate(e.target.value)} style={inp} /></Field>
            </div>
            <div style={{ marginBottom: 14 }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 7 }}>
                <span style={lbl}>Category</span>
                <button className="press" onClick={askAI} disabled={!who || ai === "loading"} style={{ display: "flex", alignItems: "center", gap: 5, background: "none", border: "none", color: who ? T.blue : T.faint, fontSize: 13.5, fontWeight: 600, cursor: who ? "pointer" : "default" }}>{ai === "loading" ? <ISpin size={14} className="spin" /> : <ISpark size={14} color={who ? T.blue : T.faint} />}{ai === "done" ? (conf != null ? `${conf}% sure` : "Suggested") : "Ask AI"}</button>
              </div>
              <select value={category} onChange={(e) => setCategory(e.target.value)} style={inp}>{CATS.map((c) => <option key={c.name} value={c.name}>{c.name}</option>)}</select>
            </div>
            {/* subcategory chips — only shows if the category has them */}
            {subList.length > 0 && (
              <div style={{ marginBottom: 14 }}>
                <div style={lbl}>Detail <span style={{ color: T.faint, fontWeight: 500, textTransform: "none" }}>· optional</span></div>
                <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginTop: 2 }}>
                  {subList.map((s) => <Chip key={s} active={sub === s} onClick={() => setSub(sub === s ? "" : s)} label={s} />)}
                </div>
              </div>
            )}
            {budget > 0 && (
              <div style={{ ...card, padding: "14px 16px", marginBottom: 18 }}>
                <div style={{ display: "flex", justifyContent: "space-between", fontSize: 13.5, marginBottom: 8 }}><span style={{ color: T.dim }}>{category} after this</span><span style={{ color: remainAfter < 0 ? T.red : T.green, fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{remainAfter < 0 ? `${fmt(-remainAfter)} over` : `${fmt(remainAfter)} left`}</span></div>
                <div style={{ height: 6, borderRadius: 3, background: T.fill2, overflow: "hidden" }}><div className="bar" style={{ height: "100%", width: `${Math.min(100, ((spentInCat + (parseFloat(amount) || 0)) / budget) * 100)}%`, background: remainAfter < 0 ? T.red : COLOR[category], borderRadius: 3 }} /></div>
              </div>
            )}
            <button className="press" onClick={submit} style={{ ...pill, width: "100%" }}>Add expense</button>
          </>
        )}
      </div>
    </div>
  );
}

/* ================================================================== */
/*  CONFIRM                                                            */
/* ================================================================== */
function ConfirmCard({ data, goal, onClose }) {
  if (data.kind === "income") {
    const { rec, monthIncome } = data;
    return (
      <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(0,0,0,.28)", backdropFilter: "blur(2px)", display: "grid", placeItems: "center", padding: 24 }}>
        <div className="pop" onClick={(e) => e.stopPropagation()} style={{ ...card, width: "100%", maxWidth: 330, padding: 22, textAlign: "center" }}>
          <div style={{ width: 52, height: 52, borderRadius: 26, margin: "0 auto 12px", background: T.green + "1A", display: "grid", placeItems: "center" }}><IPlus size={26} color={T.green} sw={2.6} /></div>
          <div style={{ fontSize: 19, fontWeight: 700 }}>+{fmt(rec.amount)} · {rec.source}</div>
          <div style={{ fontSize: 14, color: T.dim, marginTop: 8, lineHeight: 1.45 }}>{fmt(monthIncome)} in this month. Keep stacking toward {goal.name} 🧇</div>
          <button className="press" onClick={onClose} style={{ ...pill, width: "100%", marginTop: 18, background: T.green }}>Nice</button>
        </div>
      </div>
    );
  }
  const { rec, spent, budget } = data; const remaining = budget - spent; const over = remaining < 0; const pct = budget ? Math.min(100, (spent / budget) * 100) : 0;
  return (
    <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(0,0,0,.28)", backdropFilter: "blur(2px)", display: "grid", placeItems: "center", padding: 24 }}>
      <div className="pop" onClick={(e) => e.stopPropagation()} style={{ ...card, width: "100%", maxWidth: 330, padding: 22, textAlign: "center" }}>
        <div style={{ width: 52, height: 52, borderRadius: 26, margin: "0 auto 12px", background: (over ? T.red : T.green) + "1A", display: "grid", placeItems: "center" }}>{over ? <span style={{ fontSize: 26 }}>⚠️</span> : <ICheck size={26} color={T.green} sw={2.4} />}</div>
        <div style={{ fontSize: 19, fontWeight: 700 }}>{fmt(rec.amount)} · {rec.who}</div>
        <div style={{ fontSize: 13.5, color: T.dim, marginTop: 2, marginBottom: 16 }}>{rec.sub ? `${rec.category} · ${rec.sub}` : rec.category}</div>
        {budget > 0 && (<><div style={{ height: 8, borderRadius: 4, background: T.fill2, overflow: "hidden" }}><div className="bar" style={{ height: "100%", width: `${pct}%`, background: over ? T.red : COLOR[rec.category], borderRadius: 4 }} /></div><div style={{ fontSize: 14, marginTop: 12, lineHeight: 1.45, color: over ? T.red : T.text }}>{over ? `${fmt(-remaining)} over on ${rec.category}. Redirect it to ${goal.name} to stay on pace.` : `${fmt(remaining)} left in ${rec.category} this month.`}</div></>)}
        <button className="press" onClick={onClose} style={{ ...pill, width: "100%", marginTop: 18 }}>Done</button>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  SETUP                                                              */
/* ================================================================== */
function SetupView({ config, onSave }) {
  const [c, setC] = useState(config); const [saved, setSaved] = useState(false);
  const set = (path, v) => { const n = JSON.parse(JSON.stringify(c)); if (path[0] === "goal") n.goal[path[1]] = v; else if (path[0] === "budgets") n.budgets[path[1]] = v; else n[path[0]] = v; setC(n); setSaved(false); };
  const num = (v) => parseFloat(v) || 0;
  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <div style={{ ...card, padding: "18px 20px" }}>
        <div style={grpLbl}>GOAL</div>
        <Field label="Name"><input value={c.goal.name} onChange={(e) => set(["goal", "name"], e.target.value)} style={inp} /></Field>
        <div style={{ display: "flex", gap: 12 }}>
          <Field label="Target" flex><input value={c.goal.target} onChange={(e) => set(["goal", "target"], num(e.target.value))} style={inp} inputMode="numeric" /></Field>
          <Field label="Saved" flex><input value={c.goal.saved} onChange={(e) => set(["goal", "saved"], num(e.target.value))} style={inp} inputMode="numeric" /></Field>
        </div>
        <div style={{ fontSize: 12.5, color: T.faint, marginTop: 2 }}>Income is tracked on the Income tab — Waffle projects it from your last 90 days.</div>
      </div>
      <Group label="Category budgets">
        {CATS.map((cat, i) => (
          <Row key={cat.name} last={i === CATS.length - 1}>
            <span style={{ width: 11, height: 11, borderRadius: 6, background: cat.color, flexShrink: 0 }} />
            <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500 }}>{cat.name}</span>
            <div style={{ position: "relative", width: 96 }}><span style={{ position: "absolute", left: 12, top: 9, color: T.faint, fontSize: 14 }}>$</span><input value={c.budgets[cat.name] || 0} onChange={(e) => set(["budgets", cat.name], num(e.target.value))} inputMode="numeric" style={{ ...inp, padding: "8px 10px 8px 22px", fontSize: 14, textAlign: "right" }} /></div>
          </Row>
        ))}
      </Group>
      <button className="press" onClick={() => { onSave(c); setSaved(true); }} style={{ ...pill, width: "100%" }}>{saved ? "Saved ✓" : "Save"}</button>
    </div>
  );
}

/* ---- atoms ---- */
function Group({ label, children }) { return <div>{label && <div style={grpLbl}>{label.toUpperCase()}</div>}<div style={{ ...card, padding: "2px 0", marginTop: 8 }}>{children}</div></div>; }
function Row({ children, last, onClick }) { return <div onClick={onClick} className={onClick ? "press" : ""} style={{ display: "flex", alignItems: "center", gap: 12, padding: "13px 16px", borderBottom: last ? "none" : `1px solid ${T.sep}`, cursor: onClick ? "pointer" : "default" }}>{children}</div>; }
function Field({ label, children, flex }) { return <div style={{ marginBottom: 14, flex: flex ? 1 : undefined }}><div style={lbl}>{label}</div>{children}</div>; }
function Seg({ active, onClick, label }) { return <button onClick={onClick} style={{ flex: 1, border: "none", cursor: "pointer", padding: "8px 0", borderRadius: 8, fontSize: 14, fontWeight: 600, background: active ? "#fff" : "transparent", color: active ? T.text : T.dim, boxShadow: active ? "0 1px 3px rgba(0,0,0,.12)" : "none", transition: "all .2s" }}>{label}</button>; }
function Chip({ active, onClick, label }) { return <button className="press" onClick={onClick} style={{ border: "none", cursor: "pointer", padding: "7px 13px", borderRadius: 20, fontSize: 13.5, fontWeight: 500, background: active ? T.ink : T.fill2, color: active ? "#fff" : T.text }}>{label}</button>; }
function Tab({ icon: Icon, label, active, onClick }) { return <button onClick={onClick} style={{ background: "none", border: "none", cursor: "pointer", display: "flex", flexDirection: "column", alignItems: "center", gap: 3, color: active ? T.blue : T.faint, padding: "2px 14px", transition: "color .2s" }}><Icon size={25} sw={active ? 2.2 : 1.9} /><span style={{ fontSize: 10.5, fontWeight: 600 }}>{label}</span></button>; }

const card = { background: T.card, borderRadius: 20, boxShadow: "0 1px 2px rgba(0,0,0,.04), 0 8px 24px rgba(0,0,0,.045)" };
const grpLbl = { fontSize: 12.5, color: T.faint, fontWeight: 600, letterSpacing: ".03em", paddingLeft: 6 };
const lbl = { fontSize: 12.5, color: T.dim, fontWeight: 600, marginBottom: 6, paddingLeft: 2, textTransform: "uppercase", letterSpacing: ".02em" };
const inp = { width: "100%", background: T.fill, border: "1px solid transparent", borderRadius: 12, padding: "12px 14px", color: T.text, fontSize: 16, fontWeight: 500 };
const pill = { background: T.ink, color: "#fff", border: "none", borderRadius: 14, padding: "15px", fontSize: 16, fontWeight: 600, cursor: "pointer" };
const pillSm = { background: T.ink, color: "#fff", border: "none", borderRadius: 10, padding: "7px 14px", fontSize: 13, fontWeight: 600, cursor: "pointer" };


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