/* 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";

/* ================================================================== */
/*  CLOUD — Supabase auth + per-user sync (RLS-protected)              */
/* ================================================================== */
const SUPABASE_URL = "https://vejakperpivdffwrchxu.supabase.co";
const SUPABASE_KEY = "sb_publishable_Zy5jzLSN6Rwt5IHAj-G-Iw_lyk_5B5I";
const _sbLib = (typeof window !== "undefined") ? (window.supabase || window.Supabase || null) : null;
const sb = (_sbLib && _sbLib.createClient && SUPABASE_URL && SUPABASE_KEY)
  ? _sbLib.createClient(SUPABASE_URL, SUPABASE_KEY, { auth: { persistSession: true, autoRefreshToken: true } })
  : null;

/* Pull this user's row. Returns { data, updated_at } or null if no row yet. */
async function cloudLoad(userId) {
  if (!sb) return null;
  const { data, error } = await sb.from("app_state").select("data, updated_at").eq("user_id", userId).maybeSingle();
  if (error) throw error;
  return data || null;
}
/* Upsert this user's whole state blob. */
async function cloudSave(userId, payload) {
  if (!sb) return;
  const { error } = await sb.from("app_state").upsert({ user_id: userId, data: payload, updated_at: new Date().toISOString() }, { onConflict: "user_id" });
  if (error) throw error;
}

/* ================================================================== */
/*  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 THEMES = {
  light: {
    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", navbg: "rgba(249,249,251,.82)", overlay: "rgba(0,0,0,.28)", grip: "#C7C7CC",
  },
  dark: {
    bg: "#000000", card: "#1C1C1E", text: "#F5F5F7", dim: "#9B9BA1", faint: "#6B6B70",
    sep: "rgba(255,255,255,0.12)", fill: "#2C2C2E", fill2: "#3A3A3C",
    blue: "#0A84FF", green: "#30D158", red: "#FF453A", orange: "#FF9F0A", ink: "#F5F5F7",
    waffle: "#FFB23E", navbg: "rgba(20,20,22,.82)", overlay: "rgba(0,0,0,.55)", grip: "#48484A",
  },
};
/* Active palette. Components read T.* directly; we repoint its contents on theme change
   and remount the tree so every inline style picks up the new values. */
const T = { ...THEMES.light };
function applyTheme(mode) {
  const resolved = mode === "dark" ? "dark" : mode === "light" ? "light"
    : (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
  Object.assign(T, THEMES[resolved]);
  try { if (typeof refreshStyleTokens === "function") refreshStyleTokens(); } catch (_) {}
  try {
    document.documentElement.style.background = T.bg;
    document.body.style.background = T.bg;
    const meta = document.querySelector('meta[name="theme-color"]'); if (meta) meta.setAttribute("content", T.bg);
  } catch (_) {}
  return resolved;
}
const PALETTE = ["#FF3B30", "#FF9500", "#FFCC00", "#34C759", "#00C7BE", "#5AC8FA", "#007AFF", "#5856D6", "#AF52DE", "#FF2D55", "#A2845E", "#8E8E93"];
/* `essential` = a fixed, must-pay cost that forms the floor; everything else is flex. */
const DEFAULT_ESSENTIALS = new Set(["Groceries", "Transport", "Health", "Bills"]);
const DEFAULT_CATEGORIES = [
  { name: "Groceries", color: "#34C759", essential: true }, { name: "Dining Out", color: "#FF9500", essential: false },
  { name: "Shopping", color: "#FF2D55", essential: false }, { name: "Electronics", color: "#007AFF", essential: false },
  { name: "Home", color: "#5856D6", essential: false }, { name: "Transport", color: "#5AC8FA", essential: true },
  { name: "Entertainment", color: "#AF52DE", essential: false }, { name: "Health", color: "#00C7BE", essential: true },
  { name: "Bills", color: "#8E8E93", essential: true }, { name: "Subscriptions", color: "#FFCC00", essential: false },
  { name: "Other", color: "#C7C7CC", essential: false },
];
/* Subcategories only apply to the built-in categories; custom ones have none. */
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"],
};
const colorMapOf = (categories) => Object.fromEntries((categories || []).map((c) => [c.name, c.color]));

const DEFAULT_CONFIG = {
  period: "monthly",
  categories: DEFAULT_CATEGORIES.map((c) => ({ ...c })),
  goals: [
    { id: "g1", name: "House down payment", target: 40000, saved: 6500 },
    { id: "g2", name: "Emergency fund", target: 10000, saved: 3200 },
  ],
  primaryGoalId: "g1",
  autoAdjust: true,
  budgets: { Groceries: 450, "Dining Out": 300, Shopping: 200, Electronics: 80, Home: 150, Transport: 650, Entertainment: 100, Health: 80, Bills: 1700, Subscriptions: 60, Other: 100 },
  plan: { basis: "conservative", leanIncome: 0, typicalIncome: 0, savingsPct: 15, bufferMonths: 3, bufferSaved: 0 },
  savingsUnassigned: 0,
  taxSetAside: 0,
  taxLog: [],
  recurring: [],
  accounts: [],
  coachHistory: [],
  profile: { employment: "w2", sideBusiness: false, accounts: [], ownsHome: false, profiled: true },
  incomeSources: {},
  onboarded: false,
  prefs: { theme: "auto", defaultTab: "home", hideScore: false, currency: "$", coachTone: "balanced", displayName: "", gemsRead: [] },
};
const DEFAULT_PLAN = { basis: "conservative", leanIncome: 0, typicalIncome: 0, savingsPct: 15, bufferMonths: 3, bufferSaved: 0, taxRate: 0 };
/* A fresh account starts empty — the interview fills the plan, the user fills history. */
const BLANK_CONFIG = {
  period: "monthly",
  categories: DEFAULT_CATEGORIES.map((c) => ({ ...c })),
  goals: [],
  primaryGoalId: null,
  autoAdjust: true,
  budgets: Object.fromEntries(DEFAULT_CATEGORIES.map((c) => [c.name, 0])),
  plan: { ...DEFAULT_PLAN },
  savingsUnassigned: 0,
  taxSetAside: 0,
  taxLog: [],
  recurring: [],
  accounts: [],
  coachHistory: [],
  profile: { employment: "w2", sideBusiness: false, accounts: [], ownsHome: false, profiled: false, occupation: "", incomeType: "variable", lifeStage: "", priorities: [], painPoints: [], syrupNotes: [], featureRelevance: {}, lastSyrupProfile: "" },
  layout: { summaryOrder: null, hiddenTabs: [], hiddenCards: [] },
  incomeSources: {},
  onboarded: false,
  tourDone: false,
  prefs: { theme: "auto", defaultTab: "home", hideScore: false, currency: "$", coachTone: "balanced", displayName: "", gemsRead: [], hiddenFeatures: [] },
};
/* Migrate older data so existing installs don't lose anything. */
function migrateConfig(c) {
  if (!c) return DEFAULT_CONFIG;
  let next = { ...c };
  if (!Array.isArray(next.categories) || !next.categories.length) next.categories = DEFAULT_CATEGORIES.map((x) => ({ ...x }));
  next.categories = next.categories.map((c) => ({ ...c, essential: typeof c.essential === "boolean" ? c.essential : DEFAULT_ESSENTIALS.has(c.name) }));
  if (!next.budgets) next.budgets = {};
  next.plan = { ...DEFAULT_PLAN, ...(next.plan || {}) };
  if (typeof next.savingsUnassigned !== "number") next.savingsUnassigned = 0;
  if (typeof next.taxSetAside !== "number") next.taxSetAside = 0;
  if (!Array.isArray(next.taxLog)) next.taxLog = [];
  if (next.plan && typeof next.plan.taxRate !== "number") next.plan.taxRate = 0;
  if (!Array.isArray(next.recurring)) next.recurring = [];
  if (!Array.isArray(next.accounts)) next.accounts = [];
  if (!Array.isArray(next.coachHistory)) next.coachHistory = [];
  // Collapse accounts that share a last-4 (same card named slightly differently across imports).
  if (next.accounts.length > 1) {
    const l4 = (s) => { const m = (s || "").match(/(\d{4})\s*$/); return m ? m[1] : ""; };
    const byL4 = {}; const kept = [];
    next.accounts.forEach((a) => { const k = l4(a.name); if (k && byL4[k]) { return; } if (k) byL4[k] = a.name; kept.push(a); });
    next.accounts = kept;
  }
  next.profile = { employment: "w2", sideBusiness: false, accounts: [], ownsHome: false, profiled: true, occupation: "", incomeType: "variable", lifeStage: "", priorities: [], painPoints: [], syrupNotes: [], featureRelevance: {}, lastSyrupProfile: "", ...(next.profile || {}) };
  if (!Array.isArray(next.profile.accounts)) next.profile.accounts = [];
  if (!Array.isArray(next.profile.priorities)) next.profile.priorities = [];
  if (!Array.isArray(next.profile.painPoints)) next.profile.painPoints = [];
  if (!Array.isArray(next.profile.syrupNotes)) next.profile.syrupNotes = [];
  if (typeof next.profile.featureRelevance !== "object" || !next.profile.featureRelevance) next.profile.featureRelevance = {};
  if (!next.layout) next.layout = { summaryOrder: null, hiddenTabs: [], hiddenCards: [] };
  if (!Array.isArray(next.layout.hiddenTabs)) next.layout.hiddenTabs = [];
  if (!Array.isArray(next.layout.hiddenCards)) next.layout.hiddenCards = [];
  if (!next.incomeSources || typeof next.incomeSources !== "object") next.incomeSources = {};
  if (!Array.isArray(next.goals)) {
    const g = next.goal || DEFAULT_CONFIG.goals[0];
    next.goals = [{ id: "g1", name: g.name, target: g.target, saved: g.saved }];
    next.primaryGoalId = "g1";
  }
  if (!next.primaryGoalId || !next.goals.some((g) => g.id === next.primaryGoalId)) next.primaryGoalId = next.goals[0] ? next.goals[0].id : null;
  if (typeof next.autoAdjust !== "boolean") next.autoAdjust = true;
  if (typeof next.onboarded !== "boolean") next.onboarded = true;
  if (typeof next.tourDone !== "boolean") next.tourDone = true;
  next.prefs = { theme: "auto", defaultTab: "home", hideScore: false, currency: "$", coachTone: "balanced", displayName: "", gemsRead: [], hiddenFeatures: [], ...(next.prefs || {}) };
  if (!Array.isArray(next.prefs.gemsRead)) next.prefs.gemsRead = [];
  if (!Array.isArray(next.prefs.hiddenFeatures)) next.prefs.hiddenFeatures = [];
  delete next.goal;
  return next;
}

/* ================================================================== */
/*  FEATURE VISIBILITY — drives adaptive UI based on profile           */
/* ================================================================== */
function useFeatures(config, metrics) {
  const p = config.profile || {};
  const r = p.featureRelevance || {};
  const hidden = (config.prefs && config.prefs.hiddenFeatures) || [];
  const isHidden = (f) => hidden.includes(f);
  const hasTaxable = p.employment !== "w2" || p.sideBusiness;
  const hasGoals = (config.goals || []).length > 0;
  const hasIncome = metrics && metrics.projIncome > 0;
  const hasTx = metrics && metrics.totalSpent > 0;
  const hasRecurring = (config.recurring || []).length > 0;

  return {
    // Card visibility
    showTaxReserve: !isHidden("tax_reserve") && (r.taxReserve > 40 || hasTaxable),
    showBudgetScore: !isHidden("budget_score") && (r.budgetScore !== 0),
    showIncomeSwing: !isHidden("income_swing") && (r.incomeProjection > 40 || p.incomeType === "variable" || p.incomeType === "seasonal"),
    showRecurring: !isHidden("recurring") && (r.recurring > 30 || hasRecurring),
    showNetWorth: !isHidden("net_worth") && (r.netWorth > 60 || (p.accounts || []).length > 2),
    showPayday: hasIncome && metrics && metrics.nextPayday,
    showProjectedIncome: hasIncome,
    // Summary card order priority (higher = shown first)
    summaryPriority: computeSummaryPriority(p, metrics, config),
    // Tab visibility
    showPlanTab: hasGoals || (config.plan && config.plan.leanIncome > 0),
    showWorthTab: !isHidden("net_worth") && (r.netWorth > 60),
    // Profile completeness
    profileComplete: !!(p.occupation && p.incomeType && (p.priorities || []).length > 0),
    // Is this a new user Syrup hasn't learned about yet?
    syrupKnowsUser: !!(p.occupation || (p.painPoints || []).length > 0 || (p.priorities || []).length > 0),
  };
}

function computeSummaryPriority(profile, metrics, config) {
  const p = profile || {};
  const r = p.featureRelevance || {};
  const order = [];
  // Always show: primary goal, budgets, recent
  order.push({ id: "primary_goal", priority: 100 });
  order.push({ id: "other_goals", priority: 90 });
  // Coach card — higher priority for new/less-engaged users
  const syrupKnows = !!(p.occupation || (p.painPoints || []).length > 0);
  order.push({ id: "coach", priority: syrupKnows ? 60 : 95 });
  // Payday — show if there's income data
  order.push({ id: "payday", priority: metrics && metrics.nextPayday ? 75 : 0 });
  // Projected income — always relevant for variable income
  order.push({ id: "projected_income", priority: p.incomeType === "variable" || p.incomeType === "seasonal" ? 80 : 55 });
  // Spending this month
  order.push({ id: "spent_month", priority: 70 });
  // Budgets
  order.push({ id: "budgets", priority: 65 });
  // Recent
  order.push({ id: "recent", priority: 50 });
  // Sort by priority descending
  return order.sort((a, b) => b.priority - a.priority).map((x) => x.id);
}

const primaryGoal = (config) => (config.goals || []).find((g) => g.id === config.primaryGoalId) || (config.goals || [])[0] || null;

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: "Paycheck", amount: 1850, date: daysAgo(3) },
  { id: "i2", source: "Overtime", amount: 430, date: daysAgo(9) },
  { id: "i3", source: "Paycheck", amount: 1850, date: daysAgo(17) },
  { id: "i4", source: "Side gig", amount: 600, date: daysAgo(20) },
  { id: "i5", source: "Paycheck", amount: 1850, date: daysAgo(31) },
  { id: "i6", source: "Overtime", amount: 680, date: daysAgo(38) },
  { id: "i7", source: "Paycheck", amount: 1850, date: daysAgo(45) },
  { id: "i8", source: "Freelance", amount: 1100, date: daysAgo(50) },
  { id: "i9", source: "Paycheck", amount: 1850, date: daysAgo(59) },
  { id: "i10", source: "Overtime", amount: 250, date: daysAgo(66) },
  { id: "i11", source: "Paycheck", amount: 1850, date: daysAgo(73) },
  { id: "i12", source: "Paycheck", amount: 1850, date: daysAgo(87) },
];

/* ---- helpers ---- */
let CUR = "$"; // set from prefs at render time
const fmt = (n) => CUR + Math.round(n).toLocaleString("en-US");
/* Clean a numeric text input: digits + one dot, no leading zeros (so "0100" -> "100"). */
const cleanNumStr = (v) => {
  let s = String(v).replace(/[^0-9.]/g, "");
  const firstDot = s.indexOf(".");
  if (firstDot !== -1) s = s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, "");
  if (!s.startsWith(".")) s = s.replace(/^0+(?=\d)/, ""); // strip leading zeros unless it's "0.x"
  return s;
};
const fmtK = (n) => (n >= 1000 ? CUR + (n / 1000).toFixed(n >= 10000 ? 0 : 1) + "k" : CUR + 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";
}

/* ---- tiny markdown: render **bold** as real bold (and *italic*) ---- */
function RichText({ text }) {
  const parts = String(text == null ? "" : text).split(/(\*\*[^*]+\*\*|\*[^*\n]+\*)/g);
  return parts.map((p, i) => {
    if (/^\*\*[^*]+\*\*$/.test(p)) return <strong key={i} style={{ fontWeight: 700 }}>{p.slice(2, -2)}</strong>;
    if (/^\*[^*\n]+\*$/.test(p)) return <em key={i}>{p.slice(1, -1)}</em>;
    return <React.Fragment key={i}>{p}</React.Fragment>;
  });
}

/* ---- AI (preview) ---- */
async function callClaude(content, maxTokens) {
  const res = await fetch("/api/claude", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: MODEL, max_tokens: maxTokens || 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();
}
async function callClaudeMessages(messages, system, maxTokens) {
  const res = await fetch("/api/claude", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: MODEL, max_tokens: maxTokens || 1000, system, messages }) });
  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());
/* Parse a statement response that may be truncated (model hit the token limit
   mid-JSON). First try normal parse; if that fails, salvage every complete
   transaction object from the "tx" array and recover the account if present. */
function parseStatementJSON(t) {
  const clean = t.replace(/```json|```/g, "").trim();
  try { return parseJSON(clean); } catch (e) { /* fall through to salvage */ }
  const result = { account: null, tx: [] };
  const acct = clean.match(/"account"\s*:\s*(\{[^}]*\})/);
  if (acct) { try { result.account = JSON.parse(acct[1]); } catch (_) {} }
  // Grab each complete {...} object inside the tx array.
  const txStart = clean.indexOf('"tx"');
  if (txStart >= 0) {
    const slice = clean.slice(txStart);
    const objs = slice.match(/\{[^{}]*\}/g) || [];
    objs.forEach((o) => { try { const obj = JSON.parse(o); if (obj && (obj.amount != null || obj.who || obj.date)) result.tx.push(obj); } catch (_) {} });
  }
  return result;
}
async function aiCategorize(who, amount, names) { return parseJSON(await callClaude(`Finance categorizer. Categories: ${names.join(", ")}. Merchant "${who}", $${amount}. Return ONLY JSON {"category":"","sub":"","confidence":0}`)); }
async function aiReadReceipt(b64, mt, names) {
  return parseJSON(await callClaude([
    { type: "image", source: { type: "base64", media_type: mt, data: b64 } },
    { type: "text", text: `Read this store receipt. Available spending categories: ${names.join(", ")}. Return ONLY JSON in this exact shape:
{"who":"store name","date":"YYYY-MM-DD","tax":0,"total":0,"items":[{"name":"item name","price":0,"category":"one of the categories above"}]}
List each purchased line item separately with its pre-tax price (max 12) and assign the single best category to EACH item. "tax" = the total sales tax on the receipt (0 if none). "total" = the final amount charged including tax. Do NOT include tax, subtotal, or total as items — only real purchased products. If a price is unclear, use your best estimate.` },
  ]));
}
/* Read a screenshot of a bank/credit-card transaction list. Also detects the
   account it came from so multi-account history stays separated. */
async function aiReadStatement(b64, mt, names) {
  return parseStatementJSON(await callClaude([
    { type: "image", source: { type: "base64", media_type: mt, data: b64 } },
    { type: "text", text: statementPrompt(names) },
  ], 4000));
}
/* Read statement TEXT (from a CSV export or extracted PDF text). Same shape. */
async function aiReadStatementText(text, names) {
  return parseStatementJSON(await callClaudeMessages([
    { role: "user", content: `${statementPrompt(names)}\n\nHere is the statement content:\n\n${text.slice(0, 60000)}` },
  ], undefined, 4000));
}
function statementPrompt(names) {
  const today = new Date().toISOString().slice(0, 10);
  return `This is a bank or credit-card statement (transaction list). Today's date is ${today}. Available spending categories: ${names.join(", ")}. Return ONLY JSON in this exact shape:
{"account":{"name":"short account name like 'Chase Checking' or 'Amex Gold' if shown, else ''","last4":"last 4 digits if shown else ''","kind":"checking|savings|credit|other"},"tx":[{"date":"YYYY-MM-DD","who":"merchant or description","amount":0,"kind":"expense","category":"one of the categories above"}]}
For "account": infer the bank/card name and type from any header, logo text, or account label; leave fields "" if not visible. For each transaction (max 60): "amount" is the absolute dollar value (always positive). "kind" is "expense" for purchases/debits/withdrawals or "income" for deposits/credits/refunds/payments received — use the sign, +/-, or column to decide. For a credit-card statement, payments TO the card are "income" (they reduce the balance) only if clearly a payment; normal charges are "expense". For expenses assign the single best "category"; for income set "category" to "". DATES: read each transaction's date carefully and output YYYY-MM-DD. Many statements show dates as MM/DD or "May 7" without a year — in that case use the year that makes the date most recent but NOT in the future relative to today (${today}). Never guess a year more than 13 months before today. Skip pending $0 authorizations, running balances, interest-summary lines, and section headers — only real transactions.`;
}
/* Pull text out of a CSV or PDF file in the browser. Returns "" if it can't. */
async function extractFileText(file) {
  const name = (file.name || "").toLowerCase();
  const isCSV = name.endsWith(".csv") || file.type === "text/csv" || name.endsWith(".txt");
  if (isCSV) { try { return await file.text(); } catch (e) { return ""; } }
  const isPDF = name.endsWith(".pdf") || file.type === "application/pdf";
  if (isPDF && window.pdfjsLib) {
    try {
      const buf = await file.arrayBuffer();
      const pdf = await window.pdfjsLib.getDocument({ data: buf }).promise;
      let out = "";
      const pages = Math.min(pdf.numPages, 12); // cap to keep it sane
      for (let p = 1; p <= pages; p++) {
        const page = await pdf.getPage(p);
        const content = await page.getTextContent();
        out += content.items.map((it) => it.str).join(" ") + "\n";
      }
      return out;
    } catch (e) { return ""; }
  }
  return "";
}
async function fileToB64(file) {
  return new Promise((res, rej) => { const r = new FileReader(); r.onload = () => res(r.result.split(",")[1]); r.onerror = rej; r.readAsDataURL(file); });
}
/* Render a PDF's pages to base64 PNG images, for OCR fallback when a PDF has no
   extractable text (scanned/image-only statements). Returns [] if it can't. */
async function pdfPagesToImages(file, maxPages) {
  if (!window.pdfjsLib) return [];
  try {
    const buf = await file.arrayBuffer();
    const pdf = await window.pdfjsLib.getDocument({ data: buf }).promise;
    const out = [];
    const pages = Math.min(pdf.numPages, maxPages || 6);
    for (let p = 1; p <= pages; p++) {
      const page = await pdf.getPage(p);
      const viewport = page.getViewport({ scale: 2 }); // 2x for legible OCR
      const canvas = document.createElement("canvas");
      canvas.width = viewport.width; canvas.height = viewport.height;
      const ctx = canvas.getContext("2d");
      await page.render({ canvasContext: ctx, viewport }).promise;
      const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
      out.push(dataUrl.split(",")[1]);
    }
    return out;
  } catch (e) { return []; }
}
/* Reconciliation: Syrup analyzes a structured summary of recent spending and returns
   findings (unusual charges, likely subscriptions, month-over-month creep, possible
   duplicates) plus a couple of questions to understand the picture. */
async function aiReconcile(summary, tone) {
  const sys = `You are Syrup, Waffle's money coach (tone: ${tone || "balanced"}). You're reviewing a user's recent transactions to help them understand their spending. Be concrete, kind, and non-judgmental — never shame. Return ONLY JSON in this exact shape:
{"summary":"one warm sentence on the overall picture","findings":[{"type":"large|subscription|creep|duplicate|habit","title":"short label","detail":"one or two plain-language sentences","amount":0}],"questions":["a short question that helps you understand a flagged item or habit"]}
Find up to 6 findings across these types: "large" = unusually big or out-of-pattern charges; "subscription" = recurring charges that look like subscriptions/memberships they might forget; "creep" = a category trending up vs prior months; "duplicate" = two charges that might be accidental repeats; "habit" = a spending pattern worth gently naming (e.g. frequent food delivery). Set "amount" to the relevant dollar figure (0 if not applicable). Ask 1-3 short "questions" that would help you advise better. Keep everything brief and specific to the data. Do not invent transactions not in the data.`;
  return parseJSON(await callClaudeMessages([{ role: "user", content: `Here is the spending summary:\n\n${summary}` }], sys, 2000));
}
/* Build a compact text summary of recent transactions for reconciliation. */
function buildReconcileSummary(tx, income, config) {
  const cur = (config.prefs && config.prefs.currency) || "$";
  const byMonth = {};
  tx.forEach((x) => { const k = monthKey(x.date); (byMonth[k] = byMonth[k] || { total: 0, byCat: {}, items: [] }); byMonth[k].total += x.amount; byMonth[k].byCat[x.category] = (byMonth[k].byCat[x.category] || 0) + x.amount; byMonth[k].items.push(x); });
  const months = Object.keys(byMonth).sort().slice(-3); // last 3 months with data
  let out = "";
  months.forEach((k) => {
    const m = byMonth[k];
    out += `\n${k}: spent ${cur}${Math.round(m.total)}. By category: ` + Object.entries(m.byCat).sort((a, b) => b[1] - a[1]).map(([c, v]) => `${c} ${cur}${Math.round(v)}`).join(", ") + ".";
    const top = [...m.items].sort((a, b) => b.amount - a.amount).slice(0, 20);
    out += "\n  transactions: " + top.map((x) => `${x.date} ${x.who} ${cur}${x.amount}${x.category ? ` [${x.category}]` : ""}`).join("; ") + ".";
  });
  const inc = income.slice(0, 12).map((x) => `${x.date} ${x.source} ${cur}${x.amount}`).join("; ");
  if (inc) out += `\nRecent income: ${inc}.`;
  return out.trim();
}
/* Spread tax across items by their share of the pre-tax subtotal, with
   largest-remainder rounding so the cents sum exactly to subtotal + tax. */
function applyTaxToItems(items, tax) {
  const t = Math.max(0, Math.round((Number(tax) || 0) * 100)); // tax in cents
  if (!items.length || t === 0) return items.map((it) => ({ ...it, price: Math.round((Number(it.price) || 0) * 100) / 100 }));
  const cents = items.map((it) => Math.max(0, Math.round((Number(it.price) || 0) * 100)));
  const sub = cents.reduce((a, b) => a + b, 0);
  if (sub === 0) return items.map((it) => ({ ...it, price: Math.round((Number(it.price) || 0) * 100) / 100 }));
  const raw = cents.map((c) => (c / sub) * t);
  const floor = raw.map((r) => Math.floor(r));
  let left = t - floor.reduce((a, b) => a + b, 0);
  const order = raw.map((r, i) => ({ i, frac: r - Math.floor(r) })).sort((a, b) => b.frac - a.frac);
  const add = new Array(cents.length).fill(0);
  for (let k = 0; k < left; k++) add[order[k % order.length].i] += 1;
  return items.map((it, i) => ({ ...it, price: (cents[i] + floor[i] + add[i]) / 100 }));
}

/* ---- Coach: persona + live data + (optional) tappable action grammar ---- */
function coachSystem(ctx) {
  const actions = ctx.autoAdjust
    ? `You MAY end a reply with one or more actions the user applies with a single tap — only when you've landed on something concrete. Put each on its own line in exactly this form (the user never sees the raw block; the app turns it into a button):
[[ACTION]]{"type":"budget","changes":[{"category":"Dining Out","to":250}],"summary":"Trim Dining Out to $250/mo"}[[/ACTION]]
or
[[ACTION]]{"type":"add_goal","goal":{"name":"Trip to Jamaica","target":3000,"saved":0},"summary":"Add 'Trip to Jamaica' — $3,000 goal"}[[/ACTION]]
or, to move money into a goal's saved total:
[[ACTION]]{"type":"contribute","contribute":{"goal":"Home downpayment","amount":200},"summary":"Add $200 to Home downpayment"}[[/ACTION]]
or, to log a transaction the user tells you about:
[[ACTION]]{"type":"log_expense","expense":{"who":"Costco","amount":120,"category":"Groceries","date":"2026-06-02"},"summary":"Log $120 at Costco (Groceries)"}[[/ACTION]]
or
[[ACTION]]{"type":"log_income","income":{"source":"Freelance invoice","amount":800,"date":"2026-06-02"},"summary":"Log $800 income (Freelance invoice)"}[[/ACTION]]
or, to create NEW budget categories (you can include several at once):
[[ACTION]]{"type":"add_category","categories":[{"name":"Pets","budget":80,"essential":false},{"name":"Childcare","budget":600,"essential":true}],"summary":"Add Pets ($80) and Childcare ($600) categories"}[[/ACTION]]
or, to show/hide a feature section:
[[ACTION]]{"type":"toggle_feature","feature":"tax_reserve|budget_score|income_swing|recurring|net_worth","visible":true,"summary":"Show the tax reserve section"}[[/ACTION]]
or, to update the user's profile:
[[ACTION]]{"type":"update_profile","changes":{"employment":"1099","sideBusiness":true,"occupation":"firefighter"},"summary":"Update profile to 1099 firefighter with side business"}[[/ACTION]]
or, to set multiple budgets at once:
[[ACTION]]{"type":"set_budget_batch","budgets":{"Groceries":400,"Dining Out":200},"summary":"Set Groceries to $400 and Dining Out to $200"}[[/ACTION]]
or, to update plan settings:
[[ACTION]]{"type":"update_plan","changes":{"savingsPct":20,"bufferMonths":6,"taxRate":25},"summary":"Bump savings to 20% and buffer to 6 months"}[[/ACTION]]
or, to navigate the user to a screen (closes this chat):
[[ACTION]]{"type":"navigate","to":"plan|insights|setup|income|learn|profile","summary":"Go to the Plan tab"}[[/ACTION]]
or, to create a recurring transaction:
[[ACTION]]{"type":"set_recurring","recurring":{"kind":"expense|income","who":"Rent","amount":1450,"cadence":"weekly|biweekly|monthly","category":"Bills"},"summary":"Add Rent as a $1,450/mo recurring expense"}[[/ACTION]]
or, to delete a category:
[[ACTION]]{"type":"remove_category","name":"Electronics","summary":"Remove the Electronics category"}[[/ACTION]]
or, to rename a category:
[[ACTION]]{"type":"rename_category","from":"Other","to":"Pets","summary":"Rename Other → Pets"}[[/ACTION]]
or, to change which goal is primary:
[[ACTION]]{"type":"reorder_goals","primary":"g2","summary":"Make Emergency fund the primary goal"}[[/ACTION]]
or, to toggle a category between essential and flex:
[[ACTION]]{"type":"set_essential","category":"Subscriptions","essential":true,"summary":"Mark Subscriptions as essential"}[[/ACTION]]

SILENT action (no button shown — fires automatically, the user never sees it):
[[ACTION]]{"type":"learn_about_user","updates":{"occupation":"firefighter","incomeType":"variable","painPoints":["taxes"]},"summary":"Updated profile"}[[/ACTION]]
learn_about_user is INVISIBLE to the user. Do NOT show it as a button. The app applies it silently. Use it when you genuinely learn something new about the user's job, life situation, income type, or financial priorities — or infer these from conversation. Include relevant fields like occupation, incomeType (stable/variable/seasonal), painPoints (array of strings like "taxes", "irregular-income", "overspending"), priorities (array like "debt-free", "home", "travel"). Only emit when you learn something new.

The navigate action closes this chat and takes the user to the specified screen. Use it when they ask "show me my plan" or "take me to budgets" etc.

The "budget" action ONLY adjusts categories that already exist (listed below) — never use it to invent a new one. If the user wants a category that isn't in the list, you MUST use add_category, not budget. Existing categories: ${ctx.categoryNames.join(", ")}. Use log_expense/log_income only when the user clearly states a real transaction to record (amount + what it was). Use contribute when they say they put money toward a goal. Never invent amounts — if a number is missing, ask first. If you're still gathering info or not proposing a concrete change, include no action block.`
    : `Auto-adjust is OFF, so do NOT include any action blocks — talk it through, and if relevant mention they can switch on Auto-adjust in Budgets to apply changes with one tap.`;
  const tone = ctx.tone === "gentle"
    ? "Tone: gentle and reassuring. Lead with encouragement, soften critiques, never make them feel bad about overspending."
    : ctx.tone === "blunt"
    ? "Tone: blunt and direct. Skip the cushioning, call overspending what it is, be a no-nonsense straight shooter (still respectful)."
    : "Tone: balanced — warm but honest.";
  return `You are Syrup, Waffle's money coach: sharp and concise. (Your name is Syrup; if the user greets you or asks who you are, you can say so warmly.) ${tone} The user has variable, irregular income (it changes month to month). Speak directly to "you", keep replies short — usually 2–4 sentences, under 80 words unless they ask for detail — and ground everything in their real numbers below. Use **bold** for the single most important number or move.

When the user says they want to save for something new — a trip, a purchase, a milestone (e.g. "I want to take a trip to Jamaica") — do NOT create a goal right away. First ask the 1–2 things you need: roughly how much it'll cost (offer a ballpark if they're unsure), when they want it, and whether it should come before or after their current primary goal. Once you have a cost and rough timeline, THEN propose it with an add_goal action and one line on how it fits around their existing goals at their current saving pace. Ask only one or two questions per reply — guide, don't interrogate.

When the user tells you about their job, life situation, income type, or financial priorities — or when you infer these from conversation — emit a learn_about_user action. This is silent (no user button). Over time this shapes which features the app shows them. Include relevant fields like occupation, incomeType (stable/variable/seasonal), painPoints (array of strings like "taxes", "irregular-income", "overspending"), priorities (array like "debt-free", "home", "travel"). Only emit this when you genuinely learn something new.

Beyond budgeting, you know a set of financial principles ("gems"). When a conversation naturally touches one of these contexts, you MAY weave in the relevant principle as a brief, optional aside — framed as general education, never as personalized advice, and never more than one per reply. Don't force them in; only mention when genuinely relevant. These are GENERAL knowledge, not a recommendation that they specifically do it, and you are not a licensed advisor. The principles and when they fit:
${gemsForProfile(ctx.data && ctx.data.profile).map((g) => `- ${g.title} (when: ${g.when})`).join("\n")}

Their situation right now (JSON):
${JSON.stringify(ctx.data)}

You also know Waffle inside out and can explain any part of it in plain, everyday language — no jargon, no lectures. If someone asks "what does this do", "how do I…", "where's X", or seems lost, explain simply and point them to where it lives. What Waffle does and where to find it:
- Import statements: the + button (top right) → "Import from your bank". Upload screenshots, CSV, or PDF of bank/card statements — no bank login, nothing connected. It reads and sorts the transactions; you review before saving.
- Upload documents: tap the attach button (📎) right here in this chat to upload bank statements, receipts, pay stubs, or any financial document. I'll read it, categorize everything, and you confirm before it's saved.
- Scan a receipt: the + button → "Scan a receipt" — snaps a photo and splits it into items by category.
- Reconcile: the Insights tab → "Reconcile with Syrup" — you (Syrup) review their spending for forgotten subscriptions, big one-offs, and creep.
- Talk to you (Syrup): the sparkle icon anywhere — they can log expenses/income, add budget categories, adjust budgets, or add to a goal just by asking.
- Plan tab: their buffer (cash for lean months), runway (how long reserves cover essentials), income swing, and tax set-aside for 1099 income.
- Budgets tab: set monthly limits per category and create savings goals; toggle "Auto-adjust" to let you propose one-tap changes.
- Insights tab: spending donut, month-by-month history, and per-account filtering.
- Activity tab: all transactions; swipe a row left to Edit or Delete, or tap Select for bulk delete.
Explain features only when asked or clearly relevant — don't volunteer a tour unprompted. Keep explanations to a sentence or two in layperson terms.

${actions}`;
}
function extractAction(text) {
  const re = /\[\[ACTION\]\]([\s\S]*?)\[\[\/ACTION\]\]/g;
  const actions = [];
  let m;
  while ((m = re.exec(text)) !== null) {
    try { const a = JSON.parse(m[1].trim()); if (a && a.type) actions.push(a); } catch (e) { /* skip malformed */ }
  }
  // Strip every action block (parsed or not) from the visible prose, tidy whitespace.
  const prose = text.replace(/\[\[ACTION\]\][\s\S]*?\[\[\/ACTION\]\]/g, "").replace(/\n{3,}/g, "\n\n").trim();
  return { prose, action: actions[0] || null, actions };
}

/* ---- 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 IPlan = (p) => <Svg {...p}><circle cx="12" cy="12" r="8.2" /><path d="M12 12V3.8" /><path d="M12 12l7.1 4.1" /></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>;
const IStar = (p) => <Svg {...p} fill={p.fillIn ? (p.color || "currentColor") : "none"}><path d="M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 17l-5.2 2.7 1-5.8-4.3-4.1 5.9-.9z" /></Svg>;
const ISend = (p) => <Svg {...p}><path d="M4 12l16-7-7 16-2.5-6.5z" /></Svg>;
const ITrash = (p) => <Svg {...p}><path d="M4 7h16" /><path d="M9 7V5h6v2" /><path d="M6 7l1 13h10l1-13" /></Svg>;
const IUser = (p) => <Svg {...p}><circle cx="12" cy="8" r="4" /><path d="M5 20c0-3.9 3.1-7 7-7s7 3.1 7 7" /></Svg>;
const IWorth = (p) => <Svg {...p}><path d="M12 3l8 6-3 9H7L4 9z" /><path d="M9 12h6" /></Svg>;
const IBook = (p) => <Svg {...p}><path d="M5 5a2 2 0 0 1 2-2h12v15H7a2 2 0 0 0-2 2z" /><path d="M5 19a2 2 0 0 0 2 2h12" /></Svg>;
const ILock = (p) => <Svg {...p}><rect x="5" y="11" width="14" height="9" rx="2" /><path d="M8 11V8a4 4 0 0 1 8 0v3" /></Svg>;
const ICamera = (p) => <Svg {...p}><path d="M4 8a2 2 0 0 1 2-2h2l1.2-1.6a1 1 0 0 1 .8-.4h4a1 1 0 0 1 .8.4L18 6h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z" /><circle cx="12" cy="12.5" r="3.2" /></Svg>;
const ISun = (p) => <Svg {...p}><circle cx="12" cy="12" r="4.5" /><path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19" /></Svg>;
const IMoon = (p) => <Svg {...p}><path d="M20 14.5A8 8 0 1 1 9.5 4a6.3 6.3 0 0 0 10.5 10.5z" /></Svg>;
const IReplay = (p) => <Svg {...p}><path d="M4 12a8 8 0 1 0 2.3-5.6" /><path d="M4 4v4h4" /></Svg>;
const IAttach = (p) => <Svg {...p}><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.19 9.19a2 2 0 0 1-2.83-2.83l8.49-8.49" /></Svg>;

/* ================================================================== */
/*  APP                                                                */
/* ================================================================== */
function App({ session, onSignOut }) {
  const userId = session.user.id;
  const storeKey = "waffle:" + userId;
  const [config, setConfig] = useState(DEFAULT_CONFIG);
  const [themeTick, setThemeTick] = useState(0);
  // Apply theme + currency from prefs on every render (cheap; keeps T/CUR in sync)
  const prefs = config.prefs || DEFAULT_CONFIG.prefs;
  applyTheme(prefs.theme || "auto");
  CUR = prefs.currency || "$";
  // Re-apply when the OS scheme changes while in auto mode
  useEffect(() => {
    if (!window.matchMedia) return;
    const mq = window.matchMedia("(prefers-color-scheme: dark)");
    const h = () => { if ((config.prefs && config.prefs.theme) === "auto" || !(config.prefs && config.prefs.theme)) { applyTheme("auto"); setThemeTick((t) => t + 1); } };
    mq.addEventListener ? mq.addEventListener("change", h) : mq.addListener(h);
    return () => { mq.removeEventListener ? mq.removeEventListener("change", h) : mq.removeListener(h); };
  }, [config.prefs && config.prefs.theme]);
  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);
  const [onboard, setOnboard] = useState(false);
  const [profileSetup, setProfileSetup] = useState(false); // standalone profile editor (from Profile screen)
  const [coachOpen, setCoachOpen] = useState(false);
  const [startTour, setStartTour] = useState(false); // fires the tour in-session, independent of persisted flag
  const [coachSeed, setCoachSeed] = useState(""); // optional pre-filled prompt when opening the coach
  const [contrib, setContrib] = useState(null); // { defaultAmount } when open
  const [assign, setAssign] = useState(false); // assign unassigned savings to a goal
  const [editing, setEditing] = useState(null); // { kind, rec } when editing an entry
  const [syncState, setSyncState] = useState("idle"); // idle | syncing | error
  const saveTimer = useRef(null);

  useEffect(() => {
    let alive = true;
    (async () => {
      // 1) Local cache for THIS user (instant paint, offline-friendly)
      let local = null;
      try { const v = localStorage.getItem(storeKey); if (v) local = JSON.parse(v); } catch (_) {}

      // 2) Cloud row for this user
      let cloud = null, cloudReached = true;
      try { cloud = await cloudLoad(userId); } catch (_) { cloudReached = false; if (alive) setSyncState("error"); }

      // 2b) One-time legacy adoption — ONLY for the first-ever account, and only if
      // this account has neither a cloud row nor its own cache. The "claimed" flag
      // ensures a second new account never re-inherits the old single-device data.
      let legacy = null;
      const legacyClaimed = (() => { try { return localStorage.getItem("waffle:legacyClaimed") === "1"; } catch (_) { return true; } })();
      if (!local && !(cloud && cloud.data) && cloudReached && !legacyClaimed) {
        try { const lv = localStorage.getItem("waffle"); if (lv) legacy = JSON.parse(lv); } catch (_) {}
      }

      // 3) Reconcile: prefer cloud, then this user's own cache, then one-time legacy, else BLANK.
      let chosen = null, pushToCloud = false;
      const localStamp = local && local._savedAt ? local._savedAt : 0;
      const cloudStamp = cloud && cloud.updated_at ? new Date(cloud.updated_at).getTime() : 0;
      if (cloud && cloud.data && (!local || cloudStamp >= localStamp)) chosen = cloud.data;
      else if (local) { chosen = local; pushToCloud = !!sb; }
      else if (legacy && legacy.config) { chosen = legacy; pushToCloud = !!sb; }
      if (!chosen) { chosen = { config: BLANK_CONFIG, tx: [], income: [] }; pushToCloud = !!sb; }
      // Mark legacy as claimed the first time any account loads, so it's adopted at most once.
      try { localStorage.setItem("waffle:legacyClaimed", "1"); } catch (_) {}

      let mc = migrateConfig(chosen.config); let t = chosen.tx || []; let i = chosen.income || [];
      const mat = materializeRecurring(mc, t, i);
      if (mat) { mc = mat.config; t = mat.tx; i = mat.income; pushToCloud = !!sb; }
      if (!alive) return;
      setConfig(mc); setTx(t); setIncome(i);
      if (!mc.onboarded) setOnboard(true);
      else if (mc.prefs && mc.prefs.defaultTab && mc.prefs.defaultTab !== "home") setView(mc.prefs.defaultTab);
      writeLocal({ config: mc, tx: t, income: i });
      if (pushToCloud) { try { setSyncState("syncing"); await cloudSave(userId, { config: mc, tx: t, income: i }); setSyncState("idle"); } catch (_) { setSyncState("error"); } }
      setLoading(false);
    })();
    return () => { alive = false; };
  }, [userId]);

  const writeLocal = (next) => { try { localStorage.setItem(storeKey, JSON.stringify({ ...next, _savedAt: Date.now() })); } catch (_) {} };
  const persist = (next) => {
    writeLocal(next);
    if (!sb) return;
    if (saveTimer.current) clearTimeout(saveTimer.current);
    setSyncState("syncing");
    saveTimer.current = setTimeout(async () => {
      try { await cloudSave(userId, { config: next.config, tx: next.tx, income: next.income }); setSyncState("idle"); }
      catch (_) { setSyncState("error"); }
    }, 800); // debounce bursts of edits into one upload
  };
  const saveTx = (t) => { setTx(t); persist({ config, tx: t, income }); };
  const saveIncome = (i) => { setIncome(i); persist({ config, tx, income: i }); };
  const deleteMany = (ids) => {
    const set = new Set(ids);
    const keptTx = tx.filter((x) => !set.has(x.id));
    const keptInc = income.filter((x) => !set.has(x.id));
    setTx(keptTx); setIncome(keptInc); persist({ config, tx: keptTx, income: keptInc });
  };
  const saveConfig = (c) => { setConfig(c); persist({ config: c, tx, income }); };

  // Find duplicate transactions/income. A duplicate = same date + amount + merchant.
  // We deliberately ignore the account (the same charge can't be two real charges) and
  // normalize the merchant (strip trailing store numbers like "#550" or " 5289").
  const normName = (s) => String(s || "").toLowerCase().replace(/[#0-9]+\s*$/, "").replace(/\s+/g, " ").trim();
  const dupKeyTx = (x) => `${x.date}|${Math.round(x.amount * 100)}|${normName(x.who)}`;
  const dupKeyInc = (x) => `${x.date}|${Math.round(x.amount * 100)}|${normName(x.source)}`;
  const findDuplicates = () => {
    const seen = new Set(); let txDupes = 0; const seenI = new Set(); let incDupes = 0;
    tx.forEach((x) => { const k = dupKeyTx(x); if (seen.has(k)) txDupes++; else seen.add(k); });
    income.forEach((x) => { const k = dupKeyInc(x); if (seenI.has(k)) incDupes++; else seenI.add(k); });
    return { txDupes, incDupes, total: txDupes + incDupes };
  };
  const removeDuplicates = () => {
    const seen = new Set(); const keptTx = tx.filter((x) => { const k = dupKeyTx(x); if (seen.has(k)) return false; seen.add(k); return true; });
    const seenI = new Set(); const keptInc = income.filter((x) => { const k = dupKeyInc(x); if (seenI.has(k)) return false; seenI.add(k); return true; });
    setTx(keptTx); setIncome(keptInc); persist({ config, tx: keptTx, income: keptInc });
    return { removedTx: tx.length - keptTx.length, removedInc: income.length - keptInc.length };
  };

  const exportData = () => {
    try {
      const payload = { app: "waffle", version: 1, exportedAt: new Date().toISOString(), data: { config, tx, income } };
      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 = `waffle-backup-${new Date().toISOString().slice(0, 10)}.json`;
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    } catch (_) {}
  };
  const importData = (data) => {
    const c = migrateConfig(data.config);
    const t = Array.isArray(data.tx) ? data.tx : [];
    const i = Array.isArray(data.income) ? data.income : [];
    setConfig(c); setTx(t); setIncome(i);
    persist({ config: c, tx: t, income: i });
    setView("home"); // remount views so they pick up the restored data
    setConfirm({ kind: "imported", count: t.length + i.length });
  };
  const deleteAccount = async () => {
    // Get a fresh access token for the server to verify identity.
    let token = session.access_token;
    try { const { data } = await sb.auth.getSession(); if (data && data.session) token = data.session.access_token; } catch (_) {}
    const res = await fetch("/api/delete-account", { method: "POST", headers: { "Authorization": "Bearer " + token } });
    let body = {}; try { body = await res.json(); } catch (_) {}
    if (!res.ok || (body && body.error)) throw new Error((body && body.error && body.error.message) || "Couldn't delete the account.");
    try { localStorage.removeItem(storeKey); } catch (_) {}
    await onSignOut();
  };

  const colors = useMemo(() => colorMapOf(config.categories), [config.categories]);

  const addRecurringFor = (kind, fields, date, cadence) => ({ id: "rec" + Date.now().toString(36) + Math.random().toString(36).slice(2, 5), kind, cadence, anchor: date, lastPosted: date, ...fields });

  const addExpense = (rec) => {
    const { repeat, ...clean } = rec;
    const full = { ...clean, id: Date.now().toString() };
    const nextTx = [full, ...tx];
    const nextConfig = repeat ? { ...config, recurring: [...(config.recurring || []), addRecurringFor("expense", { who: clean.who, amount: clean.amount, category: clean.category, sub: clean.sub }, clean.date, repeat)] } : config;
    setTx(nextTx); if (repeat) setConfig(nextConfig);
    persist({ config: nextConfig, tx: nextTx, income });
    const spent = nextTx.filter((x) => x.category === clean.category && monthKey(x.date) === thisMonth()).reduce((a, b) => a + b.amount, 0);
    setConfirm({ kind: "expense", rec: full, spent, budget: nextConfig.budgets[clean.category] || 0 }); setAdding(false);
  };
  const addExpenseBatch = (recs, merchant) => {
    const base = Date.now();
    const stamped = recs.map((r, i) => ({ ...r, id: (base + i).toString() }));
    const next = [...stamped, ...tx]; saveTx(next);
    const total = stamped.reduce((a, b) => a + b.amount, 0);
    const catCount = new Set(stamped.map((r) => r.category)).size;
    setConfirm({ kind: "batch", count: stamped.length, total, catCount, merchant: merchant || "" }); setAdding(false);
  };
  const addStatementBatch = (rows) => {
    const base = Date.now();
    // Normalize the imported account against ones we already know, matching by last-4
    // so the same card named slightly differently doesn't fork into two accounts.
    const incomingAcct = (rows.find((r) => r.account) || {}).account || "";
    const last4Of = (s) => { const m = (s || "").match(/(\d{4})\s*$/); return m ? m[1] : ""; };
    const incLast4 = last4Of(incomingAcct);
    let acctName = incomingAcct;
    const known = config.accounts || [];
    if (incLast4) { const hit = known.find((a) => last4Of(a.name) === incLast4); if (hit) acctName = hit.name; }
    // Re-tag rows to the canonical account name.
    const tagged = rows.map((r) => (r.account ? { ...r, account: acctName } : r));

    // Skip transactions that already exist (same date + amount + normalized merchant —
    // account-independent, so a re-import or a differently-named merchant still matches).
    const nrm = (s) => String(s || "").toLowerCase().replace(/[#0-9]+\s*$/, "").replace(/\s+/g, " ").trim();
    const txKey = (d, who, amt) => `${d}|${Math.round(amt * 100)}|${nrm(who)}`;
    const existingTx = new Set(tx.map((x) => txKey(x.date, x.who, x.amount)));
    const existingInc = new Set(income.map((x) => txKey(x.date, x.source, x.amount)));

    const expRows = tagged.filter((r) => r.kind === "expense");
    const incRows = tagged.filter((r) => r.kind === "income");
    let dupes = 0;
    const exp = []; expRows.forEach((r, i) => { const k = txKey(r.date, r.who, r.amount); if (existingTx.has(k)) { dupes++; return; } existingTx.add(k); exp.push({ id: (base + i).toString(), who: r.who, amount: r.amount, date: r.date, category: r.category, account: r.account || undefined }); });
    const inc = []; incRows.forEach((r, i) => { const k = txKey(r.date, r.who, r.amount); if (existingInc.has(k)) { dupes++; return; } existingInc.add(k); inc.push({ id: (base + 5000 + i).toString(), source: r.who, amount: r.amount, date: r.date, account: r.account || undefined }); });

    const nextTx = exp.length ? [...exp, ...tx] : tx;
    const nextIncome = inc.length ? [...inc, ...income] : income;
    if (exp.length) setTx(nextTx);
    if (inc.length) setIncome(nextIncome);
    let nextConfig = config;
    if (acctName && !known.some((a) => a.name === acctName)) {
      nextConfig = { ...config, accounts: [...known, { name: acctName, addedAt: new Date().toISOString().slice(0, 10) }] };
      setConfig(nextConfig);
    }
    persist({ config: nextConfig, tx: nextTx, income: nextIncome });
    const total = exp.reduce((a, b) => a + b.amount, 0);
    const incTotal = inc.reduce((a, b) => a + b.amount, 0);
    setConfirm({ kind: "statement", expCount: exp.length, incCount: inc.length, total, incTotal, account: acctName, dupes }); setAdding(false);
  };
  const addIncome = (rec) => {
    const { repeat, ...clean } = rec;
    const full = { ...clean, id: Date.now().toString() };
    const nextIncome = [full, ...income];
    // Remember this source's tax treatment so it defaults next time.
    const srcKey = (clean.source || "").trim();
    const nextSources = srcKey ? { ...(config.incomeSources || {}), [srcKey]: { taxable: !!clean.taxable } } : (config.incomeSources || {});
    let nextConfig = { ...config, incomeSources: nextSources };
    if (repeat) nextConfig = { ...nextConfig, recurring: [...(config.recurring || []), addRecurringFor("income", { source: clean.source, amount: clean.amount, taxable: !!clean.taxable }, clean.date, repeat)] };
    setIncome(nextIncome); setConfig(nextConfig);
    persist({ config: nextConfig, tx, income: nextIncome });
    const monthIncome = nextIncome.filter((x) => monthKey(x.date) === thisMonth()).reduce((a, b) => a + b.amount, 0);
    const pct = (nextConfig.plan && nextConfig.plan.savingsPct) || 0;
    setConfirm({ kind: "income", rec: full, monthIncome, suggestSave: Math.round(rec.amount * pct / 100) }); setAdding(false);
  };
  const updateTx = (rec) => { const next = tx.map((x) => (x.id === rec.id ? rec : x)); saveTx(next); setEditing(null); };
  const updateIncome = (rec) => { const next = income.map((x) => (x.id === rec.id ? rec : x)); saveIncome(next); setEditing(null); };
  const openContribution = (amt) => setContrib({ defaultAmount: Math.max(0, Math.round(amt || 0)) });
  const contribute = (amount, targetId) => {
    const amt = Math.max(0, Math.round(amount || 0)); if (!amt) { setContrib(null); return; }
    const { adds, leftover } = distributeSavings(amt, config, targetId);
    const addMap = Object.fromEntries(adds.map((a) => [a.id, a.amount]));
    const goals = config.goals.map((g) => (addMap[g.id] ? { ...g, saved: g.saved + addMap[g.id] } : g));
    saveConfig({ ...config, goals, savingsUnassigned: (config.savingsUnassigned || 0) + leftover });
    setContrib(null);
    setConfirm({ kind: "contribution", amount: amt, adds, unassignedAdded: leftover });
  };
  const metrics = useMemo(() => computeMetrics(tx, income, config), [tx, income, config]);
  const features = useFeatures(config, metrics);

  const setAsideTax = (rate, manualAmt) => {
    const amt = manualAmt != null ? Math.round(manualAmt) : Math.round(metrics.monthTaxableIncome * (rate || 0) / 100);
    if (amt <= 0) return;
    const entry = { id: "t" + Date.now().toString(36), date: new Date().toISOString().slice(0, 10), amount: amt };
    const total = (config.taxSetAside || 0) + amt;
    saveConfig({ ...config, taxSetAside: total, taxLog: [entry, ...(config.taxLog || [])] });
    setConfirm({ kind: "tax", amount: amt, total });
  };
  const withdrawTax = (amt) => {
    const a = Math.round(amt || 0); if (a <= 0) return;
    const entry = { id: "t" + Date.now().toString(36), date: new Date().toISOString().slice(0, 10), amount: -a, note: "withdrawn" };
    const total = Math.max(0, (config.taxSetAside || 0) - a);
    saveConfig({ ...config, taxSetAside: total, taxLog: [entry, ...(config.taxLog || [])] });
  };
  const assignUnassigned = (amount, targetId) => {
    const avail = config.savingsUnassigned || 0;
    const amt = Math.min(avail, Math.max(0, Math.round(amount || 0)));
    if (!amt || !targetId) { setAssign(false); return; }
    const goals = config.goals.map((g) => (g.id === targetId ? { ...g, saved: g.saved + amt } : g));
    saveConfig({ ...config, goals, savingsUnassigned: avail - amt });
    setAssign(false);
    const g = config.goals.find((x) => x.id === targetId);
    setConfirm({ kind: "contribution", amount: amt, adds: [{ id: targetId, name: g ? g.name : "Goal", amount: amt }], unassignedAdded: 0 });
  };

  let title = "Summary";
  if (detail) title = detail; else title = { home: "Summary", plan: "Plan", networth: "Net worth", income: "Transactions", insights: "Insights", setup: "Budgets", profile: "Profile", learn: "Learn" }[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,textarea{font-family:inherit;} input:focus,select:focus,textarea: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}.stagger>*:nth-child(6){animation-delay:.27s}
        .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}}
        .msgin{animation:msgin .32s cubic-bezier(.22,.61,.36,1) both;}@keyframes msgin{from{opacity:0;transform:translateY(6px)}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)}}
        .dot{animation:dot 1.2s infinite ease-in-out both;}@keyframes dot{0%,80%,100%{opacity:.25}40%{opacity:1}}
        select option{color:${T.text};background:${T.card}}
      `}</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>
          <div style={{ display: "flex", alignItems: "center", gap: 10, flexShrink: 0 }}>
            <span title={syncState === "error" ? "Offline — changes saved on this device, will sync later" : syncState === "syncing" ? "Syncing…" : "Synced"} style={{ width: 8, height: 8, borderRadius: 4, background: syncState === "error" ? T.red : syncState === "syncing" ? T.orange : T.green, transition: "background .3s", flexShrink: 0 }} />
            <button data-tour="coach" onClick={() => setCoachOpen(true)} className="press" aria-label="Coach" style={{ width: 38, height: 38, borderRadius: 19, border: "none", cursor: "pointer", background: T.fill2, color: T.orange, display: "grid", placeItems: "center" }}><ISpark size={20} color={T.orange} /></button>
            <button data-tour="add" onClick={() => setAdding(true)} className="press" aria-label="Add" style={{ width: 38, height: 38, borderRadius: 19, border: "none", cursor: "pointer", background: T.ink, color: T.bg, display: "grid", placeItems: "center", boxShadow: "0 4px 14px rgba(0,0,0,.18)" }}><IPlus size={22} sw={2.2} /></button>
            <button data-tour="profile" onClick={() => { setDetail(null); setView("profile"); }} className="press" aria-label="Profile" style={{ width: 38, height: 38, borderRadius: 19, border: "none", cursor: "pointer", background: view === "profile" ? T.ink : T.fill2, color: view === "profile" ? T.bg : T.dim, display: "grid", placeItems: "center" }}><IUser size={20} /></button>
          </div>
        </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} colors={colors} onDelete={(id) => saveTx(tx.filter((x) => x.id !== id))} onEdit={(r) => setEditing({ kind: "expense", rec: r })} /></div>
            : <div key={view} className="vin">
              {view === "home" ? <HomeView metrics={metrics} config={config} features={features} tx={tx} colors={colors} onDelete={(id) => saveTx(tx.filter((x) => x.id !== id))} onOpenCat={setDetail} onSetPrimary={(id) => saveConfig({ ...config, primaryGoalId: id })} onCoach={() => setCoachOpen(true)} onEdit={(r) => setEditing({ kind: "expense", rec: r })} />
                : view === "plan" ? <PlanView metrics={metrics} config={config} saveConfig={saveConfig} onRedo={() => setOnboard(true)} onContribute={openContribution} onLearn={() => setView("learn")} onSetAsideTax={setAsideTax} onWithdrawTax={withdrawTax} onAssign={() => setAssign(true)} />
                : view === "networth" ? <NetWorthView onLearn={() => setView("learn")} />
                : view === "learn" ? <LearnView config={config} saveConfig={saveConfig} onCoach={() => setCoachOpen(true)} onAskGem={(g) => { setCoachSeed("Tell me more about \"" + g.title + "\" and how it applies to my situation."); setCoachOpen(true); }} />
                : view === "income" ? <TransactionsView metrics={metrics} income={income} tx={tx} colors={colors} onDelete={(id) => saveIncome(income.filter((x) => x.id !== id))} onEdit={(r) => setEditing({ kind: "income", rec: r })} onDeleteTx={(id) => saveTx(tx.filter((x) => x.id !== id))} onEditTx={(r) => setEditing({ kind: "expense", rec: r })} onDeleteMany={deleteMany} />
                  : view === "insights" ? <InsightsView metrics={metrics} config={config} saveConfig={saveConfig} hideScore={prefs.hideScore} tx={tx} income={income} onAsk={(seed) => { setCoachSeed(seed); setCoachOpen(true); }} />
                    : view === "profile" ? <ProfileView config={config} saveConfig={saveConfig} accountEmail={session.user.email} onSignOut={onSignOut} onExport={exportData} onImport={importData} onDeleteAccount={deleteAccount} onEditProfile={() => setProfileSetup(true)} onReplayTour={() => { setView("home"); setStartTour(true); saveConfig({ ...config, tourDone: false }); }} onFindDupes={findDuplicates} onRemoveDupes={removeDuplicates} />
                    : <SetupView config={config} metrics={metrics} onSave={saveConfig} onExport={exportData} onImport={importData} accountEmail={session.user.email} onSignOut={onSignOut} onSetupIncome={() => setOnboard(true)} />}
            </div>}
      </div>

      <nav style={{ position: "fixed", bottom: 0, left: 0, right: 0, maxWidth: 480, margin: "0 auto", background: T.navbg, 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" tour="tab-home" active={view === "home" && !detail} onClick={() => { setDetail(null); setView("home"); }} />
        {features.showPlanTab && <Tab icon={IPlan} label="Plan" tour="tab-plan" active={view === "plan" && !detail} onClick={() => { setDetail(null); setView("plan"); }} />}
        {features.showWorthTab && <Tab icon={IWorth} label="Worth" tour="tab-networth" active={view === "networth" && !detail} onClick={() => { setDetail(null); setView("networth"); }} />}
        <Tab icon={IWallet} label="Activity" tour="tab-income" active={view === "income" && !detail} onClick={() => { setDetail(null); setView("income"); }} />
        <Tab icon={IChart} label="Insights" tour="tab-insights" active={view === "insights" && !detail} onClick={() => { setDetail(null); setView("insights"); }} />
        <Tab icon={ISliders} label="Budgets" tour="tab-setup" active={view === "setup" && !detail} onClick={() => { setDetail(null); setView("setup"); }} />
      </nav>

      {adding && <AddSheet config={config} metrics={metrics} income={income} tx={tx} colors={colors} onClose={() => setAdding(false)} onExpense={addExpense} onExpenseBatch={addExpenseBatch} onStatementBatch={addStatementBatch} onIncome={addIncome} />}
      {confirm && <ConfirmCard data={confirm} goal={primaryGoal(config)} colors={colors} onClose={() => setConfirm(null)} onContribute={(amt) => { setConfirm(null); openContribution(amt); }} />}
      {/* New, un-profiled users meet Syrup first in a conversation. */}
      {onboard && config.profile && !config.profile.profiled && (
        <SyrupWelcome config={config} saveConfig={saveConfig} onComplete={() => {
          /* Profile is now set via learn_about_user; continue to numbers-based onboarding */
          saveConfig({ ...config, profile: { ...(config.profile || {}), profiled: true } });
        }} />
      )}
      {onboard && (!config.profile || config.profile.profiled) && <Onboarding config={config} metrics={metrics} onClose={() => { setOnboard(false); setStartTour(true); if (!config.onboarded) saveConfig({ ...config, onboarded: true, tourDone: false }); }} onComplete={(c) => { saveConfig({ ...c, onboarded: true, tourDone: false }); setOnboard(false); setView("home"); setStartTour(true); }} />}
      {profileSetup && <ProfileSetup config={config} embedded onClose={() => setProfileSetup(false)} onComplete={(c) => { saveConfig(c); setProfileSetup(false); }} />}
      {coachOpen && <CoachSheet metrics={metrics} config={config} saveConfig={saveConfig} seed={coachSeed} tx={tx} income={income} onLogExpense={addExpense} onLogIncome={addIncome} onStatementBatch={addStatementBatch} onNavigate={(to) => { setCoachOpen(false); setCoachSeed(""); setDetail(null); setView(to); }} onClose={() => { setCoachOpen(false); setCoachSeed(""); }} />}
      {contrib && <ContributionSheet config={config} metrics={metrics} defaultAmount={contrib.defaultAmount} onClose={() => setContrib(null)} onApply={contribute} />}
      {assign && <ContributionSheet config={config} metrics={metrics} assignMode defaultAmount={config.savingsUnassigned || 0} onClose={() => setAssign(false)} onApply={assignUnassigned} />}
      {editing && <EditSheet kind={editing.kind} rec={editing.rec} config={config} onClose={() => setEditing(null)} onSave={editing.kind === "income" ? updateIncome : updateTx} onDelete={(id) => { if (editing.kind === "income") saveIncome(income.filter((x) => x.id !== id)); else saveTx(tx.filter((x) => x.id !== id)); setEditing(null); }} />}
      {!loading && !onboard && (startTour || (config.onboarded && !config.tourDone)) && <Tour view={view} setView={(v) => { setDetail(null); setView(v); }} openCoach={() => setCoachOpen(true)} onDone={() => { setStartTour(false); setCoachOpen(false); saveConfig({ ...config, tourDone: true }); }} />}
    </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 categories = config.categories || [];
  const mk = thisMonth();
  const thisTx = tx.filter((t) => monthKey(t.date) === mk);
  const byCat = {}; categories.forEach((c) => (byCat[c.name] = 0));
  thisTx.forEach((t) => { if (t.category in byCat) 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 = categories.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 dataProj = iNonzero.length ? iNonzero.reduce((a, b) => a + b, 0) / iNonzero.length : 0;
  // Plan figures the user stated at setup (reliable; data is noisy until months accrue).
  const plan = config.plan || {};
  const planTypical = plan.typicalIncome || 0;
  const planLean = plan.leanIncome || 0;
  // Trust the plan until there's enough real history (3+ months with income) to lead.
  const monthsOfData = iNonzero.length;
  const projIncome = monthsOfData >= 3 ? dataProj : (planTypical || dataProj || planLean);
  const dataLow = iNonzero.length ? Math.min(...iNonzero) : 0;
  const dataHigh = iNonzero.length ? Math.max(...iNonzero) : 0;
  // Range never contradicts the stated plan: clamp the low to the lean figure, high to typical-or-data.
  const incLow = monthsOfData >= 3 ? dataLow : (planLean || dataLow);
  const incHigh = monthsOfData >= 3 ? dataHigh : (planTypical || dataHigh || planLean);
  // 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;
  // Intended savings comes from the PLAN (savingsPct of planning income), not "income minus
  // whatever spending happened to be logged" — that falsely treated every un-logged dollar as saved.
  const planningIncome = plan.basis === "typical" ? (planTypical || projIncome) : (planLean || projIncome);
  const savingsPct = typeof plan.savingsPct === "number" ? plan.savingsPct : 15;
  const saveRate = Math.round((planningIncome * savingsPct / 100) || 0);

  const goals = (config.goals || []).map((g) => {
    const toGo = Math.max(0, g.target - g.saved);
    const pct = g.target > 0 ? Math.min(100, (g.saved / g.target) * 100) : 0;
    const months = saveRate > 0 ? Math.ceil(toGo / saveRate) : null;
    return { ...g, toGo, pct, months, isPrimary: g.id === config.primaryGoalId };
  });
  const primary = goals.find((g) => g.isPrimary) || goals[0] || null;
  const monthsToGoal = primary ? primary.months : null;

  const monthIncome = income.filter((x) => monthKey(x.date) === mk).reduce((a, b) => a + b.amount, 0);

  // 6-month income + expense trend (for the Insights chart)
  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);
    const inc = income.filter((x) => monthKey(x.date) === key).reduce((a, b) => a + b.amount, 0);
    const exp = tx.filter((x) => monthKey(x.date) === key).reduce((a, b) => a + b.amount, 0);
    trend.push({ key, label: d.toLocaleDateString("en-US", { month: "short" }), amount: inc, expense: exp });
  }
  const trendMax = Math.max(1, ...trend.map((t) => Math.max(t.amount, t.expense)));

  // This-month spending by category (for the breakdown donut)
  const spendByCat = categories.map((c) => ({ name: c.name, color: c.color, amount: byCat[c.name] || 0 })).filter((c) => c.amount > 0).sort((a, b) => b.amount - a.amount);

  // Next expected payday from recurring income (soonest upcoming occurrence).
  const todayISO = new Date().toISOString().slice(0, 10);
  let nextPayday = null;
  (config.recurring || []).filter((r) => r.kind === "income").forEach((r) => {
    const nd = nextOccurrence({ lastPosted: todayISO, anchor: r.anchor, cadence: r.cadence });
    if (!nextPayday || nd < nextPayday.date) nextPayday = { date: nd, amount: r.amount, source: r.source };
  });

  // 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);

  // --- Variable-income tools ---
  // Runway: how many lean months your readily-available reserves cover at your essentials
  // floor. We count the emergency buffer + unassigned savings, but NOT money earmarked for
  // specific goals (that isn't really "available" — draining your house fund to eat is a
  // last resort, not runway). Keeps the number honest rather than falsely comforting.
  const goalSaved = (config.goals || []).reduce((a, g) => a + (g.saved || 0), 0);
  const unassignedSaved = config.savingsUnassigned || 0;
  const savedPool = goalSaved + unassignedSaved;
  const bufferSaved = plan.bufferSaved || 0;
  const essentialNames = new Set((categories.filter((c) => c.essential)).map((c) => c.name));
  const essentialsFloor = Object.entries(config.budgets || {}).reduce((a, [k, v]) => a + (essentialNames.has(k) ? (v || 0) : 0), 0);
  const monthlyBurn = essentialsFloor > 0 ? essentialsFloor : projSpend; // fall back to projected spend
  const reserves = bufferSaved + unassignedSaved; // available, not goal-locked
  const runwayMonths = monthlyBurn > 0 ? reserves / monthlyBurn : 0;

  // Volatility: how bumpy income is across the months that actually had income (coefficient of variation).
  const trendVals = trend.map((t) => t.amount).filter((x) => x > 0);
  let volatility = 0, volLabel = "steady";
  if (trendVals.length >= 2) {
    const mean = trendVals.reduce((a, b) => a + b, 0) / trendVals.length;
    const variance = trendVals.reduce((a, b) => a + (b - mean) * (b - mean), 0) / trendVals.length;
    const cv = mean > 0 ? Math.sqrt(variance) / mean : 0;
    volatility = Math.round(cv * 100); // % swing
    volLabel = cv < 0.15 ? "steady" : cv < 0.35 ? "bumpy" : "swingy";
  }

  // Tax set-aside: only on income you actually owe tax on (1099/self-employed/side gig),
  // never on W-2 pay that's already withheld. An entry is taxable if its own flag says so,
  // or — for older entries without a flag — if its source is marked taxable.
  const srcTax = config.incomeSources || {};
  const isTaxable = (x) => (typeof x.taxable === "boolean") ? x.taxable : !!(srcTax[x.source] && srcTax[x.source].taxable);
  const monthTaxableIncome = income.filter((x) => monthKey(x.date) === mk && isTaxable(x)).reduce((a, b) => a + b.amount, 0);
  const taxRate = typeof plan.taxRate === "number" ? plan.taxRate : 0; // 0 = feature off
  const taxSetAside = config.taxSetAside || 0;
  const monthTaxOwed = Math.round(monthTaxableIncome * taxRate / 100);
  const hasTaxableIncome = income.some(isTaxable);
  const taxSuggestPerIncome = taxRate; // % to set aside on each taxable deposit
  // Year-to-date: taxable income earned this calendar year, and what you'd owe at your rate.
  const yearNow = new Date().getFullYear();
  const ytdTaxableIncome = income.filter((x) => isTaxable(x) && new Date(x.date).getFullYear() === yearNow).reduce((a, b) => a + b.amount, 0);
  const ytdTaxOwed = Math.round(ytdTaxableIncome * taxRate / 100);
  const taxCoverage = ytdTaxOwed > 0 ? Math.min(100, Math.round((taxSetAside / ytdTaxOwed) * 100)) : (taxSetAside > 0 ? 100 : 0);
  const taxShortfall = Math.max(0, ytdTaxOwed - taxSetAside);

  return { thisTx, cats, totalSpent, totalBudget, overall, projIncome, incLow, incHigh, projSpend, saveRate, goals, primary, monthsToGoal, monthIncome, trend, trendMax, sources, leftToSpend: totalBudget - totalSpent,
    runwayMonths, reserves, monthlyBurn, essentialsFloor, savedPool, volatility, volLabel, taxRate, taxSetAside, monthTaxOwed, monthTaxableIncome, hasTaxableIncome, taxSuggestPerIncome,
    ytdTaxableIncome, ytdTaxOwed, taxCoverage, taxShortfall, taxYear: yearNow, spendByCat, nextPayday };
}

/* The allocation plan: tiered waterfall off a conservative income figure. */
function computePlan(metrics, config) {
  const p = { ...DEFAULT_PLAN, ...(config.plan || {}) };
  const cats = config.categories || [];
  const essentialCats = cats.filter((c) => c.essential);
  const flexCats = cats.filter((c) => !c.essential);
  const dataLean = Math.round(metrics.incLow || 0);
  const dataTypical = Math.round(metrics.projIncome || 0);
  const planningIncome = p.basis === "expected"
    ? (dataTypical || p.typicalIncome || p.leanIncome || 0)
    : (p.leanIncome || dataLean || dataTypical || 0);
  const essentialsFloor = essentialCats.reduce((a, c) => a + (config.budgets[c.name] || 0), 0);
  const savingsPct = Math.max(0, Math.min(60, p.savingsPct != null ? p.savingsPct : 15));
  const savingsTarget = Math.round(planningIncome * savingsPct / 100);
  const flexAvailable = Math.max(0, planningIncome - essentialsFloor - savingsTarget);
  const flexBudgeted = flexCats.reduce((a, c) => a + (config.budgets[c.name] || 0), 0);
  const committed = essentialsFloor + savingsTarget + flexBudgeted;
  const overBy = committed - planningIncome; // >0 means over-committed
  const bufferMonths = p.bufferMonths || 3;
  const bufferTarget = essentialsFloor * bufferMonths;
  const bufferSaved = p.bufferSaved || 0;
  const essentialsTooHigh = planningIncome > 0 && essentialsFloor > planningIncome;
  return { ...p, planningIncome, dataLean, dataTypical, essentialCats, flexCats, essentialsFloor, savingsPct, savingsTarget, flexAvailable, flexBudgeted, overBy, bufferMonths, bufferTarget, bufferSaved, essentialsTooHigh };
}

/* Savings waterfall: fill the primary goal first, overflow to the next, etc. */
function goalOrder(config) {
  const ids = (config.goals || []).map((g) => g.id);
  return [config.primaryGoalId, ...ids.filter((id) => id !== config.primaryGoalId)].filter(Boolean);
}
function distributeSavings(amount, config, targetId) {
  const amt = Math.max(0, Math.round(amount || 0));
  let remaining = amt; const adds = [];
  if (targetId) {
    const g = (config.goals || []).find((x) => x.id === targetId);
    if (g) {
      const room = Math.max(0, g.target - g.saved);
      const add = Math.min(remaining, room);
      if (add > 0) { adds.push({ id: g.id, name: g.name, amount: add }); remaining -= add; }
    }
    return { adds, leftover: remaining }; // direct: overflow waits in unassigned, no cascade
  }
  for (const id of goalOrder(config)) {
    const g = (config.goals || []).find((x) => x.id === id); if (!g) continue;
    const room = Math.max(0, g.target - g.saved);
    const add = Math.min(remaining, room);
    if (add > 0) { adds.push({ id, name: g.name, amount: add }); remaining -= add; }
    if (remaining <= 0) break;
  }
  return { adds, leftover: remaining };
}

/* Recurring entries: client-side materialization (no backend cron). */
const CADENCE_DAYS = { weekly: 7, biweekly: 14 };
const cadenceLabel = (c) => (c === "weekly" ? "Weekly" : c === "biweekly" ? "Every 2 weeks" : "Monthly");
function nextOccurrence(t) {
  const base = new Date((t.lastPosted || t.anchor) + "T00:00:00");
  if (t.cadence === "monthly") {
    const day = new Date(t.anchor + "T00:00:00").getDate();
    const d = new Date(base.getFullYear(), base.getMonth() + 1, 1);
    const dim = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
    d.setDate(Math.min(day, dim));
    return d.toISOString().slice(0, 10);
  }
  return new Date(base.getTime() + (CADENCE_DAYS[t.cadence] || 7) * 86400000).toISOString().slice(0, 10);
}
function dueDates(t, today) {
  const res = []; const todayD = new Date(today + "T00:00:00");
  let cursor = { lastPosted: t.lastPosted || t.anchor, anchor: t.anchor, cadence: t.cadence };
  let guard = 0;
  while (guard++ < 120) {
    const nd = nextOccurrence(cursor);
    if (new Date(nd + "T00:00:00") > todayD) break;
    res.push(nd); cursor = { ...cursor, lastPosted: nd };
  }
  return res;
}
function materializeRecurring(config, tx, income) {
  const today = new Date().toISOString().slice(0, 10);
  let newTx = tx, newIncome = income, changed = false, stamp = Date.now();
  const recurring = (config.recurring || []).map((t) => ({ ...t }));
  recurring.forEach((t) => {
    const dates = dueDates(t, today);
    if (!dates.length) return;
    dates.forEach((date) => {
      stamp += 1;
      if (t.kind === "income") newIncome = [{ id: "r" + stamp, source: t.source, amount: t.amount, date, recId: t.id }, ...newIncome];
      else newTx = [{ id: "r" + stamp, who: t.who, amount: t.amount, date, category: t.category, sub: t.sub || undefined, recId: t.id }, ...newTx];
    });
    t.lastPosted = dates[dates.length - 1]; changed = true;
  });
  return changed ? { config: { ...config, recurring }, tx: newTx, income: newIncome } : null;
}

/* ================================================================== */
/*  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, features, tx, colors, onDelete, onOpenCat, onSetPrimary, onCoach, onEdit }) {
  const g = metrics.primary; const pct = g ? g.pct : 0;
  const saved = useCountUp(g ? g.saved : 0);
  const recent = [...tx].sort((a, b) => b.date.localeCompare(a.date)).slice(0, 5);
  const used = metrics.cats.filter((c) => c.budget > 0);
  const others = metrics.goals.filter((x) => !x.isPrimary);
  const f = features || {};
  const order = (f.summaryPriority || ["primary_goal", "other_goals", "coach", "payday", "projected_income", "spent_month", "budgets", "recent"]);

  // Contextual nudge for the coach card — pick the most relevant thing to say
  const nudge = useMemo(() => {
    const p = config.profile || {};
    // Budget alert
    const hotCat = used.find((c) => c.budget > 0 && c.spent > c.budget * 0.85);
    if (hotCat && hotCat.spent > hotCat.budget) return { icon: "⚠️", text: `${hotCat.name} is **${fmt(hotCat.spent - hotCat.budget)} over budget**. Tap to strategize with Syrup.`, color: T.red };
    if (hotCat) return { icon: "🔥", text: `${hotCat.name} is at ${Math.round((hotCat.spent / hotCat.budget) * 100)}% with days left. Syrup can help you coast.`, color: T.orange };
    // Tax nudge for self-employed
    if ((p.sideBusiness || p.employment === "1099" || p.employment === "self") && metrics.hasTaxableIncome && (metrics.taxSetAside || 0) === 0) return { icon: "💰", text: "You've got taxable income but no tax reserve set aside yet. Let's fix that.", color: T.orange };
    // Goal pace
    if (g && metrics.monthsToGoal) return { icon: "✨", text: `At this pace, **${g.name}** is ~${metrics.monthsToGoal} months out. Want to get there faster?`, color: T.waffle };
    // Profile building
    if (!f.syrupKnowsUser) return { icon: "👋", text: "Tell Syrup about yourself — your job, goals, what matters. The app adapts to you.", color: T.blue };
    // Default
    return { icon: "✨", text: "Your money coach — plan a goal, get a read, upload a statement.", color: T.orange };
  }, [used, config.profile, metrics, g, f.syrupKnowsUser]);

  // Card renderers keyed by ID
  const cards = {
    primary_goal: () => g ? (
      <div key="primary_goal" 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>
        <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>
    ) : (
      <div key="primary_goal" style={{ ...card, padding: "22px", textAlign: "center", color: T.dim, fontSize: 14.5 }}>No goal yet — add one in <b>Budgets</b> or ask Syrup to help you set one up.</div>
    ),

    other_goals: () => others.length > 0 ? (
      <Group key="other_goals" label="Other goals">
        {others.map((o, i) => (
          <Row key={o.id} last={i === others.length - 1} onClick={() => onSetPrimary(o.id)}>
            <div style={{ flex: 1 }}>
              <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
                <span style={{ fontSize: 15.5, fontWeight: 500 }}>{o.name}</span>
                <span style={{ fontSize: 13, color: T.dim, fontVariantNumeric: "tabular-nums" }}>{fmt(o.saved)} / {fmt(o.target)}</span>
              </div>
              <div style={{ height: 5, borderRadius: 3, background: T.fill2, overflow: "hidden" }}>
                <div className="bar" style={{ height: "100%", width: `${o.pct}%`, background: T.waffle, borderRadius: 3 }} />
              </div>
            </div>
            <span style={{ fontSize: 11.5, color: T.faint, fontWeight: 600, whiteSpace: "nowrap" }}>Make primary</span>
            <IChevR size={18} color={T.faint} sw={2} />
          </Row>
        ))}
      </Group>
    ) : null,

    payday: () => f.showPayday && metrics.nextPayday ? (() => {
      const days = Math.round((new Date(metrics.nextPayday.date + "T00:00:00") - new Date(new Date().toISOString().slice(0, 10) + "T00:00:00")) / 86400000);
      const when = days <= 0 ? "today" : days === 1 ? "tomorrow" : `in ${days} days`;
      return (
        <div key="payday" style={{ ...card, padding: "16px 18px", display: "flex", alignItems: "center", gap: 13 }}>
          <span style={{ width: 40, height: 40, borderRadius: 20, background: T.green + "1A", display: "grid", placeItems: "center", flexShrink: 0 }}><IWallet size={20} color={T.green} /></span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 15.5, fontWeight: 600 }}>Next payday {when}</div>
            <div style={{ fontSize: 13, color: T.dim }}>~{fmt(metrics.nextPayday.amount)} from {metrics.nextPayday.source} · {new Date(metrics.nextPayday.date + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric" })}</div>
          </div>
        </div>
      );
    })() : null,

    coach: () => (
      <div key="coach" className="press" onClick={onCoach} style={{ ...card, padding: "16px 18px", display: "flex", alignItems: "center", gap: 13, cursor: "pointer", border: nudge.color === T.red || nudge.color === T.orange ? `1px solid ${nudge.color}33` : "none" }}>
        <span style={{ width: 40, height: 40, borderRadius: 20, background: T.orange + "1A", display: "grid", placeItems: "center", flexShrink: 0 }}><ISpark size={21} color={T.orange} /></span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15.5, fontWeight: 600 }}>Talk to Syrup</div>
          <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.4 }}><RichText text={nudge.text} /></div>
        </div>
        <IChevR size={18} color={T.faint} sw={2} />
      </div>
    ),

    projected_income: () => f.showProjectedIncome ? (
      <div key="projected_income" 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>
    ) : null,

    spent_month: () => (
      <div key="spent_month" 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: () => used.length > 0 ? (
      <Group key="budgets" 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>
    ) : null,

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

  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
      {order.map((id) => {
        const render = cards[id];
        return render ? render() : null;
      })}
    </div>
  );
}

/* Swipe-left to reveal Edit + Delete, like Apple Mail. Rebuilt in JS (we're a web app),
   so it locks to horizontal past a small threshold to avoid fighting vertical scroll and
   the browser's edge back-gesture. Uses a direct DOM transform (no per-frame re-render). */
function SwipeRow({ onEdit, onDelete, last, children }) {
  const fgRef = useRef(null);
  const start = useRef({ x: 0, y: 0, locked: null, openAt: 0 });
  const ACTION_W = onEdit ? 152 : 80; // width of the revealed action zone
  const [open, setOpen] = useState(false);

  const setX = (x) => { if (fgRef.current) fgRef.current.style.transform = `translateX(${x}px)`; };
  const onStart = (e) => {
    const t = e.touches[0];
    start.current = { x: t.clientX, y: t.clientY, locked: null, openAt: open ? -ACTION_W : 0 };
  };
  const onMove = (e) => {
    const t = e.touches[0];
    const dx = t.clientX - start.current.x;
    const dy = t.clientY - start.current.y;
    if (start.current.locked === null) {
      if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return; // dead-zone
      start.current.locked = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
    }
    if (start.current.locked !== "x") return; // vertical → let the page scroll
    if (fgRef.current) fgRef.current.style.transition = "none";
    let next = start.current.openAt + dx;
    next = Math.max(-ACTION_W - 24, Math.min(0, next)); // clamp; slight overscroll
    setX(next);
  };
  const onEnd = () => {
    if (start.current.locked !== "x") return;
    if (fgRef.current) fgRef.current.style.transition = "transform .22s cubic-bezier(.22,.61,.36,1)";
    const current = fgRef.current ? new WebKitCSSMatrix(getComputedStyle(fgRef.current).transform).m41 : 0;
    const shouldOpen = current < -ACTION_W / 2;
    setX(shouldOpen ? -ACTION_W : 0);
    setOpen(shouldOpen);
  };
  const close = () => { if (fgRef.current) fgRef.current.style.transition = "transform .22s cubic-bezier(.22,.61,.36,1)"; setX(0); setOpen(false); };

  return (
    <div style={{ position: "relative", overflow: "hidden", borderBottom: last ? "none" : `1px solid ${T.sep}` }}>
      {/* action layer underneath */}
      <div style={{ position: "absolute", top: 0, right: 0, bottom: 0, display: "flex", alignItems: "stretch" }}>
        {onEdit && <button className="press" onClick={() => { close(); onEdit(); }} style={{ width: 72, border: "none", cursor: "pointer", background: T.blue, color: "#fff", fontSize: 14.5, fontWeight: 600, display: "flex", alignItems: "center", justifyContent: "center" }}>Edit</button>}
        <button className="press" onClick={() => onDelete()} style={{ width: 80, border: "none", cursor: "pointer", background: T.red, color: "#fff", fontSize: 14, fontWeight: 600, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 3 }}><ITrash size={17} color="#fff" />Delete</button>
      </div>
      {/* foreground (the actual row) */}
      <div ref={fgRef} onTouchStart={onStart} onTouchMove={onMove} onTouchEnd={onEnd} style={{ position: "relative", background: T.card, willChange: "transform", touchAction: "pan-y" }}>
        {children}
      </div>
    </div>
  );
}

function TxRow({ r, last, colors, onDelete, onEdit }) {
  const col = (colors && colors[r.category]) || T.faint;
  return (
    <SwipeRow last={last} onEdit={onEdit ? () => onEdit(r) : null} onDelete={() => onDelete(r.id)}>
      <Row last onClick={onEdit ? () => onEdit(r) : undefined}>
        <span style={{ width: 30, height: 30, borderRadius: 8, background: col + "22", display: "grid", placeItems: "center", flexShrink: 0 }}><span style={{ width: 11, height: 11, borderRadius: 6, background: col }} /></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>
    </SwipeRow>
  );
}

/* ================================================================== */
/*  CATEGORY DETAIL (subcategories)                                    */
/* ================================================================== */
function CategoryDetail({ cat, metrics, tx, colors, onDelete, onEdit }) {
  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: 13 }}>
      <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} colors={colors} onDelete={onDelete} onEdit={onEdit} />)
          : <div style={{ padding: "18px 16px", color: T.faint, fontSize: 14 }}>Nothing logged here this month.</div>}
      </Group>
    </div>
  );
}

/* ================================================================== */
/*  INCOME                                                             */
/* ================================================================== */
function TransactionsView({ metrics, income, tx, colors, onDelete, onEdit, onDeleteTx, onEditTx, onDeleteMany }) {
  const proj = useCountUp(metrics.projIncome);
  const [filter, setFilter] = useState("both"); // income | expense | both
  const [sort, setSort] = useState("recent"); // recent | high
  const [selecting, setSelecting] = useState(false);
  const [sel, setSel] = useState(() => new Set());

  // Normalize into a unified list with a `kind`.
  const incRows = income.map((r) => ({ ...r, _kind: "income", _amt: r.amount, _label: r.source, _date: r.date }));
  const expRows = tx.map((r) => ({ ...r, _kind: "expense", _amt: r.amount, _label: r.who, _date: r.date }));
  let rows = filter === "income" ? incRows : filter === "expense" ? expRows : [...incRows, ...expRows];
  rows = rows.sort((a, b) => sort === "high" ? b._amt - a._amt : b._date.localeCompare(a._date)).slice(0, 40);

  const toggleSel = (id) => setSel((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const allShownSelected = rows.length > 0 && rows.every((r) => sel.has(r.id));
  const selectAll = () => setSel((s) => { const n = new Set(s); if (allShownSelected) rows.forEach((r) => n.delete(r.id)); else rows.forEach((r) => n.add(r.id)); return n; });
  const exitSelect = () => { setSelecting(false); setSel(new Set()); };
  const doDelete = () => { if (sel.size) onDeleteMany([...sel]); exitSelect(); };

  const monthExp = metrics.totalSpent || 0;

  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
      {/* In / out summary */}
      <div style={{ display: "flex", gap: 12 }}>
        <div style={{ ...card, flex: 1, padding: "16px 18px" }}>
          <div style={{ fontSize: 12.5, color: T.dim, fontWeight: 600 }}>In this month</div>
          <div style={{ fontSize: 24, fontWeight: 800, letterSpacing: "-0.02em", marginTop: 3, color: T.green, fontVariantNumeric: "tabular-nums" }}>{fmt(metrics.monthIncome)}</div>
        </div>
        <div style={{ ...card, flex: 1, padding: "16px 18px" }}>
          <div style={{ fontSize: 12.5, color: T.dim, fontWeight: 600 }}>Out this month</div>
          <div style={{ fontSize: 24, fontWeight: 800, letterSpacing: "-0.02em", marginTop: 3, fontVariantNumeric: "tabular-nums" }}>{fmt(monthExp)}</div>
        </div>
      </div>

      {/* projection */}
      <div style={{ ...card, padding: "18px 20px" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 7, color: T.dim, fontSize: 13.5, fontWeight: 600 }}><ITrend size={15} color={T.dim} sw={2} /> Projected income</div>
        <div style={{ fontSize: 30, fontWeight: 800, letterSpacing: "-0.02em", marginTop: 4, fontVariantNumeric: "tabular-nums" }}>{fmt(proj)}<span style={{ fontSize: 15, color: T.faint, fontWeight: 600 }}>/mo</span></div>
        <div style={{ color: T.faint, fontSize: 13, marginTop: 1 }}>last 90 days · typically {fmtK(metrics.incLow)}–{fmtK(metrics.incHigh)}</div>
      </div>

      {/* Filter + sort controls */}
      <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
        <div style={{ display: "flex", background: T.fill2, borderRadius: 10, padding: 2, flex: 1 }}>
          <Seg active={filter === "income"} onClick={() => setFilter("income")} label="Income" />
          <Seg active={filter === "expense"} onClick={() => setFilter("expense")} label="Expenses" />
          <Seg active={filter === "both"} onClick={() => setFilter("both")} label="Both" />
        </div>
        <button className="press" onClick={() => setSort(sort === "recent" ? "high" : "recent")} style={{ border: "none", cursor: "pointer", background: T.fill2, color: T.dim, borderRadius: 10, padding: "9px 13px", fontSize: 12.5, fontWeight: 600, whiteSpace: "nowrap", flexShrink: 0 }}>
          {sort === "recent" ? "Recent" : "High → Low"}
        </button>
      </div>

      {/* Select toolbar */}
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginTop: -2 }}>
        <span style={{ fontSize: 13, color: T.faint, fontWeight: 600 }}>{filter === "income" ? "Income" : filter === "expense" ? "Expenses" : "All transactions"}</span>
        {selecting ? (
          <div style={{ display: "flex", gap: 14 }}>
            <button className="press" onClick={selectAll} style={{ background: "none", border: "none", color: T.blue, fontSize: 13.5, fontWeight: 600, cursor: "pointer" }}>{allShownSelected ? "Clear" : "Select all"}</button>
            <button className="press" onClick={exitSelect} style={{ background: "none", border: "none", color: T.dim, fontSize: 13.5, fontWeight: 600, cursor: "pointer" }}>Done</button>
          </div>
        ) : (
          <button className="press" onClick={() => setSelecting(true)} style={{ background: "none", border: "none", color: T.blue, fontSize: 13.5, fontWeight: 600, cursor: "pointer" }}>Select</button>
        )}
      </div>

      {/* Unified list */}
      <div style={{ ...card, padding: "2px 0" }}>
        {rows.length ? rows.map((r, i) => {
          const last = i === rows.length - 1;
          if (selecting) {
            const on = sel.has(r.id);
            return (
              <div key={r._kind + r.id} className="press" onClick={() => toggleSel(r.id)} style={{ display: "flex", alignItems: "center", gap: 12, padding: "13px 16px", borderBottom: last ? "none" : `1px solid ${T.sep}`, cursor: "pointer" }}>
                <span style={{ width: 22, height: 22, borderRadius: 6, border: `2px solid ${on ? T.blue : T.faint}`, background: on ? T.blue : "transparent", display: "grid", placeItems: "center", flexShrink: 0 }}>{on && <ICheck size={13} color="#fff" sw={3} />}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 15.5, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{r._label}</div>
                  <div style={{ fontSize: 12.5, color: T.faint }}>{prettyDate(r._date)}{r._kind === "expense" && r.category ? ` · ${r.category}` : ""}</div>
                </div>
                <span style={{ fontSize: 15.5, fontWeight: 600, color: r._kind === "income" ? T.green : T.text, fontVariantNumeric: "tabular-nums" }}>{r._kind === "income" ? "+" : ""}{fmt(r._amt)}</span>
              </div>
            );
          }
          return r._kind === "income"
            ? <IncRow key={"i" + r.id} r={r} last={last} onDelete={onDelete} onEdit={onEdit} />
            : <TxRow key={"e" + r.id} r={r} last={last} colors={colors} onDelete={onDeleteTx} onEdit={onEditTx} />;
        }) : <div style={{ padding: "18px 16px", color: T.faint, fontSize: 14 }}>Nothing yet — add with the + button.</div>}
      </div>

      {/* Sticky delete bar */}
      {selecting && sel.size > 0 && (
        <div className="pop" style={{ position: "sticky", bottom: 12, marginTop: 4 }}>
          <button className="press" onClick={doDelete} style={{ ...pill, width: "100%", background: T.red, display: "flex", alignItems: "center", justifyContent: "center", gap: 8, boxShadow: "0 8px 24px rgba(0,0,0,.35)" }}>
            <ITrash size={17} color="#fff" /> Delete {sel.size} {sel.size === 1 ? "transaction" : "transactions"}
          </button>
        </div>
      )}
    </div>
  );
}
function IncRow({ r, last, onDelete, onEdit }) {
  return (
    <SwipeRow last={last} onEdit={onEdit ? () => onEdit(r) : null} onDelete={() => onDelete(r.id)}>
      <Row last onClick={onEdit ? () => onEdit(r) : undefined}>
        <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)}{r.taxable ? " · taxable" : ""}</div>
        </div>
        <span style={{ fontSize: 15.5, fontWeight: 600, color: T.green, fontVariantNumeric: "tabular-nums" }}>+{fmt(r.amount)}</span>
      </Row>
    </SwipeRow>
  );
}

/* ================================================================== */
/*  INSIGHTS — score + category + conversational coach                 */
/* ================================================================== */
/* Grouped income-vs-expense bars over 6 months. */
function InOutChart({ trend, max }) {
  const has = trend.some((t) => t.amount > 0 || t.expense > 0);
  if (!has) return <div style={{ ...card, padding: "20px", fontSize: 13.5, color: T.faint, textAlign: "center" }}>Log a couple months of income and spending to see the trend.</div>;
  return (
    <div style={{ ...card, padding: "18px 18px 14px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 4 }}>
        <span style={{ fontSize: 15, fontWeight: 700 }}>In vs. out</span>
        <span style={{ fontSize: 12, color: T.faint }}>last 6 months</span>
      </div>
      <div style={{ display: "flex", gap: 14, marginBottom: 12 }}>
        <span style={{ display: "flex", alignItems: "center", gap: 5, fontSize: 12, color: T.dim }}><span style={{ width: 9, height: 9, borderRadius: 5, background: T.green }} /> In</span>
        <span style={{ display: "flex", alignItems: "center", gap: 5, fontSize: 12, color: T.dim }}><span style={{ width: 9, height: 9, borderRadius: 5, background: T.waffle }} /> Out</span>
      </div>
      <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 8, height: 120 }}>
        {trend.map((m) => (
          <div key={m.key} style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 6 }}>
            <div style={{ display: "flex", alignItems: "flex-end", gap: 3, height: 90, width: "100%", justifyContent: "center" }}>
              <div className="grow" title={`In ${fmt(m.amount)}`} style={{ width: "38%", maxWidth: 16, height: `${Math.max(2, (m.amount / max) * 90)}px`, background: `linear-gradient(180deg, ${T.green}, ${T.green}bb)`, borderRadius: "4px 4px 0 0" }} />
              <div className="grow" title={`Out ${fmt(m.expense)}`} style={{ width: "38%", maxWidth: 16, height: `${Math.max(2, (m.expense / max) * 90)}px`, background: `linear-gradient(180deg, ${T.waffle}, ${T.orange})`, borderRadius: "4px 4px 0 0" }} />
            </div>
            <div style={{ fontSize: 10.5, color: T.faint }}>{m.label}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

/* Spending-by-category donut for the current month. */
function SpendDonut({ data }) {
  if (!data.length) return null;
  const total = data.reduce((a, b) => a + b.amount, 0);
  const R = 60, SW = 22, C = 2 * Math.PI * R;
  let offset = 0;
  const segs = data.slice(0, 8).map((d) => {
    const frac = d.amount / total; const len = frac * C;
    const seg = { ...d, dash: len, gap: C - len, off: offset, frac };
    offset -= len; return seg;
  });
  return (
    <div style={{ ...card, padding: "20px" }}>
      <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 16 }}>Where it went this month</div>
      <div style={{ display: "flex", alignItems: "center", gap: 20 }}>
        <div style={{ position: "relative", width: 150, height: 150, flexShrink: 0 }}>
          <svg width="150" height="150" style={{ transform: "rotate(-90deg)" }}>
            <circle cx="75" cy="75" r={R} fill="none" stroke={T.fill2} strokeWidth={SW} />
            {segs.map((s, i) => (
              <circle key={i} cx="75" cy="75" r={R} fill="none" stroke={s.color} strokeWidth={SW} strokeDasharray={`${s.dash} ${s.gap}`} strokeDashoffset={s.off} style={{ transition: "stroke-dasharray .5s ease" }} />
            ))}
          </svg>
          <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}>
            <div style={{ textAlign: "center" }}><div style={{ fontSize: 11, color: T.faint }}>spent</div><div style={{ fontSize: 19, fontWeight: 800, letterSpacing: "-0.02em", fontVariantNumeric: "tabular-nums" }}>{fmtK(total)}</div></div>
          </div>
        </div>
        <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 7 }}>
          {segs.map((s) => (
            <div key={s.name} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13 }}>
              <span style={{ width: 9, height: 9, borderRadius: 5, background: s.color, flexShrink: 0 }} />
              <span style={{ flex: 1, color: T.dim, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{s.name}</span>
              <span style={{ fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{Math.round(s.frac * 100)}%</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* Build a month-by-month history of money in/out/net, optionally filtered to one
   account. Returns newest-first. Each entry has the savings rate that month. */
function computeHistory(tx, income, accountFilter) {
  const match = (x) => !accountFilter || accountFilter === "all" || x.account === accountFilter;
  const months = {};
  const touch = (k) => (months[k] = months[k] || { key: k, income: 0, spent: 0 });
  income.filter(match).forEach((x) => { touch(monthKey(x.date)).income += x.amount; });
  tx.filter(match).forEach((x) => { touch(monthKey(x.date)).spent += x.amount; });
  return Object.values(months)
    .map((m) => ({ ...m, net: m.income - m.spent, saveRate: m.income > 0 ? Math.round(((m.income - m.spent) / m.income) * 100) : null }))
    .sort((a, b) => b.key.localeCompare(a.key));
}
function monthLabel(key) {
  const [y, mo] = key.split("-");
  return new Date(Number(y), Number(mo) - 1, 1).toLocaleDateString("en-US", { month: "short", year: "numeric" });
}

function MonthHistory({ tx, income, config }) {
  const accounts = config.accounts || [];
  const [acct, setAcct] = useState("all");
  const hist = computeHistory(tx, income, acct);
  if (!hist.length) {
    return (
      <div style={{ ...card, padding: "20px", fontSize: 13.5, color: T.faint, textAlign: "center", lineHeight: 1.5 }}>
        No history yet. Import a few months of statements (from the + button) and your spend-vs-save trend builds here.
      </div>
    );
  }
  const maxFlow = Math.max(1, ...hist.map((m) => Math.max(m.income, m.spent)));
  const avgSave = (() => { const r = hist.filter((m) => m.saveRate != null); return r.length ? Math.round(r.reduce((a, b) => a + b.saveRate, 0) / r.length) : null; })();
  return (
    <div>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 8 }}>
        <div style={grpLbl}>HISTORY</div>
        {avgSave != null && <div style={{ fontSize: 12, color: T.faint }}>avg save rate {avgSave}%</div>}
      </div>
      {accounts.length > 0 && (
        <div style={{ display: "flex", gap: 7, overflowX: "auto", paddingBottom: 8, marginBottom: 4 }}>
          <Chip active={acct === "all"} onClick={() => setAcct("all")} label="All accounts" />
          {accounts.map((a) => <Chip key={a.name} active={acct === a.name} onClick={() => setAcct(a.name)} label={a.name} />)}
        </div>
      )}
      <div style={{ ...card, padding: "6px 0" }}>
        {hist.map((m, i) => {
          const pos = m.net >= 0;
          return (
            <div key={m.key} style={{ padding: "13px 16px", borderBottom: i === hist.length - 1 ? "none" : `1px solid ${T.sep}` }}>
              <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 9 }}>
                <span style={{ fontSize: 15.5, fontWeight: 600 }}>{monthLabel(m.key)}</span>
                <span style={{ fontSize: 15, fontWeight: 700, color: pos ? T.green : T.red, fontVariantNumeric: "tabular-nums" }}>{pos ? "+" : "−"}{fmt(Math.abs(m.net))}{m.saveRate != null && <span style={{ fontSize: 12, color: T.faint, fontWeight: 600 }}> · saved {m.saveRate}%</span>}</span>
              </div>
              {/* in / out mini bars */}
              <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 5 }}>
                <span style={{ fontSize: 11, color: T.faint, width: 30 }}>In</span>
                <div style={{ flex: 1, height: 7, borderRadius: 4, background: T.fill2, overflow: "hidden" }}><div className="bar" style={{ height: "100%", width: `${(m.income / maxFlow) * 100}%`, background: T.green, borderRadius: 4 }} /></div>
                <span style={{ fontSize: 12, color: T.dim, width: 58, textAlign: "right", fontVariantNumeric: "tabular-nums" }}>{fmt(m.income)}</span>
              </div>
              <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                <span style={{ fontSize: 11, color: T.faint, width: 30 }}>Out</span>
                <div style={{ flex: 1, height: 7, borderRadius: 4, background: T.fill2, overflow: "hidden" }}><div className="bar" style={{ height: "100%", width: `${(m.spent / maxFlow) * 100}%`, background: T.waffle, borderRadius: 4 }} /></div>
                <span style={{ fontSize: 12, color: T.dim, width: 58, textAlign: "right", fontVariantNumeric: "tabular-nums" }}>{fmt(m.spent)}</span>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function ReconcileCard({ tx, income, config, onAsk }) {
  const [state, setState] = useState("idle"); // idle | loading | done | error
  const [result, setResult] = useState(null);
  const enough = tx.length >= 5;
  const run = async () => {
    setState("loading");
    try {
      const summary = buildReconcileSummary(tx, income, config);
      const out = await aiReconcile(summary, (config.prefs && config.prefs.coachTone) || "balanced");
      setResult(out); setState("done");
    } catch (e) { setState("error"); }
  };
  const typeMeta = {
    large: { icon: "⚠️", color: T.orange }, subscription: { icon: "🔁", color: T.blue },
    creep: { icon: "📈", color: T.red }, duplicate: { icon: "⧉", color: T.orange }, habit: { icon: "💡", color: T.waffle },
  };
  return (
    <div style={{ ...card, padding: "18px 20px" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 4 }}>
        <ISpark size={18} color={T.waffle} />
        <span style={{ fontSize: 15.5, fontWeight: 700 }}>Reconcile with Syrup</span>
      </div>
      <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.5, marginBottom: 14 }}>Let Syrup comb your recent transactions for unusual charges, forgotten subscriptions, spending creep, and possible duplicates — then talk it through.</div>

      {state === "idle" && (
        enough
          ? <button className="press" onClick={run} style={{ ...pill, width: "100%" }}>Review my spending</button>
          : <div style={{ fontSize: 12.5, color: T.faint, textAlign: "center", padding: "4px 0" }}>Add or import a few more transactions and Syrup can review them here.</div>
      )}
      {state === "loading" && <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "6px 0", color: T.dim, fontSize: 14 }}><ISpin size={18} className="spin" color={T.waffle} /> Syrup's reading your spending…</div>}
      {state === "error" && <div style={{ fontSize: 13.5, color: T.dim }}>Couldn't run the review just now. <button className="press" onClick={run} style={{ background: "none", border: "none", color: T.blue, fontSize: 13.5, fontWeight: 600, cursor: "pointer", padding: 0 }}>Try again</button></div>}

      {state === "done" && result && (
        <div className="vin">
          {result.summary && <div style={{ fontSize: 14, color: T.text, lineHeight: 1.5, marginBottom: 14, fontWeight: 500 }}>{result.summary}</div>}
          <div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
            {(result.findings || []).map((f, i) => {
              const meta = typeMeta[f.type] || { icon: "•", color: T.dim };
              return (
                <div key={i} className="press" onClick={() => onAsk(`About my spending — "${f.title}": ${f.detail} Can you help me think through this?`)} style={{ display: "flex", gap: 11, padding: "12px 14px", borderRadius: 14, background: T.fill2, cursor: "pointer" }}>
                  <span style={{ fontSize: 17, flexShrink: 0 }}>{meta.icon}</span>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: "flex", justifyContent: "space-between", gap: 8 }}>
                      <span style={{ fontSize: 14.5, fontWeight: 700 }}>{f.title}</span>
                      {f.amount > 0 && <span style={{ fontSize: 14, fontWeight: 700, color: meta.color, fontVariantNumeric: "tabular-nums", flexShrink: 0 }}>{fmt(f.amount)}</span>}
                    </div>
                    <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.45, marginTop: 2 }}>{f.detail}</div>
                  </div>
                </div>
              );
            })}
          </div>
          {(result.questions || []).length > 0 && (
            <div style={{ marginTop: 16 }}>
              <div style={{ fontSize: 11.5, color: T.faint, fontWeight: 600, marginBottom: 8 }}>SYRUP WANTS TO KNOW</div>
              <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
                {result.questions.map((q, i) => (
                  <button key={i} className="press" onClick={() => onAsk(q)} style={{ textAlign: "left", background: "none", border: `1px solid ${T.sep}`, borderRadius: 12, padding: "11px 13px", color: T.text, fontSize: 13.5, cursor: "pointer", lineHeight: 1.4 }}>{q}</button>
                ))}
              </div>
            </div>
          )}
          <button className="press" onClick={run} style={{ ...pillSm, marginTop: 14, background: T.fill2, color: T.dim }}>Re-run review</button>
        </div>
      )}
    </div>
  );
}

function InsightsView({ metrics, config, saveConfig, hideScore, tx, income, onAsk }) {
  const ring = useCountUp(metrics.overall); const C = 2 * Math.PI * 54;
  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: 13 }}>
      {!hideScore && (
      <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, textAlign: "center", lineHeight: 1.5, padding: "0 8px" }}>{(() => {
          const over = used.filter((c) => c.spent > c.budget).sort((a, b) => (b.spent - b.budget) - (a.spent - a.budget));
          const tight = used.filter((c) => c.spent <= c.budget && c.budget > 0 && c.spent / c.budget > 0.85);
          if (over.length) return <>You're over on <b style={{ color: T.text }}>{over[0].name}</b> by {fmt(over[0].spent - over[0].budget)}{over.length > 1 ? ` (and ${over.length - 1} other${over.length > 2 ? "s" : ""})` : ""}. That's where the score's leaking.</>;
          if (tight.length) return <><b style={{ color: T.text }}>{tight[0].name}</b> is close to its limit — keep an eye on it.</>;
          if (metrics.overall >= 80) return <>Dialed in — every category's comfortably under budget.</>;
          return <>Add budgets to your categories to start scoring your month.</>;
        })()}</div>
      </div>
      )}

      <InOutChart trend={metrics.trend} max={metrics.trendMax} />
      <SpendDonut data={metrics.spendByCat} />

      <ReconcileCard tx={tx} income={income} config={config} onAsk={onAsk} />

      <MonthHistory tx={tx} income={income} config={config} />

      <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>
  );
}

/* Finance "gems" — general principles the coach can surface and the Learn
   section lists. `when` tells the coach the rough context to mention it in.
   These are EDUCATION, never personalized advice. */
const GEMS = [
  { id: "match", path: "foundations", icon: "ISpark", title: "Always capture the full employer match", short: "It's a 50–100% instant return — the best you'll ever get.", body: "If your employer matches retirement contributions (401k, 457, etc.), contributing at least enough to get the full match is usually the highest-return move available — you're turning down free money otherwise. It comes before almost any other investing priority.", step: "Check your benefits portal for the match formula (e.g. '50% up to 6%') and confirm you're contributing at least enough to get all of it.", when: "retirement, 401k, employer benefits, raises" },
  { id: "idle", path: "foundations", icon: "IWallet", title: "Idle cash loses value every day", short: "Cash beyond your buffer should be working.", body: "Money sitting in a checking account earns nothing while inflation chips at it. Beyond your emergency buffer, parking cash in a high-yield savings account, money market, or short-term treasuries keeps it safe and actually earning. Idle cash is a quiet, daily cost.", step: "Note your checking balance. Anything well above your buffer is a candidate to move into a high-yield savings account.", when: "large savings balance, idle cash, where to keep savings" },
  { id: "hsa", path: "foundations", icon: "ICheck", title: "Max your HSA — triple tax advantage", short: "Deductible going in, grows tax-free, tax-free for medical.", body: "If you have a high-deductible health plan, an HSA is the only account that's tax-advantaged three ways: contributions reduce taxable income, growth is untaxed, and withdrawals for medical costs are tax-free. Many people treat it as a stealth retirement account by paying medical costs out of pocket and letting it grow.", step: "Check whether your health plan is HSA-eligible. If so, see if you can route even a small monthly contribution into it.", when: "health, HSA, medical, taxes, retirement" },
  { id: "roth457", path: "investing", icon: "ITrend", title: "Know your Roth vs. pre-tax order", short: "Roth IRA and 457s are powerful, often-overlooked buckets.", body: "Tax-advantaged accounts each have a role: a Roth grows tax-free, traditional accounts cut today's taxable income, and a 457 (common for public employees) has unusually flexible early-access rules. The right order depends on your bracket now vs. later — worth understanding, not guessing.", step: "Identify which tax-advantaged accounts you have access to, and ask Syrup how the typical funding order would apply to your bracket.", when: "retirement, IRA, taxes, public-sector job" },
  { id: "allocation", path: "investing", icon: "IChart", title: "Your allocation matters more than your contribution", short: "What it's invested in beats how much you add.", body: "People obsess over contribution amounts but ignore what the money is actually invested in. A retirement account sitting in a default money-market option barely grows. Over decades, your asset allocation drives the outcome far more than small changes in how much you put in.", step: "Log into your retirement account and check what it's actually invested in — not the balance, the holdings. A default cash/money-market option is a red flag.", when: "investing, 401k, allocation, portfolio" },
  { id: "brokerage", path: "investing", icon: "ISpark", title: "Brokerage accounts aren't just for the rich", short: "A taxable brokerage is flexible, accessible investing.", body: "Beyond retirement accounts, a regular taxable brokerage lets anyone invest with no contribution caps and no withdrawal penalties — useful for goals before retirement age. You don't need to be wealthy; you need to start. It's one of the most underused tools for ordinary savers.", step: "If your retirement match and buffer are handled, look up what opening a taxable brokerage account involves — it's usually a 10-minute process.", when: "investing, beyond retirement, mid-term goals" },
  { id: "taxstrategy", path: "leverage", icon: "ITrend", title: "Income up means tax strategy up", short: "More income unlocks moves that weren't worth it before.", body: "As your income grows, strategies that didn't matter at lower earnings start to pay off — tax-loss harvesting, pre-tax contributions to drop a bracket, timing income. Rising income is the trigger to revisit how much you're handing over unnecessarily.", step: "If your income has jumped, jot down roughly what you paid in tax last year — that's the number a strategy conversation starts from.", when: "raise, more income, high earner, taxes" },
  { id: "entity", path: "leverage", icon: "ISliders", title: "The wrong entity structure costs thousands", short: "Side income? How it's structured changes your tax bill.", body: "If you earn self-employment or business income, whether it's a sole proprietorship, LLC, or S-corp can mean thousands in taxes saved or wasted at higher income levels. It's worth understanding once your side income becomes real — a one-time setup with lasting payoff.", step: "If your side income is becoming real (roughly five figures), note your annual self-employment total — that's the threshold where entity choice starts to matter.", when: "business income, self-employed, side business, LLC" },
  { id: "equity", path: "leverage", icon: "IHome", title: "Idle home equity is wasted leverage", short: "Equity sitting still can sometimes be put to work.", body: "Home equity that just sits there is dormant capital. Depending on rates and goals, it can sometimes be tapped responsibly — for higher-return investments, debt consolidation, or property — though it carries real risk. The point: equity is an asset to think about, not just a number that grows passively.", step: "If you own a home, estimate your equity (value minus mortgage balance). Knowing the number is the first step to deciding whether it should stay idle.", when: "home, mortgage, equity, real estate" },
];

/* Learning paths group the gems into a sense of progression. */
const LEARN_PATHS = [
  { id: "foundations", title: "Foundations", sub: "Stop the leaks, lock in free money", icon: "ICheck" },
  { id: "investing", title: "Make it grow", sub: "Put money to work the right way", icon: "ITrend" },
  { id: "leverage", title: "Level up", sub: "Moves that matter as you earn more", icon: "ISpark" },
];

/* Which gems apply to a given profile. Anything universal returns true;
   account- or situation-specific gems only show when relevant. We err toward
   showing — only hide a gem when the profile clearly rules it out. */
function gemRelevant(id, profile) {
  const p = profile || {};
  const acct = Array.isArray(p.accounts) ? p.accounts : [];
  const has = (a) => acct.includes(a);
  const selfEmployed = p.employment === "1099" || p.employment === "self" || p.employment === "mix" || p.sideBusiness;
  switch (id) {
    case "match": return p.employment === "w2" || p.employment === "mix" || has("401k") || has("457"); // employer match only exists with an employer
    case "hsa": return has("hsa") || acct.length === 0; // show if they have one, or haven't told us yet
    case "roth457": return has("457") || has("ira") || acct.length === 0;
    case "allocation": return has("401k") || has("457") || has("ira") || has("brokerage") || acct.length === 0;
    case "brokerage": return true; // universal — anyone can open one
    case "idle": return true; // universal
    case "taxstrategy": return true; // universal as income grows
    case "entity": return selfEmployed; // only relevant with self-employment/business income
    case "equity": return p.ownsHome; // only relevant to homeowners
    default: return true;
  }
}
const gemsForProfile = (profile) => GEMS.filter((g) => gemRelevant(g.id, profile));

const STARTERS = [
  { label: "Tweak my spending", prompt: "Where did I run hot this month? Suggest one realistic budget tweak." },
  { label: "Reach my goal faster", prompt: "How can I hit my primary goal sooner without making my month miserable?" },
  { label: "Save for something new", prompt: "I want to start saving for something new." },
  { label: "What should I do next?", prompt: "Based on everything you know about me, what's the single most impactful thing I should do with my money right now?" },
];

function CoachSheet({ metrics, config, saveConfig, seed, tx, income, onLogExpense, onLogIncome, onStatementBatch, onNavigate, onClose }) {
  return (
    <Sheet onClose={onClose} zIndex={55} scroll padCss="10px 16px calc(16px + env(safe-area-inset-bottom))"
      title="Syrup"
      right={<button className="press" onClick={onClose} aria-label="Close" style={{ width: 32, height: 32, borderRadius: 16, border: "none", background: T.fill2, color: T.dim, display: "grid", placeItems: "center", cursor: "pointer" }}><IX size={18} sw={2.2} /></button>}>
      <CoachChat metrics={metrics} config={config} saveConfig={saveConfig} seed={seed} tx={tx} income={income} onLogExpense={onLogExpense} onLogIncome={onLogIncome} onStatementBatch={onStatementBatch} onNavigate={onNavigate} embedded />
    </Sheet>
  );
}

function CoachChat({ metrics, config, saveConfig, seed, tx, income, onLogExpense, onLogIncome, onStatementBatch, onNavigate, embedded }) {
  const [msgs, setMsgs] = useState(() => (Array.isArray(config.coachHistory) ? config.coachHistory.map((m) => ({ role: m.role, text: m.text, batch: m.batch })) : []));
  const [pending, setPending] = useState(false);
  const [input, setInput] = useState("");
  const [err, setErr] = useState(false);
  const scrollRef = useRef(null);
  const seeded = useRef(false);
  const fileRef = useRef(null);

  const recurSummary = (config.recurring || []).slice(0, 15).map((r) => ({ kind: r.kind, who: r.who || r.source, amount: r.amount, cadence: r.cadence, category: r.category }));

  const ctx = {
    autoAdjust: !!config.autoAdjust,
    tone: (config.prefs && config.prefs.coachTone) || "balanced",
    categoryNames: (config.categories || []).map((c) => c.name),
    data: {
      projectedIncome: Math.round(metrics.projIncome),
      incomeRange: [Math.round(metrics.incLow), Math.round(metrics.incHigh)],
      savingPerMonth: Math.round(metrics.saveRate),
      goals: metrics.goals.map((g) => ({ name: g.name, target: g.target, saved: g.saved, isPrimary: g.isPrimary, monthsToGo: g.months })),
      categories: metrics.cats.filter((c) => c.budget > 0).map((c) => ({ name: c.name, spent: Math.round(c.spent), budget: c.budget, score: c.score, essential: c.essential })),
      profile: config.profile || null,
      hasTaxableIncome: !!metrics.hasTaxableIncome,
      taxSetAside: Math.round(metrics.taxSetAside || 0),
      plan: config.plan || {},
      recurring: recurSummary,
      accounts: (config.accounts || []).map((a) => a.name),
    },
  };

  useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [msgs, pending]);

  const send = async (text) => {
    if (!text.trim() || pending) return;
    setErr(false);
    const history = msgs.map((m) => ({ role: m.role === "user" ? "user" : "assistant", content: m.text }));
    setMsgs((m) => [...m, { role: "user", text: text.trim() }]);
    setInput("");
    setPending(true);
    try {
      const raw = await callClaudeMessages([...history, { role: "user", content: text.trim() }], coachSystem(ctx));
      const { prose, actions } = extractAction(raw);
      // Separate silent learn_about_user actions from user-facing actions
      const silentActions = actions.filter((a) => a.type === "learn_about_user");
      const userActions = actions.filter((a) => a.type !== "learn_about_user");
      // Apply silent actions immediately
      silentActions.forEach((sa) => {
        if (sa.updates && typeof sa.updates === "object") {
          const profile = { ...(config.profile || {}), ...sa.updates };
          saveConfig({ ...config, profile });
        }
      });
      // Handle navigate actions — apply immediately and close
      const navAction = userActions.find((a) => a.type === "navigate");
      if (navAction && onNavigate) {
        const replyText = prose || "Taking you there now.";
        setMsgs((m) => {
          const next = [...m, { role: "assistant", text: replyText }];
          const hist = next.map((x) => ({ role: x.role, text: x.text })).slice(-20);
          saveConfig({ ...config, coachHistory: hist });
          return next;
        });
        setPending(false);
        setTimeout(() => onNavigate(navAction.to), 300);
        return;
      }
      const acts = ctx.autoAdjust ? userActions.map((a) => ({ a, applied: false })) : [];
      const replyText = prose || (acts.length ? "Here's what I can do:" : "Done.");
      setMsgs((m) => {
        const next = [...m, { role: "assistant", text: replyText, acts }];
        const hist = next.map((x) => ({ role: x.role, text: x.text })).slice(-20);
        saveConfig({ ...config, coachHistory: hist });
        return next;
      });
    } catch (e) { setErr(true); }
    finally { setPending(false); }
  };

  // If opened with a seed (e.g. tapping a reconciliation finding), send it automatically
  // so Syrup actually responds — rather than leaving the text sitting in the input.
  useEffect(() => { if (seed && seed.trim() && !seeded.current) { seeded.current = true; send(seed); } }, [seed]);

  const applyAction = (idx, ai) => {
    const msg = msgs[idx]; if (!msg || !msg.acts || !msg.acts[ai]) return;
    const a = msg.acts[ai].a; if (!a) return;
    if (a.type === "budget" && Array.isArray(a.changes)) {
      const budgets = { ...config.budgets };
      a.changes.forEach((ch) => { if (ch && ch.category in budgets && typeof ch.to === "number") budgets[ch.category] = Math.max(0, Math.round(ch.to)); });
      saveConfig({ ...config, budgets });
    } else if (a.type === "add_goal" && a.goal && a.goal.name) {
      const ng = { id: "g" + Date.now().toString(36), name: String(a.goal.name).slice(0, 40), target: Math.max(1, Math.round(a.goal.target || 0)), saved: Math.max(0, Math.round(a.goal.saved || 0)) };
      saveConfig({ ...config, goals: [...config.goals, ng] });
    } else if (a.type === "add_category" && Array.isArray(a.categories)) {
      const cats = (config.categories || []).slice();
      const budgets = { ...config.budgets };
      const names = new Set(cats.map((x) => x.name.toLowerCase()));
      const usedColors = new Set(cats.map((x) => x.color));
      a.categories.forEach((nc) => {
        const nm = String((nc && nc.name) || "").trim().slice(0, 30);
        if (!nm || names.has(nm.toLowerCase())) return;
        const color = PALETTE.find((p) => !usedColors.has(p)) || PALETTE[cats.length % PALETTE.length];
        usedColors.add(color); names.add(nm.toLowerCase());
        cats.push({ name: nm, color, essential: !!(nc && nc.essential) });
        budgets[nm] = Math.max(0, Math.round((nc && nc.budget) || 0));
      });
      saveConfig({ ...config, categories: cats, budgets });
    } else if (a.type === "contribute" && a.contribute && typeof a.contribute.amount === "number") {
      const amt = Math.max(0, Math.round(a.contribute.amount));
      const goals = (config.goals || []).slice();
      let gi = goals.findIndex((g) => g.name === a.contribute.goal);
      if (gi < 0) gi = goals.findIndex((g) => g.isPrimary); if (gi < 0) gi = 0;
      if (goals[gi]) { goals[gi] = { ...goals[gi], saved: (goals[gi].saved || 0) + amt }; saveConfig({ ...config, goals }); }
    } else if (a.type === "log_expense" && a.expense && onLogExpense) {
      onLogExpense({ who: String(a.expense.who || "Expense").slice(0, 50), amount: Math.max(0, Number(a.expense.amount) || 0), category: a.expense.category, date: /^\d{4}-\d{2}-\d{2}$/.test(a.expense.date) ? a.expense.date : new Date().toISOString().slice(0, 10) });
    } else if (a.type === "log_income" && a.income && onLogIncome) {
      onLogIncome({ source: String(a.income.source || "Income").slice(0, 50), amount: Math.max(0, Number(a.income.amount) || 0), date: /^\d{4}-\d{2}-\d{2}$/.test(a.income.date) ? a.income.date : new Date().toISOString().slice(0, 10) });
    } else if (a.type === "toggle_feature") {
      const hidden = [...(config.prefs && config.prefs.hiddenFeatures || [])];
      const feat = a.feature;
      if (a.visible) {
        const idx2 = hidden.indexOf(feat); if (idx2 >= 0) hidden.splice(idx2, 1);
      } else {
        if (!hidden.includes(feat)) hidden.push(feat);
      }
      saveConfig({ ...config, prefs: { ...config.prefs, hiddenFeatures: hidden } });
    } else if (a.type === "update_profile" && a.changes) {
      saveConfig({ ...config, profile: { ...(config.profile || {}), ...a.changes } });
    } else if (a.type === "set_budget_batch" && a.budgets) {
      const budgets = { ...config.budgets };
      Object.entries(a.budgets).forEach(([cat, val]) => { if (typeof val === "number") budgets[cat] = Math.max(0, Math.round(val)); });
      saveConfig({ ...config, budgets });
    } else if (a.type === "update_plan" && a.changes) {
      saveConfig({ ...config, plan: { ...(config.plan || {}), ...a.changes } });
    } else if (a.type === "navigate" && onNavigate) {
      onNavigate(a.to);
      return; // don't mark as applied — navigation closes the sheet
    } else if (a.type === "set_recurring" && a.recurring) {
      const r = a.recurring;
      const newRec = { id: "rec" + Date.now().toString(36) + Math.random().toString(36).slice(2, 5), kind: r.kind || "expense", cadence: r.cadence || "monthly", anchor: new Date().toISOString().slice(0, 10), lastPosted: "", who: r.who, amount: Math.max(0, Number(r.amount) || 0), category: r.category || "Other" };
      if (r.kind === "income") { newRec.source = r.who; delete newRec.who; delete newRec.category; }
      saveConfig({ ...config, recurring: [...(config.recurring || []), newRec] });
    } else if (a.type === "remove_category" && a.name) {
      const cats = (config.categories || []).filter((c) => c.name !== a.name);
      const budgets = { ...config.budgets }; delete budgets[a.name];
      saveConfig({ ...config, categories: cats, budgets });
    } else if (a.type === "rename_category" && a.from && a.to) {
      const cats = (config.categories || []).map((c) => c.name === a.from ? { ...c, name: a.to } : c);
      const budgets = { ...config.budgets };
      if (a.from in budgets) { budgets[a.to] = budgets[a.from]; delete budgets[a.from]; }
      saveConfig({ ...config, categories: cats, budgets });
    } else if (a.type === "reorder_goals" && a.primary) {
      saveConfig({ ...config, primaryGoalId: a.primary });
    } else if (a.type === "set_essential" && a.category) {
      const cats = (config.categories || []).map((c) => c.name === a.category ? { ...c, essential: !!a.essential } : c);
      saveConfig({ ...config, categories: cats });
    } else return;
    setMsgs((m) => m.map((x, i) => (i === idx ? { ...x, acts: x.acts.map((it, j) => (j === ai ? { ...it, applied: true } : it)) } : x)));
  };

  // File upload handler
  const handleFileUpload = async (e) => {
    const file = e.target.files && e.target.files[0];
    if (fileRef.current) fileRef.current.value = "";
    if (!file) return;
    const names = (config.categories || []).map((c) => c.name);
    setMsgs((m) => [...m, { role: "user", text: "📎 " + file.name }]);
    setPending(true);
    setErr(false);
    try {
      const fname = (file.name || "").toLowerCase();
      const isImage = file.type.startsWith("image/");
      const isPDF = fname.endsWith(".pdf") || file.type === "application/pdf";
      const isCSV = fname.endsWith(".csv") || fname.endsWith(".txt") || file.type === "text/csv";
      let result;
      if (isImage) {
        const b64 = await fileToB64(file);
        result = await aiReadStatement(b64, file.type, names);
      } else if (isPDF) {
        const text = await extractFileText(file);
        if (text && text.trim().length > 50) {
          result = await aiReadStatementText(text, names);
        } else {
          const pages = await pdfPagesToImages(file, 6);
          if (pages.length) {
            result = await aiReadStatement(pages[0], "image/jpeg", names);
            for (let pi = 1; pi < pages.length; pi++) {
              try {
                const more = await aiReadStatement(pages[pi], "image/jpeg", names);
                if (more && more.tx) result.tx = [...(result.tx || []), ...more.tx];
              } catch (_) {}
            }
          } else {
            throw new Error("Could not read this PDF.");
          }
        }
      } else if (isCSV) {
        const text = await extractFileText(file);
        if (!text || !text.trim()) throw new Error("Could not read this file.");
        result = await aiReadStatementText(text, names);
      } else {
        throw new Error("Unsupported file type. Try an image, PDF, or CSV.");
      }
      const rows = (result && result.tx) || [];
      if (!rows.length) {
        setMsgs((m) => [...m, { role: "assistant", text: "I read the document but couldn't find any transactions. Try a clearer image or a different format." }]);
      } else {
        const expCount = rows.filter((r) => r.kind === "expense").length;
        const incCount = rows.filter((r) => r.kind === "income").length;
        const acct = (result && result.account && result.account.name) || "";
        const total = rows.reduce((s, r) => s + (r.amount || 0), 0);
        let summary = `I found **${rows.length} transaction${rows.length === 1 ? "" : "s"}**`;
        if (acct) summary += ` from ${acct}`;
        summary += ` totaling **${fmt(total)}**`;
        if (expCount && incCount) summary += ` (${expCount} expenses, ${incCount} income)`;
        summary += ". Tap below to add them.";
        const taggedRows = rows.map((r) => (acct ? { ...r, account: acct } : r));
        setMsgs((m) => {
          const next = [...m, { role: "assistant", text: summary, batch: taggedRows }];
          const hist = next.map((x) => ({ role: x.role, text: x.text })).slice(-20);
          saveConfig({ ...config, coachHistory: hist });
          return next;
        });
      }
    } catch (e) {
      setMsgs((m) => [...m, { role: "assistant", text: "Sorry, I couldn't read that file. " + (e.message || "Try again with a different format.") }]);
    } finally {
      setPending(false);
    }
  };

  return (
    <div style={embedded ? { display: "flex", flexDirection: "column" } : { ...card, padding: "18px 18px 16px", display: "flex", flexDirection: "column" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 12 }}>
        {!embedded && <><ISpark size={18} color={T.orange} /><span style={{ fontSize: 17, fontWeight: 700 }}>Syrup</span></>}
        <span style={{ marginLeft: embedded ? 0 : "auto", fontSize: 11, color: T.faint, fontWeight: 600 }}>{config.autoAdjust ? "Auto-adjust on" : "Suggestions only"}</span>
        {msgs.length > 0 && <button className="press" onClick={() => { setMsgs([]); saveConfig({ ...config, coachHistory: [] }); }} style={{ marginLeft: embedded ? "auto" : 10, background: "none", border: "none", color: T.faint, fontSize: 12.5, fontWeight: 600, cursor: "pointer" }}>Clear</button>}
      </div>

      {msgs.length === 0 && !pending && (
        <div style={{ fontSize: 14, color: T.dim, lineHeight: 1.5, marginBottom: 14 }}>
          A real read on your month — what ran hot, how it lands on {metrics.primary ? metrics.primary.name : "your goals"}, and what to do next. Start with one of these, or just tell me what's on your mind.
        </div>
      )}

      {(msgs.length > 0 || pending) && (
        <div ref={scrollRef} style={{ maxHeight: embedded ? "52vh" : 340, overflowY: "auto", display: "flex", flexDirection: "column", gap: 10, marginBottom: 12, paddingRight: 2 }}>
          {msgs.map((m, i) => (
            <div key={i} className="msgin" style={{ display: "flex", justifyContent: m.role === "user" ? "flex-end" : "flex-start" }}>
              <div style={{ maxWidth: "86%" }}>
                <div style={{
                  fontSize: 14.5, lineHeight: 1.5, whiteSpace: "pre-wrap", padding: "10px 13px", borderRadius: 16,
                  background: m.role === "user" ? T.blue : T.fill,
                  color: m.role === "user" ? "#fff" : T.text,
                  borderBottomRightRadius: m.role === "user" ? 5 : 16,
                  borderBottomLeftRadius: m.role === "user" ? 16 : 5,
                }}>
                  {m.role === "user" ? m.text : <RichText text={m.text} />}
                </div>
                {m.role === "assistant" && m.batch && m.batch.length > 0 && !m.batchAdded && (
                  <div style={{ marginTop: 8, padding: "12px 14px", borderRadius: 14, background: T.green + "1A", border: `1px solid ${T.green}44` }}>
                    <button className="press" onClick={() => {
                      if (onStatementBatch) onStatementBatch(m.batch);
                      setMsgs((mm) => mm.map((x, j) => (j === i ? { ...x, batchAdded: true } : x)));
                    }} style={{ ...pillSm, background: T.green, color: "#fff", width: "100%" }}>Add {m.batch.length} transaction{m.batch.length === 1 ? "" : "s"}</button>
                  </div>
                )}
                {m.role === "assistant" && m.batchAdded && (
                  <div style={{ marginTop: 8, display: "flex", alignItems: "center", gap: 6, color: T.green, fontSize: 13.5, fontWeight: 600 }}><ICheck size={16} color={T.green} sw={2.6} /> Added</div>
                )}
                {m.role === "assistant" && m.acts && m.acts.map((it, ai) => (
                  <div key={ai} style={{ marginTop: 8, padding: "12px 14px", borderRadius: 14, background: T.waffle + "1A", border: `1px solid ${T.waffle}66` }}>
                    <div style={{ fontSize: 13.5, fontWeight: 600, color: T.text, marginBottom: 9 }}>{it.a.summary || "Apply this change?"}</div>
                    {it.applied
                      ? <div style={{ display: "flex", alignItems: "center", gap: 6, color: T.green, fontSize: 13.5, fontWeight: 600 }}><ICheck size={16} color={T.green} sw={2.6} /> Applied</div>
                      : <div style={{ display: "flex", gap: 8 }}>
                          <button className="press" onClick={() => applyAction(i, ai)} style={{ ...pillSm, background: T.blue }}>Apply</button>
                          <button className="press" onClick={() => setMsgs((mm) => mm.map((x, j) => (j === i ? { ...x, acts: x.acts.filter((_, k) => k !== ai) } : x)))} style={{ ...pillSm, background: T.fill2, color: T.dim }}>Dismiss</button>
                        </div>}
                  </div>
                ))}
              </div>
            </div>
          ))}
          {pending && (
            <div className="msgin" style={{ display: "flex", justifyContent: "flex-start" }}>
              <div style={{ padding: "12px 14px", borderRadius: 16, borderBottomLeftRadius: 5, background: T.fill, display: "flex", gap: 5 }}>
                {[0, 1, 2].map((d) => <span key={d} className="dot" style={{ width: 7, height: 7, borderRadius: 4, background: T.faint, animationDelay: `${d * 0.16}s` }} />)}
              </div>
            </div>
          )}
        </div>
      )}

      {err && <div style={{ color: T.red, fontSize: 13, marginBottom: 10 }}>Couldn't reach Syrup. Try again.</div>}

      <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginBottom: 12 }}>
        {STARTERS.map((s) => <Chip key={s.label} onClick={() => send(s.prompt)} label={s.label} />)}
      </div>

      <input ref={fileRef} type="file" accept="image/*,.pdf,.csv,.txt" style={{ display: "none" }} onChange={handleFileUpload} />
      <div style={{ display: "flex", gap: 8, alignItems: "flex-end" }}>
        <button className="press" onClick={() => fileRef.current && fileRef.current.click()} disabled={pending} aria-label="Attach file" style={{ width: 44, height: 44, borderRadius: 14, border: "none", cursor: "pointer", background: T.fill2, color: T.dim, display: "grid", placeItems: "center", flexShrink: 0 }}>
          <IAttach size={19} sw={2} color={T.dim} />
        </button>
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(input); } }}
          placeholder="Ask Syrup anything…"
          rows={1}
          style={{ flex: 1, resize: "none", background: T.fill, border: "1px solid transparent", borderRadius: 14, padding: "11px 14px", color: T.text, fontSize: 15, lineHeight: 1.4, maxHeight: 120 }}
        />
        <button className="press" onClick={() => send(input)} disabled={pending || !input.trim()} aria-label="Send" style={{ width: 44, height: 44, borderRadius: 14, border: "none", cursor: input.trim() ? "pointer" : "default", background: input.trim() ? T.blue : T.fill2, color: input.trim() ? "#fff" : T.faint, display: "grid", placeItems: "center", flexShrink: 0 }}>
          {pending ? <ISpin size={18} className="spin" color={input.trim() ? "#fff" : T.faint} /> : <ISend size={19} sw={2} color={input.trim() ? "#fff" : T.faint} />}
        </button>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  ADD SHEET (expense + receipt split + income)                       */
/* ================================================================== */
function AddSheet({ config, metrics, income, tx, colors, onClose, onExpense, onExpenseBatch, onStatementBatch, onIncome }) {
  const catNames = (config.categories || []).map((c) => c.name);
  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(catNames[0] || "Other"); const [sub, setSub] = useState("");
  const [source, setSource] = useState("");
  const [repeat, setRepeat] = useState("");
  // Tax treatment: default from the source's saved setting, else from profile
  // (self-employed/side-business → owes tax; pure W-2 → withheld).
  const srcDefaultTaxable = (() => {
    const prof = config.profile || {};
    return prof.employment === "self" || prof.employment === "1099" || prof.sideBusiness === true;
  })();
  const [taxable, setTaxable] = useState(srcDefaultTaxable);
  const [receiptItems, setReceiptItems] = useState(null);
  const [receiptTax, setReceiptTax] = useState(0);
  const [stmtRows, setStmtRows] = useState(null);
  const [stmtProgress, setStmtProgress] = useState(null);
  const [stmtAccount, setStmtAccount] = useState(null);
  const [stmtDiag, setStmtDiag] = useState("");
  const [ai, setAi] = useState(""); const [conf, setConf] = useState(null);
  const fileRef = useRef(); const stmtRef = useRef();
  useEffect(() => { if (type === "expense" && mode === "manual" && who && ai !== "done") { const guess = localGuess(who); setCategory(catNames.includes(guess) ? guess : (catNames[0] || "Other")); } }, [who, type, mode]);
  useEffect(() => { setSub(""); }, [category]);

  const knownSources = [...new Set(income.map((x) => x.source))].slice(0, 5);
  useEffect(() => {
    const saved = (config.incomeSources || {})[source.trim()];
    if (saved && typeof saved.taxable === "boolean") setTaxable(saved.taxable);
  }, [source]);
  const askAI = async () => { if (!who) return; setAi("loading"); try { const g = await aiCategorize(who, amount || 0, catNames); if (g.category && catNames.includes(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) { setAi("error"); } };
  const onReceipt = async (e) => {
    const f = e.target.files && e.target.files[0]; if (!f) return;
    setMode("receipt"); setAi("loading"); setReceiptItems(null); setReceiptTax(0);
    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", catNames);
      if (out.who) setWho(out.who);
      if (out.date) setDate(out.date);
      const its = (Array.isArray(out.items) ? out.items : [])
        .filter((it) => it && (Number(it.price) || 0) > 0)
        .slice(0, 12)
        .map((it) => ({ name: String(it.name || "Item"), price: Number(it.price) || 0, category: catNames.includes(it.category) ? it.category : (catNames[0] || "Other") }));
      const tax = Math.max(0, Number(out.tax) || 0);
      const withTax = applyTaxToItems(its, tax);
      setReceiptTax(tax);
      setReceiptItems(withTax);
      setAi(withTax.length ? "done" : "error");
    } catch (err) { setAi("error"); setReceiptItems([]); }
  };

  const setItem = (i, key, v) => setReceiptItems((arr) => arr.map((it, j) => (j === i ? { ...it, [key]: v } : it)));
  const removeItem = (i) => setReceiptItems((arr) => arr.filter((_, j) => j !== i));
  const addItemRow = () => setReceiptItems((arr) => [...(arr || []), { name: "", price: 0, category: catNames[0] || "Other" }]);

  const onStatement = async (e) => {
    const files = Array.from((e.target.files) || []); if (!files.length) return;
    setMode("statement"); setAi("loading"); setStmtRows(null); setStmtAccount(null); setStmtDiag(""); setStmtProgress(files.length > 1 ? { done: 0, total: files.length } : null);
    const seen = new Set(); const all = []; let anyError = false; let detected = null;
    for (let k = 0; k < files.length; k++) {
      const f = files[k];
      try {
        const nm = (f.name || "").toLowerCase();
        const isImage = (f.type || "").startsWith("image/") || /\.(png|jpe?g|heic|webp|gif)$/.test(nm);
        const isPDF = nm.endsWith(".pdf") || f.type === "application/pdf";
        let outs = []; // one or more parsed results to merge
        if (isImage) {
          outs = [await aiReadStatement(await fileToB64(f), f.type || "image/jpeg", catNames)];
        } else if (isPDF) {
          if (!window.pdfjsLib) { setStmtDiag("The PDF reader isn't loaded on this version. Re-deploy index.html (it needs the pdf.js script), or use a CSV or screenshot."); anyError = true; continue; }
          const text = await extractFileText(f);
          if (text && text.trim().length >= 20) {
            // Text-based PDF — read the extracted text directly.
            outs = [await aiReadStatementText(text, catNames)];
          } else {
            // Scanned/image-only PDF — render pages to images and OCR each with vision.
            const imgs = await pdfPagesToImages(f, 6);
            if (!imgs.length) { setStmtDiag("This PDF has no readable text and its pages couldn't be rendered. Try a CSV export or a screenshot instead."); anyError = true; continue; }
            for (let pi = 0; pi < imgs.length; pi++) {
              try { outs.push(await aiReadStatement(imgs[pi], "image/jpeg", catNames)); } catch (e) { setStmtDiag("Found the pages but Syrup couldn't read them — the image may be low-res. A CSV export reads most reliably."); anyError = true; }
            }
          }
        } else {
          const text = await extractFileText(f); // CSV / txt
          if (!text || text.trim().length < 10) { setStmtDiag("Couldn't read that file's contents. Make sure it's a CSV, PDF, or image."); anyError = true; continue; }
          outs = [await aiReadStatementText(text, catNames)];
        }
        outs.forEach((out) => {
        if (!detected && out && out.account && (out.account.name || out.account.last4)) detected = out.account;
        (Array.isArray(out.tx) ? out.tx : [])
          .filter((r) => r && (Number(r.amount) || 0) > 0)
          .forEach((r) => {
            // Date sanity: accept only YYYY-MM-DD within a sane window (not in the
            // future, not absurdly old). Otherwise fall back to today's date.
            let d = date;
            if (/^\d{4}-\d{2}-\d{2}$/.test(r.date)) {
              const dt = new Date(r.date + "T00:00:00"); const now = new Date();
              const minD = new Date(); minD.setMonth(minD.getMonth() - 18);
              if (!isNaN(dt) && dt <= now && dt >= minD) d = r.date;
            }
            const row = {
              date: d,
              who: String(r.who || "Transaction"),
              amount: Number(r.amount) || 0,
              kind: r.kind === "income" ? "income" : "expense",
              category: catNames.includes(r.category) ? r.category : (catNames[0] || "Other"),
              keep: true,
            };
            const key = `${row.date}|${row.who.toLowerCase()}|${row.amount}|${row.kind}`;
            if (seen.has(key)) return;
            seen.add(key); all.push(row);
          });
        });
      } catch (err) { anyError = true; setStmtDiag((err && err.message) ? ("Error: " + err.message) : "Something went wrong reading that file."); }
      if (files.length > 1) setStmtProgress({ done: k + 1, total: files.length });
    }
    all.sort((a, b) => b.date.localeCompare(a.date));
    const rows = all.slice(0, 120);
    // Flag rows that already exist in stored data (account-independent, normalized merchant).
    const nrm = (s) => String(s || "").toLowerCase().replace(/[#0-9]+\s*$/, "").replace(/\s+/g, " ").trim();
    const existKey = new Set([
      ...(tx || []).map((x) => `${x.date}|${Math.round(x.amount * 100)}|${nrm(x.who)}`),
      ...(income || []).map((x) => `${x.date}|${Math.round(x.amount * 100)}|${nrm(x.source)}`),
    ]);
    const flagged = rows.map((r) => {
      const dupe = existKey.has(`${r.date}|${Math.round(r.amount * 100)}|${nrm(r.who)}`);
      return dupe ? { ...r, dupe: true, keep: false } : r;
    });
    setStmtRows(flagged);
    setStmtProgress(null);
    // Seed the account label from detection (user can edit before saving).
    setStmtAccount({ name: (detected && detected.name) || "", last4: (detected && detected.last4) || "", kind: (detected && detected.kind) || "other" });
    setAi(flagged.length ? "done" : "error");
  };
  const setStmt = (i, key, v) => setStmtRows((arr) => arr.map((r, j) => (j === i ? { ...r, [key]: v } : r)));
  const removeStmt = (i) => setStmtRows((arr) => arr.filter((_, j) => j !== i));
  const keptStmt = (stmtRows || []).filter((r) => r.keep && r.who.trim() && (Number(r.amount) || 0) > 0);
  const submitStatement = () => {
    if (!keptStmt.length) return;
    const acctLabel = stmtAccount && stmtAccount.name.trim()
      ? stmtAccount.name.trim() + (stmtAccount.last4 ? ` ••${stmtAccount.last4}` : "")
      : "";
    onStatementBatch(keptStmt.map(({ keep, ...r }) => (acctLabel ? { ...r, account: acctLabel } : r)));
  };
  const receiptTotal = (receiptItems || []).reduce((a, b) => a + (Number(b.price) || 0), 0);
  const validItems = (receiptItems || []).filter((it) => it.name.trim() && (Number(it.price) || 0) > 0);

  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, repeat: repeat || undefined, taxable }); }
    else { if (!who.trim()) return; onExpense({ who: who.trim(), amount: amt, date, category, sub: sub || undefined, repeat: repeat || undefined }); }
  };
  const submitReceipt = () => {
    if (!validItems.length) return;
    onExpenseBatch(validItems.map((it) => ({ who: it.name.trim(), amount: Number(it.price), date, category: it.category })), who.trim());
  };

  return (
    <Sheet onClose={onClose} zIndex={50} scroll maxHeight="94vh">
        {/* 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="Paycheck, tips, side gig…" 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>
            <div style={{ marginBottom: 14 }}>
              <div style={lbl}>Taxes on this income</div>
              <div style={{ display: "flex", gap: 7, marginTop: 2 }}>
                <Chip active={!taxable} onClick={() => setTaxable(false)} label="Already withheld" />
                <Chip active={taxable} onClick={() => setTaxable(true)} label="I owe tax on it" />
              </div>
              <div style={{ fontSize: 12, color: T.faint, marginTop: 8, lineHeight: 1.4 }}>{taxable ? "Waffle will suggest setting aside a slice for taxes (1099, self-employed, side gig)." : "A regular W-2 paycheck with taxes already taken out — no set-aside needed."}</div>
            </div>
            <div style={{ marginBottom: 14 }}>
              <div style={lbl}>Repeat</div>
              <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginTop: 2 }}>
                <Chip active={!repeat} onClick={() => setRepeat("")} label="Once" />
                <Chip active={repeat === "weekly"} onClick={() => setRepeat("weekly")} label="Weekly" />
                <Chip active={repeat === "biweekly"} onClick={() => setRepeat("biweekly")} label="Every 2 weeks" />
                <Chip active={repeat === "monthly"} onClick={() => setRepeat("monthly")} label="Monthly" />
              </div>
            </div>
            <button className="press" onClick={submit} style={{ ...pill, width: "100%", background: T.green }}>Add income</button>
          </>
        ) : (
          <>
            {/* Hero: scan a receipt — the friction-killer for the no-bank-connection flow */}
            {mode === "manual" && (
              <button className="press" onClick={() => fileRef.current && fileRef.current.click()} style={{ width: "100%", border: "none", cursor: "pointer", background: `linear-gradient(135deg, ${T.waffle}, ${T.orange})`, color: "#fff", borderRadius: 16, padding: "16px 18px", display: "flex", alignItems: "center", gap: 14, marginBottom: 12, boxShadow: `0 8px 20px ${T.waffle}44` }}>
                <span style={{ width: 42, height: 42, borderRadius: 21, background: "rgba(255,255,255,.22)", display: "grid", placeItems: "center", flexShrink: 0 }}><ICamera size={22} color="#fff" /></span>
                <div style={{ textAlign: "left", flex: 1 }}>
                  <div style={{ fontSize: 16, fontWeight: 800, letterSpacing: "-0.01em" }}>Scan a receipt</div>
                  <div style={{ fontSize: 12.5, opacity: .92 }}>Snap it — Syrup splits it by item for you</div>
                </div>
                <IChevR size={18} color="#fff" sw={2.4} />
              </button>
            )}
            {mode === "manual" && (
              <button className="press" onClick={() => stmtRef.current && stmtRef.current.click()} style={{ width: "100%", border: "none", cursor: "pointer", background: T.fill2, color: T.text, borderRadius: 16, padding: "14px 18px", display: "flex", alignItems: "center", gap: 14, marginBottom: 12 }}>
                <span style={{ width: 42, height: 42, borderRadius: 21, background: T.waffle + "22", display: "grid", placeItems: "center", flexShrink: 0 }}><IWorth size={20} color={T.waffle} /></span>
                <div style={{ textAlign: "left", flex: 1 }}>
                  <div style={{ fontSize: 16, fontWeight: 800, letterSpacing: "-0.01em" }}>Import from your bank</div>
                  <div style={{ fontSize: 12.5, color: T.dim }}>Screenshots, CSV or PDF statements — no login</div>
                </div>
                <IChevR size={18} color={T.faint} sw={2.4} />
              </button>
            )}
            <div style={{ display: "flex", alignItems: "center", gap: 10, margin: "2px 0 12px" }}>
              <div style={{ flex: 1, height: 1, background: T.sep }} />
              <span style={{ fontSize: 11.5, color: T.faint, fontWeight: 600 }}>or enter by hand</span>
              <div style={{ flex: 1, height: 1, background: T.sep }} />
            </div>
            <input ref={fileRef} type="file" accept="image/*" capture="environment" onChange={onReceipt} style={{ display: "none" }} />
            <input ref={stmtRef} type="file" accept="image/*,.csv,.pdf,text/csv,application/pdf" multiple onChange={onStatement} style={{ display: "none" }} />

            {mode === "manual" ? (
              <>
                <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}>{catNames.map((c) => <option key={c} value={c}>{c}</option>)}</select>
                </div>
                {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 : (colors[category] || T.blue), borderRadius: 3 }} /></div>
                  </div>
                )}
                    <div style={{ marginBottom: 14 }}>
                      <div style={lbl}>Repeat</div>
                      <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginTop: 2 }}>
                    <Chip active={!repeat} onClick={() => setRepeat("")} label="Once" />
                    <Chip active={repeat === "weekly"} onClick={() => setRepeat("weekly")} label="Weekly" />
                    <Chip active={repeat === "biweekly"} onClick={() => setRepeat("biweekly")} label="Every 2 weeks" />
                    <Chip active={repeat === "monthly"} onClick={() => setRepeat("monthly")} label="Monthly" />
                      </div>
                </div>
                <button className="press" onClick={submit} style={{ ...pill, width: "100%" }}>Add expense</button>
              </>
            ) : mode === "statement" ? (
              <>
                <div style={{ ...card, padding: 14, marginBottom: 14, display: "flex", alignItems: "center", gap: 11 }}>
                  {ai === "loading" ? <ISpin size={18} className="spin" color={T.waffle} /> : <IWorth size={18} color={T.waffle} />}
                  <span style={{ fontSize: 13.5, color: T.dim, flex: 1 }}>{ai === "loading" ? (stmtProgress ? `Reading file ${stmtProgress.done} of ${stmtProgress.total}…` : "Reading your statement…") : ai === "done" ? "Review below — uncheck anything you don't want, then save." : ai === "error" ? (stmtDiag || "Couldn't read that — try a CSV export, a clearer screenshot, or a text-based PDF.") : "Pick screenshots, CSV, or PDF statements."}</span>
                  <button className="press" onClick={() => stmtRef.current && stmtRef.current.click()} style={{ ...pillSm }}>Photo</button>
                </div>
                {ai === "done" && stmtRows && (
                  <>
                    {stmtAccount && (
                      <div style={{ ...card, padding: "12px 14px", marginBottom: 12 }}>
                        <div style={{ fontSize: 11.5, color: T.faint, fontWeight: 600, marginBottom: 7 }}>WHICH ACCOUNT IS THIS?{(stmtAccount.name || stmtAccount.last4) ? " · detected" : ""}</div>
                        <div style={{ display: "flex", gap: 8 }}>
                          <input value={stmtAccount.name} onChange={(e) => setStmtAccount((a) => ({ ...a, name: e.target.value }))} placeholder="e.g. Chase Checking, Amex" style={{ ...inp, padding: "9px 11px", fontSize: 14.5, flex: 1 }} />
                          <input value={stmtAccount.last4} onChange={(e) => setStmtAccount((a) => ({ ...a, last4: e.target.value.replace(/[^0-9]/g, "").slice(0, 4) }))} placeholder="••1234" inputMode="numeric" style={{ ...inp, padding: "9px 11px", fontSize: 14.5, width: 80, textAlign: "center" }} />
                        </div>
                        <div style={{ fontSize: 11.5, color: T.faint, marginTop: 7, lineHeight: 1.4 }}>Naming the account keeps each card or bank separate in your history.</div>
                      </div>
                    )}
                    {stmtRows.some((r) => r.dupe) && (
                      <div className="pop" style={{ padding: "12px 14px", borderRadius: 14, background: T.orange + "14", border: `1px solid ${T.orange}44`, marginBottom: 12 }}>
                        <div style={{ fontSize: 13.5, fontWeight: 700, color: T.orange, marginBottom: 2 }}>⚠️ {stmtRows.filter((r) => r.dupe).length} look like you've already added them</div>
                        <div style={{ fontSize: 12.5, color: T.dim, lineHeight: 1.45 }}>They're unchecked below so you won't double up. Tick one only if it's genuinely a separate charge.</div>
                      </div>
                    )}
                    <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 14 }}>
                      {stmtRows.map((r, i) => (
                        <div key={i} style={{ ...card, padding: "12px 13px", opacity: r.keep ? 1 : 0.5, border: r.dupe ? `1px solid ${T.orange}55` : "none" }}>
                          {/* Row 1: checkbox + merchant (full width) + remove */}
                          <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: r.dupe ? 6 : 9 }}>
                            <button className="press" onClick={() => setStmt(i, "keep", !r.keep)} aria-label="toggle" style={{ width: 24, height: 24, borderRadius: 7, border: `2px solid ${r.keep ? T.waffle : T.faint}`, background: r.keep ? T.waffle : "transparent", display: "grid", placeItems: "center", flexShrink: 0, cursor: "pointer" }}>{r.keep && <ICheck size={14} color="#fff" sw={3} />}</button>
                            <input value={r.who} onChange={(e) => setStmt(i, "who", e.target.value)} style={{ ...inp, padding: "8px 10px", fontSize: 15, flex: 1, minWidth: 0 }} />
                            <button className="press" onClick={() => removeStmt(i)} aria-label="remove" style={{ background: "none", border: "none", color: T.faint, cursor: "pointer", padding: 4, flexShrink: 0 }}><IX size={17} /></button>
                          </div>
                          {r.dupe && <div style={{ fontSize: 11.5, color: T.orange, fontWeight: 600, paddingLeft: 34, marginBottom: 7 }}>Already in your history</div>}
                          {/* Row 2: amount + date */}
                          <div style={{ display: "flex", gap: 8, marginBottom: 9, paddingLeft: 34 }}>
                            <div style={{ position: "relative", flex: 1 }}><span style={{ position: "absolute", left: 11, top: 9, color: T.faint, fontSize: 14 }}>$</span><input value={r.amount} onChange={(e) => setStmt(i, "amount", Number(e.target.value.replace(/[^0-9.]/g, "")) || 0)} inputMode="decimal" style={{ ...inp, padding: "8px 10px 8px 22px", fontSize: 15, fontVariantNumeric: "tabular-nums" }} /></div>
                            <input type="date" value={r.date} onChange={(e) => setStmt(i, "date", e.target.value)} style={{ ...inp, padding: "8px 10px", fontSize: 14, color: T.dim, flex: 1, fontFamily: SF }} />
                          </div>
                          {/* Row 3: kind toggle + category */}
                          <div style={{ display: "flex", alignItems: "center", gap: 8, paddingLeft: 34 }}>
                            <div style={{ display: "flex", background: T.fill2, borderRadius: 9, padding: 2, flexShrink: 0 }}>
                              <button className="press" onClick={() => setStmt(i, "kind", "expense")} style={{ border: "none", cursor: "pointer", padding: "6px 11px", borderRadius: 7, fontSize: 12.5, fontWeight: 600, background: r.kind === "expense" ? T.ink : "transparent", color: r.kind === "expense" ? T.bg : T.dim }}>Expense</button>
                              <button className="press" onClick={() => setStmt(i, "kind", "income")} style={{ border: "none", cursor: "pointer", padding: "6px 11px", borderRadius: 7, fontSize: 12.5, fontWeight: 600, background: r.kind === "income" ? T.green : "transparent", color: r.kind === "income" ? "#fff" : T.dim }}>Income</button>
                            </div>
                            {r.kind === "expense" ? (
                              <select value={r.category} onChange={(e) => setStmt(i, "category", e.target.value)} style={{ flex: 1, minWidth: 0, border: "none", background: T.fill2, color: T.text, borderRadius: 9, padding: "8px 10px", fontSize: 13.5, fontFamily: SF }}>
                                {catNames.map((c) => <option key={c} value={c}>{c}</option>)}
                              </select>
                            ) : <div style={{ flex: 1, fontSize: 12.5, color: T.faint, paddingLeft: 2 }}>Counts as income</div>}
                          </div>
                        </div>
                      ))}
                    </div>
                    <button className="press" onClick={submitStatement} disabled={!keptStmt.length} style={{ ...pill, width: "100%", opacity: keptStmt.length ? 1 : 0.5 }}>{keptStmt.length ? `Add ${keptStmt.length} transaction${keptStmt.length === 1 ? "" : "s"}` : "Add transactions"}</button>
                  </>
                )}
                {ai !== "done" && ai !== "loading" && (
                  <div style={{ fontSize: 12.5, color: T.faint, lineHeight: 1.5, padding: "0 2px" }}>Add screenshots, or a CSV / PDF statement exported from your bank. You can select several at once and mix months — Syrup reads each, detects the account, and merges them. Nothing is sent to your bank and no login is needed. (Scanned/image-only PDFs may not read — a CSV export or screenshot works best.)</div>
                )}
              </>
            ) : (
              <>
                <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" ? "Check the split below, then save." : ai === "error" ? "Couldn't read that one — snap again or add items manually." : "Tap to photograph your receipt."}</span>
                  <button className="press" onClick={() => fileRef.current && fileRef.current.click()} style={{ ...pillSm }}>Photo</button>
                </div>

                {receiptItems && (
                  <>
                    <div style={{ display: "flex", gap: 12, marginBottom: 4 }}>
                      <Field label="Store" flex><input value={who} onChange={(e) => setWho(e.target.value)} placeholder="Store" style={inp} /></Field>
                      <Field label="Date" flex><input type="date" value={date} onChange={(e) => setDate(e.target.value)} style={inp} /></Field>
                    </div>
                    <div style={lbl}>Items · {(receiptItems || []).length} · each goes to its own category</div>
                    {receiptTax > 0 && <div style={{ fontSize: 12, color: T.faint, marginTop: -2, marginBottom: 8, paddingLeft: 2 }}>{fmt(receiptTax)} tax spread across items, so prices include their share.</div>}
                    <div style={{ ...card, padding: "4px 0", marginTop: 8, marginBottom: 12 }}>
                      {(receiptItems || []).map((it, i) => (
                        <div key={i} style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 12px", borderBottom: i === receiptItems.length - 1 ? "none" : `1px solid ${T.sep}` }}>
                          <span style={{ width: 9, height: 9, borderRadius: 5, background: colors[it.category] || T.faint, flexShrink: 0 }} />
                          <div style={{ flex: 1, minWidth: 0 }}>
                            <input value={it.name} onChange={(e) => setItem(i, "name", e.target.value)} placeholder="Item" style={{ ...inp, padding: "6px 8px", fontSize: 14.5, background: "transparent", borderRadius: 8 }} />
                            <select value={it.category} onChange={(e) => setItem(i, "category", e.target.value)} style={{ border: "none", background: "transparent", color: T.dim, fontSize: 12.5, fontWeight: 600, paddingLeft: 8, marginTop: -2 }}>{catNames.map((c) => <option key={c} value={c}>{c}</option>)}</select>
                          </div>
                          <div style={{ position: "relative", width: 76, flexShrink: 0 }}><span style={{ position: "absolute", left: 8, top: 8, color: T.faint, fontSize: 13 }}>$</span><input value={it.price} onChange={(e) => setItem(i, "price", e.target.value.replace(/[^0-9.]/g, ""))} inputMode="decimal" style={{ ...inp, padding: "7px 8px 7px 18px", fontSize: 14, textAlign: "right", fontVariantNumeric: "tabular-nums" }} /></div>
                          <button className="press" onClick={() => removeItem(i)} aria-label="Remove" style={{ background: "none", border: "none", cursor: "pointer", color: T.faint, display: "flex", flexShrink: 0 }}><IX size={17} sw={2} /></button>
                        </div>
                      ))}
                    </div>
                    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
                      <button className="press" onClick={addItemRow} style={{ display: "flex", alignItems: "center", gap: 5, background: "none", border: "none", color: T.blue, fontSize: 14, fontWeight: 600, cursor: "pointer" }}><IPlus size={16} color={T.blue} sw={2.2} /> Add item</button>
                      <span style={{ fontSize: 15, fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>Total {fmt(receiptTotal)}</span>
                    </div>
                    <button className="press" onClick={submitReceipt} disabled={!validItems.length} style={{ ...pill, width: "100%", opacity: validItems.length ? 1 : 0.5 }}>{validItems.length ? `Add ${validItems.length} ${validItems.length === 1 ? "expense" : "expenses"}` : "Add expenses"}</button>
                  </>
                )}
              </>
            )}
          </>
        )}
    </Sheet>
  );
}

/* ================================================================== */
/*  CONFIRM                                                            */
/* ================================================================== */
function ConfirmCard({ data, goal, colors, onClose, onContribute }) {
  const goalName = goal ? goal.name : "your goal";
  const wrap = (children) => (
    <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 60, background: T.overlay, 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" }}>{children}</div>
    </div>
  );

  if (data.kind === "income") {
    const { rec, monthIncome, suggestSave } = data;
    return wrap(<>
      <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 {goalName} 🧇</div>
      {suggestSave > 0 && onContribute && (
        <button className="press" onClick={() => onContribute(suggestSave)} style={{ ...pill, width: "100%", marginTop: 18, background: T.waffle, color: T.ink }}>Set aside {fmt(suggestSave)} to savings</button>
      )}
      <button className="press" onClick={onClose} style={{ ...pill, width: "100%", marginTop: 10, background: suggestSave > 0 ? T.fill2 : T.green, color: suggestSave > 0 ? T.text : "#fff" }}>{suggestSave > 0 ? "Not now" : "Nice"}</button>
    </>);
  }

  if (data.kind === "contribution") {
    const { amount, adds, unassignedAdded } = data;
    return wrap(<>
      <div style={{ width: 52, height: 52, borderRadius: 26, margin: "0 auto 12px", background: T.green + "1A", display: "grid", placeItems: "center" }}><ICheck size={26} color={T.green} sw={2.4} /></div>
      <div style={{ fontSize: 19, fontWeight: 700 }}>{fmt(amount)} set aside</div>
      <div style={{ textAlign: "left", marginTop: 14, display: "flex", flexDirection: "column", gap: 8 }}>
        {adds.map((a) => (
          <div key={a.id} style={{ display: "flex", justifyContent: "space-between", fontSize: 14 }}><span style={{ color: T.dim }}>{a.name}</span><span style={{ fontWeight: 700, color: T.green, fontVariantNumeric: "tabular-nums" }}>+{fmt(a.amount)}</span></div>
        ))}
        {unassignedAdded > 0 && <div style={{ display: "flex", justifyContent: "space-between", fontSize: 14 }}><span style={{ color: T.dim }}>Unassigned</span><span style={{ fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>+{fmt(unassignedAdded)}</span></div>}
      </div>
      <button className="press" onClick={onClose} style={{ ...pill, width: "100%", marginTop: 18, background: T.green }}>Done</button>
    </>);
  }

  if (data.kind === "tax") {
    return wrap(<>
      <div style={{ width: 52, height: 52, borderRadius: 26, margin: "0 auto 12px", background: T.green + "1A", display: "grid", placeItems: "center" }}><ICheck size={26} color={T.green} sw={2.4} /></div>
      <div style={{ fontSize: 19, fontWeight: 700 }}>{fmt(data.amount)} set aside for taxes</div>
      <div style={{ fontSize: 14, color: T.dim, marginTop: 8, lineHeight: 1.45 }}>Tax reserve is now {fmt(data.total)}. Future-you, at tax time, says thanks.</div>
      <button className="press" onClick={onClose} style={{ ...pill, width: "100%", marginTop: 18, background: T.green }}>Done</button>
    </>);
  }

  if (data.kind === "imported") {
    return wrap(<>
      <div style={{ width: 52, height: 52, borderRadius: 26, margin: "0 auto 12px", background: T.green + "1A", display: "grid", placeItems: "center" }}><ICheck size={26} color={T.green} sw={2.4} /></div>
      <div style={{ fontSize: 19, fontWeight: 700 }}>Backup restored</div>
      <div style={{ fontSize: 14, color: T.dim, marginTop: 8, lineHeight: 1.45 }}>{data.count} {data.count === 1 ? "entry" : "entries"} loaded. Your goals, budgets, and plan are back.</div>
      <button className="press" onClick={onClose} style={{ ...pill, width: "100%", marginTop: 18, background: T.green }}>Done</button>
    </>);
  }

  if (data.kind === "statement") {
    const { expCount, incCount, total, incTotal } = data;
    return wrap(<>
      <div style={{ width: 52, height: 52, borderRadius: 26, margin: "0 auto 12px", background: T.green + "1A", display: "grid", placeItems: "center" }}><ICheck size={26} color={T.green} sw={2.4} /></div>
      <div style={{ fontSize: 19, fontWeight: 700 }}>Imported {expCount + incCount} transaction{expCount + incCount === 1 ? "" : "s"}</div>
      <div style={{ fontSize: 14, color: T.dim, marginTop: 8, lineHeight: 1.45 }}>
        {data.account ? <><b style={{ color: T.text }}>{data.account}</b> · </> : null}
        {expCount > 0 && <>{expCount} expense{expCount === 1 ? "" : "s"} ({fmt(total)}){incCount > 0 ? " and " : "."}</>}
        {incCount > 0 && <>{incCount} income deposit{incCount === 1 ? "" : "s"} ({fmt(incTotal)}).</>}
        {" "}All added straight from your statement — no bank login.
        {data.dupes > 0 && <><br /><span style={{ color: T.faint }}>Skipped {data.dupes} you'd already added.</span></>}
      </div>
      <button className="press" onClick={onClose} style={{ ...pill, width: "100%", marginTop: 18, background: T.green }}>Done</button>
    </>);
  }

  if (data.kind === "batch") {
    const { count, total, catCount, merchant } = data;
    return wrap(<>
      <div style={{ width: 52, height: 52, borderRadius: 26, margin: "0 auto 12px", background: T.green + "1A", display: "grid", placeItems: "center" }}><ICheck size={26} color={T.green} sw={2.4} /></div>
      <div style={{ fontSize: 19, fontWeight: 700 }}>{fmt(total)} logged</div>
      <div style={{ fontSize: 14, color: T.dim, marginTop: 8, lineHeight: 1.45 }}>{count} {count === 1 ? "item" : "items"}{merchant ? ` from ${merchant}` : ""} split across {catCount} {catCount === 1 ? "category" : "categories"}. Each budget's been updated.</div>
      <button className="press" onClick={onClose} style={{ ...pill, width: "100%", marginTop: 18, background: T.green }}>Done</button>
    </>);
  }

  const { rec, spent, budget } = data; const remaining = budget - spent; const over = remaining < 0; const pct = budget ? Math.min(100, (spent / budget) * 100) : 0;
  return wrap(<>
    <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 : (colors[rec.category] || T.blue), 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 ${goalName} 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>
  </>);
}

/* ================================================================== */
/*  PROFILE — settings hub: appearance, display, coach, account        */
/* ================================================================== */
function ProfileView({ config, saveConfig, accountEmail, onSignOut, onExport, onImport, onDeleteAccount, onEditProfile, onReplayTour, onFindDupes, onRemoveDupes }) {
  const prefs = config.prefs || DEFAULT_CONFIG.prefs;
  const setPref = (k, v) => saveConfig({ ...config, prefs: { ...prefs, [k]: v } });
  const [name, setName] = useState(prefs.displayName || "");
  const importRef = useRef(); const [pendingImport, setPendingImport] = useState(null); const [importErr, setImportErr] = useState("");
  const [delStage, setDelStage] = useState(0); const [delBusy, setDelBusy] = useState(false); const [delErr, setDelErr] = useState("");
  const [dupScan, setDupScan] = useState(null); const [dupDone, setDupDone] = useState(null);
  const runDelete = async () => { setDelBusy(true); setDelErr(""); try { await onDeleteAccount(); } catch (e) { setDelErr((e && e.message) || "Couldn't delete the account."); setDelBusy(false); } };
  const onPickFile = (e) => {
    const f = e.target.files && e.target.files[0]; if (!f) return; setImportErr("");
    const r = new FileReader();
    r.onload = () => { try { const parsed = JSON.parse(r.result); const data = parsed && parsed.data ? parsed.data : parsed; if (!data || !data.config) throw 0; setPendingImport(data); } catch (_) { setImportErr("That doesn't look like a Waffle backup."); } };
    r.onerror = () => setImportErr("Couldn't read that file."); r.readAsText(f); e.target.value = "";
  };
  const initial = (prefs.displayName || accountEmail || "?").trim().charAt(0).toUpperCase();

  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
      {/* Identity */}
      <div style={{ ...card, padding: "22px", display: "flex", alignItems: "center", gap: 16 }}>
        <div style={{ width: 56, height: 56, borderRadius: 28, background: T.waffle + "26", color: T.waffle, display: "grid", placeItems: "center", fontSize: 24, fontWeight: 800, flexShrink: 0 }}>{initial}</div>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 19, fontWeight: 700, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{prefs.displayName || "Your profile"}</div>
          <div style={{ fontSize: 13.5, color: T.dim, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{accountEmail}</div>
        </div>
      </div>

      {/* Appearance */}
      <div>
        <div style={grpLbl}>APPEARANCE</div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 8 }}>
          <div style={{ fontSize: 15.5, fontWeight: 600, marginBottom: 12 }}>Theme</div>
          <div style={{ display: "flex", background: T.fill2, borderRadius: 10, padding: 2 }}>
            <Seg active={(prefs.theme || "auto") === "light"} onClick={() => setPref("theme", "light")} label="Light" />
            <Seg active={(prefs.theme || "auto") === "dark"} onClick={() => setPref("theme", "dark")} label="Dark" />
            <Seg active={(prefs.theme || "auto") === "auto"} onClick={() => setPref("theme", "auto")} label="Auto" />
          </div>
          <div style={{ fontSize: 12.5, color: T.faint, marginTop: 10 }}>Auto follows your device's light/dark setting.</div>
        </div>
      </div>

      {/* Display */}
      <div>
        <div style={grpLbl}>DISPLAY</div>
        <div style={{ ...card, padding: "2px 0", marginTop: 8 }}>
          <Row>
            <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500 }}>Opening tab</span>
            <select value={prefs.defaultTab || "home"} onChange={(e) => setPref("defaultTab", e.target.value)} style={{ ...inp, width: "auto", padding: "8px 10px", fontSize: 14 }}>
              <option value="home">Summary</option><option value="plan">Plan</option><option value="income">Income</option><option value="insights">Insights</option><option value="setup">Budgets</option>
            </select>
          </Row>
          <Row>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 15.5, fontWeight: 500 }}>Hide spending score</div>
              <div style={{ fontSize: 12.5, color: T.faint }}>Removes the score ring from Insights</div>
            </div>
            <Toggle on={!!prefs.hideScore} onChange={(v) => setPref("hideScore", v)} />
          </Row>
          <Row last>
            <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500 }}>Currency symbol</span>
            <div style={{ display: "flex", gap: 7 }}>{["$", "€", "£", "¥"].map((s) => <Chip key={s} active={(prefs.currency || "$") === s} onClick={() => setPref("currency", s)} label={s} />)}</div>
          </Row>
        </div>
      </div>

      {/* Coach */}
      <div>
        <div style={grpLbl}>SYRUP</div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 8 }}>
          <div style={{ fontSize: 15.5, fontWeight: 600, marginBottom: 4 }}>Coaching tone</div>
          <div style={{ fontSize: 12.5, color: T.faint, marginBottom: 12 }}>How blunt Syrup is with you.</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
            <OptionRow active={(prefs.coachTone || "balanced") === "gentle"} onClick={() => setPref("coachTone", "gentle")} title="Gentle" sub="Encouraging, eases into hard truths" />
            <OptionRow active={(prefs.coachTone || "balanced") === "balanced"} onClick={() => setPref("coachTone", "balanced")} title="Balanced" sub="Warm but honest" />
            <OptionRow active={(prefs.coachTone || "balanced") === "blunt"} onClick={() => setPref("coachTone", "blunt")} title="Blunt" sub="Straight to the point, no sugar" />
          </div>
          <div style={{ marginTop: 16, paddingTop: 14, borderTop: `1px solid ${T.sep}`, display: "flex", alignItems: "center", gap: 14 }}>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 15.5, fontWeight: 600 }}>Auto-adjust by default</div>
              <div style={{ fontSize: 12.5, color: T.faint, marginTop: 3, lineHeight: 1.4 }}>Let Syrup propose one-tap changes. Nothing applies without your confirmation.</div>
            </div>
            <Toggle on={!!config.autoAdjust} onChange={(v) => saveConfig({ ...config, autoAdjust: v })} />
          </div>
        </div>
      </div>

      {/* Name */}
      <div>
        <div style={grpLbl}>NAME</div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 8, display: "flex", gap: 10, alignItems: "center" }}>
          <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Display name" style={{ ...inp, flex: 1 }} />
          <button className="press" onClick={() => setPref("displayName", name.trim())} disabled={name.trim() === (prefs.displayName || "")} style={{ ...pillSm, opacity: name.trim() === (prefs.displayName || "") ? 0.5 : 1 }}>Save</button>
        </div>
      </div>

      {/* About you */}
      <div>
        <div style={grpLbl}>ABOUT YOU</div>
        <div style={{ ...card, padding: "2px 0", marginTop: 8 }}>
          <Row last onClick={onEditProfile}>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 15.5, fontWeight: 500 }}>Your profile</div>
              <div style={{ fontSize: 12.5, color: T.faint, marginTop: 2 }}>{(() => { const p = config.profile || {}; const emp = (EMPLOYMENT_OPTS.find((o) => o.id === p.employment) || {}).title || "—"; return `${emp}${p.sideBusiness ? " · side gig" : ""}`; })()}</div>
            </div>
            <IChevR size={18} color={T.faint} sw={2} />
          </Row>
        </div>
        <div style={{ fontSize: 12, color: T.faint, padding: "8px 6px 0", lineHeight: 1.45 }}>Tailors your tax set-aside and which money lessons you see.</div>
      </div>

      {/* Help */}
      <div>
        <div style={grpLbl}>HELP</div>
        <div style={{ ...card, padding: "2px 0", marginTop: 8 }}>
          <Row last onClick={onReplayTour}>
            <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500 }}>Replay the walkthrough</span>
            <IChevR size={18} color={T.faint} sw={2} />
          </Row>
        </div>
      </div>

      {/* Data */}
      <div>
        <div style={grpLbl}>DATA</div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 8 }}>
          <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.5, marginBottom: 14 }}>Your data syncs to your account. Export a local backup file too if you want an offline copy you control.</div>
          <input ref={importRef} type="file" accept="application/json,.json" onChange={onPickFile} style={{ display: "none" }} />
          <div style={{ display: "flex", gap: 10 }}>
            <button className="press" onClick={onExport} style={{ ...pill, flex: 1 }}>Export backup</button>
            <button className="press" onClick={() => importRef.current && importRef.current.click()} style={{ ...pill, flex: 1, background: T.fill2, color: T.text }}>Import backup</button>
          </div>
          {importErr && <div style={{ fontSize: 13, color: T.red, marginTop: 12 }}>{importErr}</div>}
          {pendingImport && (
            <div className="pop" style={{ marginTop: 14, padding: "14px 16px", borderRadius: 14, background: T.orange + "12", border: `1px solid ${T.orange}44` }}>
              <div style={{ fontSize: 14, fontWeight: 700, color: T.orange, marginBottom: 4 }}>Replace everything with this backup?</div>
              <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.45, marginBottom: 12 }}>Overwrites current data — {Array.isArray(pendingImport.tx) ? pendingImport.tx.length : 0} expenses and {Array.isArray(pendingImport.income) ? pendingImport.income.length : 0} income entries restored.</div>
              <div style={{ display: "flex", gap: 8 }}>
                <button className="press" onClick={() => { onImport(pendingImport); setPendingImport(null); }} style={{ ...pillSm, background: T.red }}>Replace</button>
                <button className="press" onClick={() => setPendingImport(null)} style={{ ...pillSm, background: T.fill2, color: T.dim }}>Cancel</button>
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Duplicate cleanup */}
      <div>
        <div style={grpLbl}>CLEAN UP</div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 8 }}>
          {!dupScan && !dupDone && (
            <>
              <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.5, marginBottom: 12 }}>Imported the same statement twice? Scan for exact-repeat transactions and clear them out.</div>
              <button className="press" onClick={() => setDupScan(onFindDupes())} style={{ ...pill, width: "100%", background: T.fill2, color: T.text }}>Scan for duplicates</button>
            </>
          )}
          {dupScan && !dupDone && (
            dupScan.total > 0 ? (
              <div className="pop">
                <div style={{ fontSize: 14, fontWeight: 700, color: T.orange, marginBottom: 4 }}>Found {dupScan.total} duplicate{dupScan.total === 1 ? "" : "s"}</div>
                <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.45, marginBottom: 12 }}>{dupScan.txDupes} expense{dupScan.txDupes === 1 ? "" : "s"} and {dupScan.incDupes} income entr{dupScan.incDupes === 1 ? "y" : "ies"} look like exact repeats (same date, name, amount, account). Removing keeps one of each. Export a backup first if you want a safety net.</div>
                <div style={{ display: "flex", gap: 8 }}>
                  <button className="press" onClick={() => { const r = onRemoveDupes(); setDupDone(r); setDupScan(null); }} style={{ ...pillSm, background: T.red }}>Remove {dupScan.total}</button>
                  <button className="press" onClick={() => setDupScan(null)} style={{ ...pillSm, background: T.fill2, color: T.dim }}>Cancel</button>
                </div>
              </div>
            ) : (
              <div style={{ fontSize: 13.5, color: T.dim }}>No duplicates found — your transactions are clean. <button className="press" onClick={() => setDupScan(null)} style={{ background: "none", border: "none", color: T.blue, fontSize: 13.5, fontWeight: 600, cursor: "pointer", padding: 0 }}>OK</button></div>
            )
          )}
          {dupDone && (
            <div style={{ fontSize: 13.5, color: T.green, fontWeight: 600 }}>Removed {dupDone.removedTx + dupDone.removedInc} duplicate{(dupDone.removedTx + dupDone.removedInc) === 1 ? "" : "s"}. <button className="press" onClick={() => setDupDone(null)} style={{ background: "none", border: "none", color: T.blue, fontSize: 13.5, fontWeight: 600, cursor: "pointer", padding: 0 }}>Done</button></div>
          )}
        </div>
      </div>

      {/* Account */}
      <div>
        <div style={grpLbl}>ACCOUNT</div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 8, display: "flex", alignItems: "center", gap: 12 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 15.5, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{accountEmail}</div>
            <div style={{ fontSize: 13, color: T.dim, marginTop: 2 }}>Synced across your devices</div>
          </div>
          <button className="press" onClick={onSignOut} style={{ ...pillSm, background: T.fill2, color: T.text, flexShrink: 0 }}>Sign out</button>
        </div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 12 }}>
          {delStage === 0 ? (
            <button className="press" onClick={() => { setDelStage(1); setDelErr(""); }} style={{ width: "100%", background: "none", border: "none", color: T.red, fontSize: 14.5, fontWeight: 600, cursor: "pointer", padding: 4, textAlign: "left" }}>Delete account</button>
          ) : (
            <div className="pop">
              <div style={{ fontSize: 14.5, fontWeight: 700, color: T.red, marginBottom: 4 }}>Permanently delete your account?</div>
              <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.5, marginBottom: 12 }}>This erases your login and all your data — goals, budgets, plan, history — from every device. It can't be undone. Export a backup first if you might want it back.</div>
              {delErr && <div style={{ fontSize: 13, color: T.red, marginBottom: 10 }}>{delErr}</div>}
              <div style={{ display: "flex", gap: 8 }}>
                <button className="press" onClick={runDelete} disabled={delBusy} style={{ ...pillSm, background: T.red, opacity: delBusy ? 0.6 : 1 }}>{delBusy ? "Deleting…" : "Delete forever"}</button>
                <button className="press" onClick={() => setDelStage(0)} disabled={delBusy} style={{ ...pillSm, background: T.fill2, color: T.dim }}>Cancel</button>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  TOUR — coach-mark walkthrough, shown once after onboarding         */
/* ================================================================== */
/* ================================================================== */
/*  TOUR — auto-driving spotlight walkthrough, shown once             */
/* ================================================================== */
const TOUR_STEPS = [
  { view: "home", target: null, place: "center", title: "Welcome in 👋", body: "Quick tour of where everything lives — about 20 seconds. Tap Next, or the glowing button when I point at one." },
  { view: "home", target: "tab-home", place: "bottom", title: "Summary", body: "Home base — your primary goal, this month's spending, and your budgets. Tap any budget to drill in." },
  { view: "home", target: "coach", place: "top", title: "Meet Syrup", body: "This sparkle opens Syrup, your money coach, from any screen. Ask anything, or say \u201cI want to save for a trip\u201d and Syrup builds the goal with you." },
  { view: "home", target: "add", place: "top", title: "Add anything", body: "Log expenses and income here. Snap a receipt and it splits each item into its own category automatically." },
  { view: "plan", target: "tab-plan", place: "bottom", title: "Your Plan", body: "The engine. It splits a lean month into essentials, savings, and flex, and holds your savings buckets — log contributions here." },
  { view: "income", target: "tab-income", place: "bottom", title: "Income", body: "Log every paycheck and side gig. Waffle projects your typical month from the last 90 days so the plan stays honest." },
  { view: "insights", target: "tab-insights", place: "bottom", title: "Insights", body: "Your monthly spending score and where it's leaking — Syrup lives here too." },
  { view: "profile", target: "profile", place: "top", title: "Make it yours", body: "Dark mode, your opening tab, coach tone, backups. You can replay this tour from here anytime. You're all set!" },
];
function Tour({ view, setView, openCoach, onDone }) {
  const [i, setI] = useState(0);
  const [rect, setRect] = useState(null);
  const s = TOUR_STEPS[i]; const last = i === TOUR_STEPS.length - 1;

  // Drive the app to the screen this step describes.
  useEffect(() => { if (s.view && s.view !== view) setView(s.view); }, [i]);

  // Locate the target element after the view settles; retry until it paints; track on resize/scroll.
  useEffect(() => {
    let timers = [];
    const find = () => {
      if (!s.target) { setRect(null); return; }
      const el = document.querySelector(`[data-tour="${s.target}"]`);
      if (el) setRect(el.getBoundingClientRect());
    };
    if (!s.target) { setRect(null); }
    // Retry a few times to cover the view transition + first paint.
    [80, 220, 400, 650].forEach((ms) => timers.push(setTimeout(find, ms)));
    window.addEventListener("resize", find); window.addEventListener("scroll", find, true);
    return () => { timers.forEach(clearTimeout); window.removeEventListener("resize", find); window.removeEventListener("scroll", find, true); };
  }, [i, view]);

  const advance = () => (last ? onDone() : setI(i + 1));
  const pad = 8;
  const hole = rect ? { left: rect.left - pad, top: rect.top - pad, width: rect.width + pad * 2, height: rect.height + pad * 2 } : null;
  const vh = (typeof window !== "undefined" ? window.innerHeight : 800);

  // Card placement that can't collide with the target. NO transforms here —
  // horizontal centering is done on the container via left+right, and a transform
  // in this object would clobber it (the bug that kept cutting the card off).
  const cardPos = !hole ? { top: Math.max(80, vh / 2 - 130) + "px" }
    : s.place === "bottom" ? { top: "max(calc(env(safe-area-inset-top) + 84px), 14vh)" }
    : { top: Math.min(vh - 260, hole.top + hole.height + 14) + "px" };

  return (
    <div style={{ position: "fixed", inset: 0, zIndex: 80 }}>
      {/* Dimmer: a rounded transparent hole punched via box-shadow, so the cutout corners are soft. */}
      {hole ? (
        <>
          {/* Big shadow spread dims everything except the rounded hole. Click anywhere on it advances. */}
          <div onClick={advance} style={{ position: "fixed", left: hole.left + "px", top: hole.top + "px", width: hole.width + "px", height: hole.height + "px", borderRadius: 14, boxShadow: `0 0 0 9999px ${T.overlay}`, cursor: "pointer" }} />
          {/* Pulsing ring around the live element */}
          <div style={{ position: "fixed", left: hole.left + "px", top: hole.top + "px", width: hole.width + "px", height: hole.height + "px", borderRadius: 14, boxShadow: `0 0 0 2px ${T.waffle}`, pointerEvents: "none", animation: "tourpulse 1.6s ease-in-out infinite" }} />
          {/* Tapping the spotlighted element advances the tour */}
          <div onClick={advance} style={{ position: "fixed", left: hole.left + "px", top: hole.top + "px", width: hole.width + "px", height: hole.height + "px", borderRadius: 14, cursor: "pointer", background: "transparent" }} />
        </>
      ) : (
        <div onClick={advance} style={{ position: "fixed", inset: 0, background: T.overlay }} />
      )}

      <style>{`@keyframes tourpulse{0%,100%{box-shadow:0 0 0 2px ${T.waffle},0 0 0 6px ${T.waffle}33}50%{box-shadow:0 0 0 2px ${T.waffle},0 0 0 12px ${T.waffle}00}}`}</style>

      {/* Coaching card — centered via left+right (transform-free so it can't be clobbered) */}
      <div className="pop" style={{ position: "fixed", left: 16, right: 16, maxWidth: 360, marginLeft: "auto", marginRight: "auto", ...cardPos, zIndex: 82 }}>
        <div style={{ ...card, padding: 18 }}>
          <div style={{ display: "flex", gap: 5, marginBottom: 14 }}>
            {TOUR_STEPS.map((_, k) => <span key={k} style={{ flex: 1, height: 4, borderRadius: 2, background: k <= i ? T.waffle : T.fill2, transition: "background .25s" }} />)}
          </div>
          <div style={{ fontSize: 18, fontWeight: 800, letterSpacing: "-0.01em", marginBottom: 7 }}>{s.title}</div>
          <div style={{ fontSize: 14.5, color: T.dim, lineHeight: 1.5, marginBottom: 18 }}>{s.body}</div>
          <div style={{ display: "flex", gap: 10, alignItems: "center" }}>
            <button className="press" onClick={onDone} style={{ background: "none", border: "none", color: T.faint, fontSize: 14, fontWeight: 600, cursor: "pointer", padding: "8px 4px" }}>Skip</button>
            <div style={{ flex: 1 }} />
            <button className="press" onClick={advance} style={{ ...pill, padding: "11px 24px" }}>{last ? "Done" : "Next"}</button>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  SETUP — goals, auto-adjust, custom category budgets                */
/* ================================================================== */
function SetupView({ config, metrics, onSave, onExport, onImport, accountEmail, onSignOut, onSetupIncome }) {
  const [c, setC] = useState(config); const [saved, setSaved] = useState(false);
  const [pendingImport, setPendingImport] = useState(null); const [importErr, setImportErr] = useState("");
  const importRef = useRef();
  const num = (v) => parseFloat(v) || 0;
  const update = (next) => { setC(next); setSaved(false); };
  // Autosave: commit the draft shortly after edits stop, so nothing is lost if you
  // navigate away without tapping Save. The Save button still works as an explicit confirm.
  const firstRun = useRef(true); const autoTimer = useRef(null);
  useEffect(() => {
    if (firstRun.current) { firstRun.current = false; return; }
    if (autoTimer.current) clearTimeout(autoTimer.current);
    autoTimer.current = setTimeout(() => { onSave(c); setSaved(true); }, 900);
    return () => { if (autoTimer.current) clearTimeout(autoTimer.current); };
  }, [c]);
  const onPickFile = (e) => {
    const f = e.target.files && e.target.files[0]; if (!f) { return; }
    setImportErr("");
    const r = new FileReader();
    r.onload = () => {
      try {
        const parsed = JSON.parse(r.result);
        const data = parsed && parsed.data ? parsed.data : parsed;
        if (!data || typeof data !== "object" || !data.config) throw new Error("bad");
        setPendingImport(data);
      } catch (_) { setImportErr("That doesn't look like a Waffle backup."); }
    };
    r.onerror = () => setImportErr("Couldn't read that file.");
    r.readAsText(f);
    e.target.value = "";
  };

  const setGoal = (idx, key, v) => { const n = JSON.parse(JSON.stringify(c)); n.goals[idx][key] = v; update(n); };
  const addGoal = () => { const n = JSON.parse(JSON.stringify(c)); n.goals.push({ id: "g" + Date.now().toString(36), name: "New goal", target: 5000, saved: 0 }); update(n); };
  const removeGoal = (idx) => { const n = JSON.parse(JSON.stringify(c)); const [rm] = n.goals.splice(idx, 1); if (n.primaryGoalId === rm.id) n.primaryGoalId = n.goals[0] ? n.goals[0].id : null; update(n); };
  const setPrimary = (id) => { const n = JSON.parse(JSON.stringify(c)); n.primaryGoalId = id; update(n); };
  const moveGoal = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= c.goals.length) return; const n = JSON.parse(JSON.stringify(c)); const [g] = n.goals.splice(idx, 1); n.goals.splice(j, 0, g); update(n); };
  const removeRecurring = (id) => { const n = JSON.parse(JSON.stringify(c)); n.recurring = (n.recurring || []).filter((r) => r.id !== id); update(n); };
  const toggleAuto = (v) => update({ ...c, autoAdjust: v });

  const setBudget = (name, v) => { const n = JSON.parse(JSON.stringify(c)); n.budgets[name] = v; update(n); };
  const setCatField = (idx, key, v) => {
    const n = JSON.parse(JSON.stringify(c)); const old = n.categories[idx].name;
    if (key === "name") { const val = v; if (val && val !== old) { n.budgets[val] = n.budgets[old] || 0; delete n.budgets[old]; } n.categories[idx].name = val; }
    else n.categories[idx][key] = v;
    update(n);
  };
  const addCategory = () => {
    const n = JSON.parse(JSON.stringify(c));
    const used = new Set(n.categories.map((x) => x.color));
    const color = PALETTE.find((p) => !used.has(p)) || PALETTE[n.categories.length % PALETTE.length];
    let base = "New category", name = base, k = 1; const names = new Set(n.categories.map((x) => x.name));
    while (names.has(name)) { name = `${base} ${++k}`; }
    n.categories.push({ name, color }); n.budgets[name] = 0; update(n);
  };
  const removeCategory = (idx) => { const n = JSON.parse(JSON.stringify(c)); const [rm] = n.categories.splice(idx, 1); delete n.budgets[rm.name]; update(n); };

  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
      {/* Goals */}
      <div>
        <div style={grpLbl}>GOALS</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 12, marginTop: 8 }}>
          {c.goals.map((g, i) => {
            const isPrimary = c.primaryGoalId === g.id;
            return (
              <div key={g.id} style={{ ...card, padding: "16px 18px", border: isPrimary ? `1.5px solid ${T.waffle}` : "1.5px solid transparent" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 12 }}>
                  <button className="press" onClick={() => setPrimary(g.id)} aria-label="Make primary" style={{ background: "none", border: "none", cursor: "pointer", display: "flex", alignItems: "center", gap: 5, color: isPrimary ? T.waffle : T.faint, fontSize: 12, fontWeight: 700 }}>
                    <IStar size={18} color={isPrimary ? T.waffle : T.faint} fillIn={isPrimary} sw={1.8} />{isPrimary ? "PRIMARY" : "Set primary"}
                  </button>
                  {c.goals.length > 1 && <div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 2 }}>
                    <button className="press" onClick={() => moveGoal(i, -1)} disabled={i === 0} aria-label="Move up" style={{ background: "none", border: "none", cursor: i === 0 ? "default" : "pointer", color: T.faint, opacity: i === 0 ? 0.3 : 1, display: "flex", padding: 4, transform: "rotate(-90deg)" }}><IChevR size={18} sw={2.2} /></button>
                    <button className="press" onClick={() => moveGoal(i, 1)} disabled={i === c.goals.length - 1} aria-label="Move down" style={{ background: "none", border: "none", cursor: i === c.goals.length - 1 ? "default" : "pointer", color: T.faint, opacity: i === c.goals.length - 1 ? 0.3 : 1, display: "flex", padding: 4, transform: "rotate(90deg)" }}><IChevR size={18} sw={2.2} /></button>
                    <button className="press" onClick={() => removeGoal(i)} aria-label="Remove goal" style={{ background: "none", border: "none", cursor: "pointer", color: T.faint, display: "flex", padding: 4 }}><ITrash size={18} color={T.faint} sw={1.8} /></button>
                  </div>}
                </div>
                <Field label="Name"><input value={g.name} onChange={(e) => setGoal(i, "name", e.target.value)} style={inp} /></Field>
                <div style={{ display: "flex", gap: 12 }}>
                  <Field label="Target" flex><input value={g.target} onChange={(e) => setGoal(i, "target", num(cleanNumStr(e.target.value)))} style={inp} inputMode="numeric" /></Field>
                  <Field label="Saved" flex><input value={g.saved} onChange={(e) => setGoal(i, "saved", num(cleanNumStr(e.target.value)))} style={inp} inputMode="numeric" /></Field>
                </div>
                {(() => {
                  const tgt = num(g.target) || 0, sv = num(g.saved) || 0;
                  const pct = tgt > 0 ? Math.min(100, (sv / tgt) * 100) : 0;
                  const done = tgt > 0 && sv >= tgt;
                  return (
                    <div style={{ marginTop: 4 }}>
                      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 6 }}>
                        <span style={{ fontSize: 12.5, color: T.faint, fontWeight: 600 }}>{done ? "Reached! 🧇" : `${fmt(Math.max(0, tgt - sv))} to go`}</span>
                        <span style={{ fontSize: 14, fontWeight: 800, color: done ? T.green : T.waffle, fontVariantNumeric: "tabular-nums" }}>{Math.round(pct)}%</span>
                      </div>
                      <div style={{ height: 8, borderRadius: 4, background: T.fill2, overflow: "hidden" }}>
                        <div className="bar" style={{ height: "100%", width: `${pct}%`, background: done ? T.green : `linear-gradient(90deg, ${T.waffle}, ${T.orange})`, borderRadius: 4 }} />
                      </div>
                    </div>
                  );
                })()}
              </div>
            );
          })}
          <button className="press" onClick={addGoal} style={dashedBtn}><IPlus size={17} color={T.dim} sw={2.2} /> Add a goal</button>
          <div style={{ fontSize: 12.5, color: T.faint, paddingLeft: 6 }}>Income is tracked on the Income tab — Waffle projects it from your last 90 days. The primary goal drives your on-pace estimate.</div>
        </div>
      </div>

      {/* Auto-adjust */}
      <div>
        <div style={grpLbl}>SYRUP</div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 8, display: "flex", alignItems: "center", gap: 14 }}>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 15.5, fontWeight: 600 }}>Auto-adjust goals</div>
            <div style={{ fontSize: 13, color: T.dim, marginTop: 3, lineHeight: 1.45 }}>Let Syrup propose budget changes and new goals you can apply with one tap. Nothing ever changes without your confirmation.</div>
          </div>
          <Toggle on={!!c.autoAdjust} onChange={toggleAuto} />
        </div>
      </div>

      {/* Category budgets */}
      {(() => {
        const inc = (metrics && (metrics.projIncome || metrics.incHigh)) || (c.plan && (c.plan.typicalIncome || c.plan.leanIncome)) || 0;
        const hasIncome = inc > 0;
        if (!hasIncome) {
          return (
            <div>
              <div style={grpLbl}>CATEGORY BUDGETS</div>
              <div style={{ ...card, padding: "22px 20px", marginTop: 8, textAlign: "center" }}>
                <div style={{ width: 46, height: 46, borderRadius: 23, background: T.fill2, display: "grid", placeItems: "center", margin: "0 auto 12px" }}><ILock size={20} color={T.faint} /></div>
                <div style={{ fontSize: 15.5, fontWeight: 700, marginBottom: 6 }}>Set your income first</div>
                <div style={{ fontSize: 13.5, color: T.dim, lineHeight: 1.5, marginBottom: 16 }}>Budgets are measured against what you actually bring in — that's the whole point. Tell Waffle your income and we'll ground your budgets in a real number.</div>
                <button className="press" onClick={onSetupIncome} style={{ ...pill, width: "100%" }}>Set up income</button>
              </div>
            </div>
          );
        }
        return (
      <div>
        <div style={grpLbl}>CATEGORY BUDGETS</div>
        {(() => {
          const totalBud = Object.values(c.budgets || {}).reduce((a, b) => a + (b || 0), 0);
          const ratio = totalBud / inc;
          if (ratio <= 1) return (
            <div style={{ fontSize: 12.5, color: T.faint, padding: "8px 6px 0", lineHeight: 1.45 }}>Budgets total {fmt(totalBud)} of your ~{fmt(inc)}/mo income.</div>
          );
          return (
            <div style={{ marginTop: 8, padding: "12px 14px", borderRadius: 12, background: (ratio > 2 ? T.red : T.orange) + "14", border: `1px solid ${(ratio > 2 ? T.red : T.orange)}44` }}>
              <div style={{ fontSize: 13.5, fontWeight: 700, color: ratio > 2 ? T.red : T.orange, marginBottom: 3 }}>{ratio > 2 ? "That's a lot more than you make" : "Budgets exceed your income"}</div>
              <div style={{ fontSize: 12.5, color: T.dim, lineHeight: 1.45 }}>Your budgets total {fmt(totalBud)} but you bring in about {fmt(inc)}/mo. {ratio > 2 ? "Double-check for a typo — that's over 2× your income." : "Trim a category or two so the plan stays realistic."}</div>
            </div>
          );
        })()}
        <div style={{ ...card, padding: "2px 0", marginTop: 8 }}>
          {c.categories.map((cat, i) => (
            <CategoryEditRow key={i} cat={cat} budget={c.budgets[cat.name] || 0} last={i === c.categories.length - 1} canRemove={c.categories.length > 1}
              onName={(v) => setCatField(i, "name", v)} onColor={(v) => setCatField(i, "color", v)} onBudget={(v) => setBudget(cat.name, num(v))} onRemove={() => removeCategory(i)} />
          ))}
        </div>
        <button className="press" onClick={addCategory} style={{ ...dashedBtn, marginTop: 12 }}><IPlus size={17} color={T.dim} sw={2.2} /> Add a category</button>
      </div>
        );
      })()}

      {/* Recurring */}
      {(c.recurring || []).length > 0 && (
        <div>
          <div style={grpLbl}>RECURRING</div>
          <div style={{ ...card, padding: "2px 0", marginTop: 8 }}>
            {c.recurring.map((r, i) => (
              <Row key={r.id} last={i === c.recurring.length - 1}>
                <span style={{ width: 30, height: 30, borderRadius: 8, background: (r.kind === "income" ? T.green : (colorMapOf(c.categories)[r.category] || T.faint)) + "22", display: "grid", placeItems: "center", flexShrink: 0 }}>{r.kind === "income" ? <IPlus size={15} color={T.green} sw={2.4} /> : <span style={{ width: 11, height: 11, borderRadius: 6, background: colorMapOf(c.categories)[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.kind === "income" ? r.source : r.who}</div>
                  <div style={{ fontSize: 12.5, color: T.faint }}>{cadenceLabel(r.cadence)} · {fmt(r.amount)} · next {prettyDate(nextOccurrence(r))}</div>
                </div>
                <button className="press" onClick={() => removeRecurring(r.id)} aria-label="Stop recurring" style={{ background: "none", border: "none", cursor: "pointer", color: T.faint, display: "flex", flexShrink: 0 }}><IX size={17} sw={2} /></button>
              </Row>
            ))}
          </div>
          <div style={{ fontSize: 12.5, color: T.faint, paddingLeft: 6, marginTop: 8 }}>Set something to repeat from the + button. Removing one here stops future entries (and applies when you Save) — already-logged ones stay.</div>
        </div>
      )}

      <button className="press" onClick={() => { onSave(c); setSaved(true); }} style={{ ...pill, width: "100%" }}>{saved ? "Saved ✓" : "Save changes"}</button>

      {/* Data — backup & restore */}
      <div>
        <div style={grpLbl}>DATA</div>
        <div style={{ ...card, padding: "16px 18px", marginTop: 8 }}>
          <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.5, marginBottom: 14 }}>Everything lives only on this device. Export a backup now and then so a cleared browser or a new phone can't wipe it — the file restores all your goals, budgets, plan, and history.</div>
          <input ref={importRef} type="file" accept="application/json,.json" onChange={onPickFile} style={{ display: "none" }} />
          <div style={{ display: "flex", gap: 10 }}>
            <button className="press" onClick={onExport} style={{ ...pill, flex: 1 }}>Export backup</button>
            <button className="press" onClick={() => importRef.current && importRef.current.click()} style={{ ...pill, flex: 1, background: T.fill2, color: T.text }}>Import backup</button>
          </div>
          {importErr && <div style={{ fontSize: 13, color: T.red, marginTop: 12 }}>{importErr}</div>}
          {pendingImport && (
            <div className="pop" style={{ marginTop: 14, padding: "14px 16px", borderRadius: 14, background: T.orange + "12", border: `1px solid ${T.orange}44` }}>
              <div style={{ fontSize: 14, fontWeight: 700, color: T.orange, marginBottom: 4 }}>Replace everything with this backup?</div>
              <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.45, marginBottom: 12 }}>This overwrites your current data — {Array.isArray(pendingImport.tx) ? pendingImport.tx.length : 0} expenses and {Array.isArray(pendingImport.income) ? pendingImport.income.length : 0} income entries will be restored.</div>
              <div style={{ display: "flex", gap: 8 }}>
                <button className="press" onClick={() => { onImport(pendingImport); setPendingImport(null); }} style={{ ...pillSm, background: T.red }}>Replace</button>
                <button className="press" onClick={() => setPendingImport(null)} style={{ ...pillSm, background: T.fill2, color: T.dim }}>Cancel</button>
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Account */}
      {accountEmail && (
        <div>
          <div style={grpLbl}>ACCOUNT</div>
          <div style={{ ...card, padding: "16px 18px", marginTop: 8, display: "flex", alignItems: "center", gap: 12 }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 15.5, fontWeight: 600, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{accountEmail}</div>
              <div style={{ fontSize: 13, color: T.dim, marginTop: 2 }}>Synced to your account across devices</div>
            </div>
            <button className="press" onClick={onSignOut} style={{ ...pillSm, background: T.fill2, color: T.text, flexShrink: 0 }}>Sign out</button>
          </div>
        </div>
      )}
    </div>
  );
}

function CategoryEditRow({ cat, budget, last, canRemove, onName, onColor, onBudget, onRemove }) {
  const [pick, setPick] = useState(false);
  return (
    <div style={{ borderBottom: last ? "none" : `1px solid ${T.sep}` }}>
      <div style={{ display: "flex", alignItems: "center", gap: 11, padding: "11px 16px" }}>
        <button className="press" onClick={() => setPick((p) => !p)} aria-label="Color" style={{ width: 20, height: 20, borderRadius: 10, background: cat.color, border: "2px solid #fff", boxShadow: "0 0 0 1px rgba(0,0,0,.08)", cursor: "pointer", flexShrink: 0, padding: 0 }} />
        <input value={cat.name} onChange={(e) => onName(e.target.value)} style={{ flex: 1, minWidth: 0, border: "none", background: "transparent", fontSize: 15.5, fontWeight: 500, color: T.text, padding: 0 }} />
        <div style={{ position: "relative", width: 92, flexShrink: 0 }}><span style={{ position: "absolute", left: 12, top: 9, color: T.faint, fontSize: 14 }}>$</span><input value={budget} onChange={(e) => onBudget(cleanNumStr(e.target.value))} inputMode="numeric" style={{ ...inp, padding: "8px 10px 8px 22px", fontSize: 14, textAlign: "right" }} /></div>
        {canRemove && <button className="press" onClick={onRemove} aria-label="Remove category" style={{ background: "none", border: "none", cursor: "pointer", color: T.faint, display: "flex", flexShrink: 0 }}><IX size={17} sw={2} /></button>}
      </div>
      {pick && (
        <div className="pop" style={{ display: "flex", flexWrap: "wrap", gap: 9, padding: "2px 16px 14px 47px" }}>
          {PALETTE.map((p) => (
            <button key={p} className="press" onClick={() => { onColor(p); setPick(false); }} aria-label={p} style={{ width: 26, height: 26, borderRadius: 13, background: p, border: cat.color === p ? "2px solid #1D1D1F" : "2px solid #fff", boxShadow: "0 0 0 1px rgba(0,0,0,.08)", cursor: "pointer", padding: 0 }} />
          ))}
        </div>
      )}
    </div>
  );
}

/* ================================================================== */
/*  PLAN — tiered allocation breakdown                                 */
/* ================================================================== */
function StackBar({ parts }) {
  const total = parts.reduce((a, p) => a + Math.max(0, p.value), 0) || 1;
  return (
    <div style={{ display: "flex", height: 12, borderRadius: 6, overflow: "hidden", background: T.fill2 }}>
      {parts.map((p, i) => (
        <div key={i} title={p.label} style={{ width: `${(Math.max(0, p.value) / total) * 100}%`, background: p.color, transition: "width .6s cubic-bezier(.22,.61,.36,1)" }} />
      ))}
    </div>
  );
}

function TaxReserveCard({ metrics, config, saveConfig, onSetAside, onWithdraw }) {
  const [showHist, setShowHist] = useState(false);
  const [custom, setCustom] = useState("");
  const rate = metrics.taxRate || 0;
  const log = config.taxLog || [];
  const covered = metrics.ytdTaxOwed > 0 && metrics.taxSetAside >= metrics.ytdTaxOwed;
  const barColor = covered ? T.green : metrics.taxCoverage >= 60 ? T.orange : T.red;

  return (
    <div style={{ ...card, padding: "18px 20px" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 3 }}>
        <div>
          <div style={{ fontSize: 15, fontWeight: 700 }}>Tax reserve</div>
          <div style={{ fontSize: 12.5, color: T.faint, marginTop: 1 }}>Money parked for taxes you owe yourself</div>
        </div>
        <div style={{ textAlign: "right" }}>
          <div style={{ fontSize: 24, fontWeight: 800, letterSpacing: "-0.02em", fontVariantNumeric: "tabular-nums" }}>{fmt(metrics.taxSetAside)}</div>
          <div style={{ fontSize: 11.5, color: T.faint }}>set aside</div>
        </div>
      </div>

      {/* Coverage vs YTD estimate */}
      {rate > 0 && (
        <div style={{ marginTop: 14, marginBottom: 4 }}>
          <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12.5, marginBottom: 6 }}>
            <span style={{ color: T.dim }}>{metrics.taxYear} estimate · {fmt(metrics.ytdTaxOwed)}</span>
            <span style={{ fontWeight: 700, color: barColor }}>{covered ? "Covered ✓" : metrics.ytdTaxOwed > 0 ? `${fmt(metrics.taxShortfall)} short` : "—"}</span>
          </div>
          <div style={{ height: 8, borderRadius: 4, background: T.fill2, overflow: "hidden" }}>
            <div className="bar" style={{ height: "100%", width: `${metrics.taxCoverage}%`, background: barColor, borderRadius: 4 }} />
          </div>
          <div style={{ fontSize: 12, color: T.faint, marginTop: 7, lineHeight: 1.45 }}>
            You've earned {fmt(metrics.ytdTaxableIncome)} of taxable income this year. At {rate}%, that's about {fmt(metrics.ytdTaxOwed)} owed — an estimate, not your real return.
          </div>
        </div>
      )}

      {/* Rate selector */}
      <div style={{ marginTop: 16 }}>
        <div style={{ fontSize: 11.5, color: T.faint, fontWeight: 600, marginBottom: 6 }}>SET ASIDE THIS % OF TAXABLE INCOME</div>
        <div style={{ display: "flex", gap: 7 }}>
          {[0, 15, 25, 30].map((r) => <button key={r} className="press" onClick={() => saveConfig({ ...config, plan: { ...(config.plan || {}), taxRate: r } })} style={{ flex: 1, border: "none", cursor: "pointer", padding: "9px 0", borderRadius: 10, fontSize: 13.5, fontWeight: 600, background: rate === r ? T.ink : T.fill2, color: rate === r ? T.bg : T.dim }}>{r === 0 ? "Off" : r + "%"}</button>)}
        </div>
        <div style={{ fontSize: 12, color: T.faint, marginTop: 8, lineHeight: 1.4 }}>Applies only to income you mark "I owe tax on it" — W-2 pay that's already withheld is left alone.</div>
      </div>

      {/* Actions */}
      {rate > 0 && (
        <div style={{ marginTop: 14, display: "flex", flexDirection: "column", gap: 8 }}>
          {metrics.monthTaxableIncome > 0
            ? <button className="press" onClick={() => onSetAside(rate)} style={{ ...pill, width: "100%" }}>Set aside {fmt(metrics.monthTaxOwed)} from this month</button>
            : <div style={{ fontSize: 12.5, color: T.faint, textAlign: "center", padding: "2px 0", lineHeight: 1.4 }}>No taxable income logged this month yet.</div>}
          <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
            <div style={{ position: "relative", flex: 1 }}><span style={{ position: "absolute", left: 12, top: 11, color: T.faint, fontSize: 14 }}>$</span><input value={custom} onChange={(e) => setCustom(cleanNumStr(e.target.value))} inputMode="numeric" placeholder="Custom amount" style={{ ...inp, padding: "10px 12px 10px 22px", fontSize: 14 }} /></div>
            <button className="press" onClick={() => { const a = parseFloat(custom) || 0; if (a > 0) { onSetAside(rate, a); setCustom(""); } }} disabled={!parseFloat(custom)} style={{ ...pillSm, padding: "10px 16px", background: T.fill2, color: T.text, opacity: parseFloat(custom) ? 1 : 0.5 }}>Add</button>
          </div>
        </div>
      )}

      {/* History */}
      {log.length > 0 && (
        <div style={{ marginTop: 14, paddingTop: 14, borderTop: `1px solid ${T.sep}` }}>
          <button className="press" onClick={() => setShowHist((s) => !s)} style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between", background: "none", border: "none", cursor: "pointer", padding: 0 }}>
            <span style={{ fontSize: 13, fontWeight: 600, color: T.dim }}>History · {log.length} {log.length === 1 ? "entry" : "entries"}</span>
            <span style={{ transform: showHist ? "rotate(90deg)" : "none", transition: "transform .2s", color: T.faint, display: "flex" }}><IChevR size={16} sw={2} /></span>
          </button>
          {showHist && (
            <div className="vin" style={{ marginTop: 10, display: "flex", flexDirection: "column", gap: 8 }}>
              {log.slice(0, 24).map((e) => (
                <div key={e.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 13.5 }}>
                  <span style={{ color: T.dim }}>{new Date(e.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}{e.note ? ` · ${e.note}` : ""}</span>
                  <span style={{ fontWeight: 700, color: e.amount < 0 ? T.red : T.green, fontVariantNumeric: "tabular-nums" }}>{e.amount < 0 ? "−" : "+"}{fmt(Math.abs(e.amount))}</span>
                </div>
              ))}
              {metrics.taxSetAside > 0 && (
                <button className="press" onClick={() => { const a = parseFloat(window.prompt("Withdraw how much from the tax reserve? (e.g. when you pay your taxes)") || "0"); if (a > 0) onWithdraw(a); }} style={{ ...pillSm, marginTop: 4, background: T.fill2, color: T.dim, alignSelf: "flex-start", padding: "7px 14px" }}>Withdraw…</button>
              )}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function PlanView({ metrics, config, saveConfig, onRedo, onContribute, onLearn, onSetAsideTax, onWithdrawTax, onAssign }) {
  const [showEss, setShowEss] = useState(false);
  const [quickInc, setQuickInc] = useState(false);
  const plan = computePlan(metrics, config);
  const income = plan.planningIncome;
  const pctOf = (n) => (income > 0 ? Math.round((n / income) * 100) : 0);
  const setBasis = (b) => saveConfig({ ...config, plan: { ...config.plan, basis: b } });
  const bumpSavings = (d) => saveConfig({ ...config, plan: { ...config.plan, savingsPct: Math.max(0, Math.min(60, (config.plan.savingsPct || 15) + d)) } });
  const setBufferSaved = (v) => {
    const amt = Math.max(0, Math.round(parseFloat(v) || 0));
    const target = metrics.bufferTarget || 0;
    if (target > 0 && amt > target * 3 && amt >= 10000) {
      if (!window.confirm(`That's ${fmt(amt)} — well above your ${fmt(target)} buffer target. Save it anyway?`)) return;
    }
    saveConfig({ ...config, plan: { ...config.plan, bufferSaved: amt } });
  };
  const toggleEssential = (name) => saveConfig({ ...config, categories: config.categories.map((c) => (c.name === name ? { ...c, essential: !c.essential } : c)) });
  const rebalanceFlex = () => {
    if (plan.flexBudgeted <= 0) return;
    const k = plan.flexAvailable / plan.flexBudgeted;
    const budgets = { ...config.budgets };
    plan.flexCats.forEach((c) => { budgets[c.name] = Math.max(0, Math.round((config.budgets[c.name] || 0) * k)); });
    saveConfig({ ...config, budgets });
  };
  const primary = metrics.primary;
  const monthsAtPlan = primary && plan.savingsTarget > 0 ? Math.ceil(Math.max(0, primary.target - primary.saved) / plan.savingsTarget) : null;
  const committed = plan.essentialsFloor + plan.savingsTarget + plan.flexBudgeted;
  const leftover = income - committed;

  if (!income) {
    return (
      <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
        <div style={{ ...card, padding: "26px 22px", textAlign: "center" }}>
          <div style={{ fontSize: 17, fontWeight: 700, marginBottom: 6 }}>Build your plan</div>
          <div style={{ fontSize: 14, color: T.dim, lineHeight: 1.5, marginBottom: 18 }}>Answer a few quick questions and Waffle will suggest how to split a typical month into essentials, savings, and flex.</div>
          <button className="press" onClick={onRedo} style={{ ...pill, width: "100%" }}>Start setup</button>
        </div>
      </div>
    );
  }

  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
      {/* Planning income + basis */}
      <div style={{ ...card, padding: "20px 22px" }}>
        <div style={{ fontSize: 13, color: T.dim, fontWeight: 600 }}>Planning on a {plan.basis === "expected" ? "typical" : "lean"} month</div>
        <div style={{ fontSize: 40, fontWeight: 800, letterSpacing: "-0.03em", marginTop: 4, fontVariantNumeric: "tabular-nums" }}>{fmt(income)}</div>
        <div style={{ display: "flex", background: T.fill2, borderRadius: 10, padding: 2, marginTop: 14 }}>
          <Seg active={plan.basis === "conservative"} onClick={() => setBasis("conservative")} label="Conservative" />
          <Seg active={plan.basis === "expected"} onClick={() => setBasis("expected")} label="Typical" />
        </div>
        <div style={{ fontSize: 12.5, color: T.faint, marginTop: 10, lineHeight: 1.45 }}>Conservative plans off a lean month so a slow stretch never breaks you. Typical uses your projected average.</div>
      </div>

      {/* The split */}
      <div style={{ ...card, padding: "20px 22px" }}>
        <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 14 }}>Where it goes</div>
        <StackBar parts={[{ label: "Essentials", value: plan.essentialsFloor, color: T.blue }, { label: "Savings", value: plan.savingsTarget, color: T.green }, { label: "Flex", value: plan.flexAvailable, color: T.waffle }]} />
        <div style={{ marginTop: 16, display: "flex", flexDirection: "column", gap: 14 }}>
          <PlanRow color={T.blue} label="Essentials" sub="Fixed costs, funded first" amount={plan.essentialsFloor} pct={pctOf(plan.essentialsFloor)} />
          <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
            <span style={{ width: 11, height: 11, borderRadius: 6, background: T.green, flexShrink: 0 }} />
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 15.5, fontWeight: 600 }}>Savings &amp; goals</div>
              <div style={{ fontSize: 12.5, color: T.faint }}>Paid before you can spend it</div>
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <button className="press" onClick={() => bumpSavings(-5)} aria-label="less" style={stepBtn}>−</button>
              <div style={{ textAlign: "right", minWidth: 70 }}>
                <div style={{ fontSize: 15.5, fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{fmt(plan.savingsTarget)}</div>
                <div style={{ fontSize: 11.5, color: T.faint }}>{plan.savingsPct}%</div>
              </div>
              <button className="press" onClick={() => bumpSavings(5)} aria-label="more" style={stepBtn}>+</button>
            </div>
          </div>
          <PlanRow color={T.waffle} label="Flex" sub="Everything discretionary" amount={plan.flexAvailable} pct={pctOf(plan.flexAvailable)} />
        </div>
      </div>

      {/* Fit check */}
      {plan.essentialsTooHigh ? (
        <div style={{ ...card, padding: "16px 20px", border: `1px solid ${T.red}44` }}>
          <div style={{ fontSize: 14.5, fontWeight: 700, color: T.red, marginBottom: 4 }}>Your fixed costs are higher than a lean month</div>
          <div style={{ fontSize: 13.5, color: T.dim, lineHeight: 1.5 }}>Essentials run {fmt(plan.essentialsFloor)} but a lean month brings in {fmt(income)}. Before any saving, this is the gap to close — trim a fixed cost or lift your floor income. Everything else can wait.</div>
        </div>
      ) : plan.overBy > 0 ? (
        <div style={{ ...card, padding: "16px 20px", border: `1px solid ${T.orange}44` }}>
          <div style={{ fontSize: 14.5, fontWeight: 700, color: T.orange, marginBottom: 4 }}>Over-committed by {fmt(plan.overBy)}</div>
          <div style={{ fontSize: 13.5, color: T.dim, lineHeight: 1.5, marginBottom: 12 }}>Your category budgets add up to {fmt(committed)} — more than your {plan.basis === "expected" ? "typical" : "lean"}-month plan. Scale the flexible budgets down to fit the {fmt(plan.flexAvailable)} that's actually free.</div>
          <button className="press" onClick={rebalanceFlex} style={{ ...pillSm, background: T.blue }}>Rebalance flex to fit</button>
        </div>
      ) : (
        <div style={{ ...card, padding: "16px 20px" }}>
          <div style={{ fontSize: 14.5, fontWeight: 700, color: T.green, marginBottom: 4 }}>Your budgets fit</div>
          <div style={{ fontSize: 13.5, color: T.dim, lineHeight: 1.5 }}>{leftover > 0 ? `${fmt(leftover)} a month is unspoken for — bump savings or a goal to put it to work.` : "Essentials, savings, and flex all land inside a lean month. Nice."}</div>
        </div>
      )}

      {/* Goal pace at this savings rate */}
      {primary && monthsAtPlan != null && (
        <div style={{ ...card, padding: "16px 20px", display: "flex", alignItems: "center", gap: 12 }}>
          <span style={{ fontSize: 11.5, fontWeight: 700, color: T.waffle, background: T.waffle + "1C", padding: "3px 9px", borderRadius: 20, whiteSpace: "nowrap" }}>AT THIS RATE</span>
          <span style={{ fontSize: 13.5, color: T.dim }}>Saving {fmt(plan.savingsTarget)}/mo puts <b style={{ color: T.text }}>{primary.name}</b> ~{monthsAtPlan} months out.</span>
        </div>
      )}

      {/* Savings buckets */}
      <div style={{ ...card, padding: "18px 22px" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 4 }}>
          <span style={{ fontSize: 15, fontWeight: 700 }}>Savings buckets</span>
          <span style={{ fontSize: 13, color: T.dim, fontVariantNumeric: "tabular-nums" }}>pool {fmt((config.goals || []).reduce((a, g) => a + g.saved, 0) + (config.savingsUnassigned || 0))}</span>
        </div>
        <div style={{ fontSize: 12.5, color: T.faint, marginBottom: 14, lineHeight: 1.4 }}>One savings pool, split by goal. Contributions fill your primary first, then cascade down.</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 13 }}>
          {goalOrder(config).map((id, i) => {
            const g = (config.goals || []).find((x) => x.id === id); if (!g) return null;
            const pct = g.target > 0 ? Math.min(100, (g.saved / g.target) * 100) : 0;
            return (
              <div key={id} style={{ display: "flex", alignItems: "center", gap: 12 }}>
                <span style={{ fontSize: 12, fontWeight: 700, color: T.faint, width: 14, fontVariantNumeric: "tabular-nums" }}>{i + 1}</span>
                <div style={{ flex: 1 }}>
                  <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
                    <span style={{ fontSize: 15, fontWeight: 500 }}>{g.name}{id === config.primaryGoalId ? <span style={{ fontSize: 11, color: T.waffle, fontWeight: 700 }}> · primary</span> : null}</span>
                    <span style={{ fontSize: 13, color: T.dim, fontVariantNumeric: "tabular-nums" }}>{fmt(g.saved)} / {fmt(g.target)}</span>
                  </div>
                  <div style={{ height: 6, borderRadius: 3, background: T.fill2, overflow: "hidden" }}><div className="bar" style={{ height: "100%", width: `${pct}%`, background: T.waffle, borderRadius: 3 }} /></div>
                </div>
              </div>
            );
          })}
          {(config.savingsUnassigned || 0) > 0 && (
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: 13.5, paddingLeft: 26, color: T.dim }}>
              <span>Unassigned</span>
              <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
                <span style={{ fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{fmt(config.savingsUnassigned)}</span>
                {(config.goals || []).length > 0 && <button className="press" onClick={onAssign} style={{ ...pillSm, padding: "5px 12px", background: T.fill2, color: T.blue, fontSize: 12.5 }}>Assign</button>}
              </div>
            </div>
          )}
        </div>
        <button className="press" onClick={() => onContribute(plan.savingsTarget)} style={{ ...pill, width: "100%", marginTop: 16 }}>Log a contribution</button>
      </div>

      {/* Buffer */}
      <div style={{ ...card, padding: "18px 22px" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 4 }}>
          <span style={{ fontSize: 15, fontWeight: 700 }}>Buffer</span>
          <span style={{ fontSize: 13, color: T.dim, fontVariantNumeric: "tabular-nums" }}>target {fmt(plan.bufferTarget)} · {plan.bufferMonths} mo</span>
        </div>
        <div style={{ fontSize: 12.5, color: T.faint, marginBottom: 12, lineHeight: 1.4 }}>Cash that covers essentials when a month comes up short. Good months top it up.</div>
        {(() => {
          const tgt = plan.bufferTarget || 0; const sav = plan.bufferSaved || 0;
          const pctBuf = tgt > 0 ? Math.min(100, (sav / tgt) * 100) : 0;
          const full = tgt > 0 && sav >= tgt;
          return (<>
            {full && (
              <div className="pop" style={{ display: "flex", alignItems: "center", gap: 10, padding: "11px 14px", borderRadius: 12, background: T.green + "18", marginBottom: 12 }}>
                <ICheck size={18} color={T.green} sw={2.6} />
                <span style={{ fontSize: 13.5, fontWeight: 600, color: T.green }}>Buffer fully funded — {plan.bufferMonths} months of essentials covered. 🧇</span>
              </div>
            )}
            <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 6 }}>
              <span style={{ fontSize: 12.5, color: T.faint }}>{full ? "Complete" : `${Math.round(pctBuf)}% there`}</span>
              {!full && tgt > 0 && <span style={{ fontSize: 12.5, color: T.faint, fontVariantNumeric: "tabular-nums" }}>{fmt(Math.max(0, tgt - sav))} to go</span>}
            </div>
            <div style={{ height: 8, borderRadius: 4, background: T.fill2, overflow: "hidden", marginBottom: 12 }}>
              <div className="bar" style={{ height: "100%", width: `${pctBuf}%`, background: T.green, borderRadius: 4 }} />
            </div>
          </>);
        })()}
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <span style={{ fontSize: 13.5, color: T.dim }}>Saved so far</span>
          <div style={{ position: "relative", flex: 1 }}><span style={{ position: "absolute", left: 12, top: 11, color: T.faint, fontSize: 14 }}>$</span><input value={plan.bufferSaved} onChange={(e) => setBufferSaved(e.target.value)} inputMode="numeric" style={{ ...inp, padding: "10px 12px 10px 22px", fontSize: 15, textAlign: "right" }} /></div>
        </div>
      </div>

      {/* What counts as essential */}
      <div>
        <button className="press" onClick={() => setShowEss((s) => !s)} style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between", background: "none", border: "none", cursor: "pointer", padding: "4px 6px" }}>
          <span style={grpLbl}>WHAT COUNTS AS ESSENTIAL</span>
          <span style={{ transform: showEss ? "rotate(90deg)" : "none", transition: "transform .2s", color: T.faint, display: "flex" }}><IChevR size={16} sw={2} /></span>
        </button>
        {showEss && (
          <div style={{ ...card, padding: "2px 0", marginTop: 8 }}>
            {config.categories.map((c, i) => (
              <Row key={c.name} last={i === config.categories.length - 1}>
                <span style={{ width: 11, height: 11, borderRadius: 6, background: c.color, flexShrink: 0 }} />
                <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500 }}>{c.name}</span>
                <span style={{ fontSize: 12.5, color: c.essential ? T.blue : T.faint, fontWeight: 600, marginRight: 4 }}>{c.essential ? "Essential" : "Flex"}</span>
                <Toggle on={!!c.essential} onChange={() => toggleEssential(c.name)} />
              </Row>
            ))}
          </div>
        )}
      </div>

      {/* Variable-income tools */}
      <div style={{ ...card, padding: "18px 20px" }}>
        <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 3 }}>Built for bumpy income</div>
        <div style={{ fontSize: 12.5, color: T.faint, marginBottom: 16, lineHeight: 1.4 }}>The numbers that matter when your paycheck isn't the same twice.</div>

        {/* Runway */}
        <div style={{ marginBottom: 16 }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
            <span style={{ fontSize: 14, fontWeight: 600 }}>Runway</span>
            <span style={{ fontSize: 20, fontWeight: 800, fontVariantNumeric: "tabular-nums", color: metrics.runwayMonths >= 3 ? T.green : metrics.runwayMonths >= 1 ? T.orange : T.red }}>{metrics.runwayMonths >= 0.1 ? metrics.runwayMonths.toFixed(1) : "0"} mo</span>
          </div>
          <div style={{ fontSize: 12.5, color: T.dim, lineHeight: 1.45, marginTop: 3 }}>
            {metrics.monthlyBurn > 0
              ? <>Your {fmt(metrics.reserves)} in available reserves (buffer + unassigned savings) covers about {metrics.runwayMonths.toFixed(1)} months of your {fmt(metrics.monthlyBurn)} essentials floor if income stopped. Money set aside for goals isn't counted.</>
              : <>Set your essentials in the plan to see how long your savings would last.</>}
          </div>
        </div>

        {/* Volatility */}
        <div style={{ marginBottom: 16, paddingTop: 16, borderTop: `1px solid ${T.sep}` }}>
          <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline" }}>
            <span style={{ fontSize: 14, fontWeight: 600 }}>Income swing</span>
            <span style={{ fontSize: 14.5, fontWeight: 700, textTransform: "capitalize", color: metrics.volLabel === "steady" ? T.green : metrics.volLabel === "bumpy" ? T.orange : T.red }}>{metrics.volLabel}{metrics.volatility ? ` · ±${metrics.volatility}%` : ""}</span>
          </div>
          <div style={{ fontSize: 12.5, color: T.dim, lineHeight: 1.45, marginTop: 3 }}>
            {metrics.volatility
              ? <>Your monthly income varies about {metrics.volatility}% around its average. {metrics.volLabel === "steady" ? "Steady enough to plan tightly." : metrics.volLabel === "bumpy" ? "Worth keeping a healthy buffer." : "Plan off your lean month and keep runway long."}</>
              : <>Log a few months of income to see how bumpy it really is.</>}
          </div>
        </div>
      </div>

      {/* Tax Reserve — a real home for taxes you owe yourself */}
      <TaxReserveCard metrics={metrics} config={config} saveConfig={saveConfig} onSetAside={onSetAsideTax} onWithdraw={onWithdrawTax} />

      <div className="press" onClick={onLearn} style={{ ...card, padding: "16px 18px", display: "flex", alignItems: "center", gap: 13, cursor: "pointer" }}>
        <span style={{ width: 38, height: 38, borderRadius: 19, background: T.waffle + "1A", display: "grid", placeItems: "center", flexShrink: 0 }}><IBook size={19} color={T.waffle} /></span>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15.5, fontWeight: 600 }}>Learn the money moves</div>
          <div style={{ fontSize: 13, color: T.dim }}>{(() => { const read = (config.prefs && config.prefs.gemsRead) || []; const d = GEMS.filter((g) => read.includes(g.id)).length; return d > 0 ? `${d} of ${GEMS.length} learned · keep going` : "Principles that build wealth beyond saving"; })()}</div>
        </div>
        <IChevR size={18} color={T.faint} sw={2} />
      </div>

      <div style={{ display: "flex", gap: 10 }}>
        <button className="press" onClick={() => setQuickInc(true)} style={{ ...dashedBtn, flex: 1 }}>Update income</button>
        <button className="press" onClick={onRedo} style={{ ...dashedBtn, flex: 1 }}>Redo full plan</button>
      </div>
      {quickInc && <QuickIncomeSheet config={config} onClose={() => setQuickInc(false)} onSave={(plan) => { saveConfig({ ...config, plan }); setQuickInc(false); }} />}
    </div>
  );
}
function QuickIncomeSheet({ config, onClose, onSave }) {
  const p = config.plan || {};
  const [lean, setLean] = useState(p.leanIncome ? String(p.leanIncome) : "");
  const [typical, setTypical] = useState(p.typicalIncome ? String(p.typicalIncome) : "");
  const [pct, setPct] = useState(typeof p.savingsPct === "number" ? p.savingsPct : 15);
  const leanN = parseFloat(lean) || 0, typN = parseFloat(typical) || 0;
  const save = () => onSave({ ...p, leanIncome: leanN, typicalIncome: typN || leanN, savingsPct: pct });
  return (
    <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 58, background: T.overlay, 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: "92vh", overflowY: "auto" }}>
        <div style={{ width: 38, height: 5, borderRadius: 3, background: T.grip, margin: "0 auto 12px" }} />
        <div style={{ fontSize: 22, fontWeight: 800, letterSpacing: "-0.02em", marginBottom: 4 }}>Update income</div>
        <div style={{ fontSize: 13.5, color: T.dim, marginBottom: 16, lineHeight: 1.45 }}>Just the numbers — no need to redo your whole plan. Your budgets and goals stay as they are.</div>
        <Field label="A lean month"><div style={{ position: "relative" }}><span style={dollar}>$</span><input value={lean} onChange={(e) => setLean(cleanNumStr(e.target.value))} placeholder="0" inputMode="decimal" style={{ ...inp, paddingLeft: 26 }} /></div></Field>
        <Field label="A typical month"><div style={{ position: "relative" }}><span style={dollar}>$</span><input value={typical} onChange={(e) => setTypical(cleanNumStr(e.target.value))} placeholder="0" inputMode="decimal" style={{ ...inp, paddingLeft: 26 }} /></div></Field>
        <div style={{ marginBottom: 18 }}>
          <div style={lbl}>Save this much of it</div>
          <div style={{ display: "flex", gap: 7 }}>
            {[8, 15, 25].map((r) => <button key={r} className="press" onClick={() => setPct(r)} style={{ flex: 1, border: "none", cursor: "pointer", padding: "11px 0", borderRadius: 10, fontSize: 14, fontWeight: 600, background: pct === r ? T.ink : T.fill2, color: pct === r ? T.bg : T.dim }}>{r}%</button>)}
          </div>
        </div>
        <button className="press" onClick={save} disabled={!leanN} style={{ ...pill, width: "100%", opacity: leanN ? 1 : 0.5 }}>Save income</button>
      </div>
    </div>
  );
}
function PlanRow({ color, label, sub, amount, pct }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
      <span style={{ width: 11, height: 11, borderRadius: 6, background: color, flexShrink: 0 }} />
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 15.5, fontWeight: 600 }}>{label}</div>
        <div style={{ fontSize: 12.5, color: T.faint }}>{sub}</div>
      </div>
      <div style={{ textAlign: "right" }}>
        <div style={{ fontSize: 15.5, fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{fmt(amount)}</div>
        <div style={{ fontSize: 11.5, color: T.faint }}>{pct}%</div>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  ONBOARDING — guided interview that seeds the plan                  */
/* ================================================================== */
function OptionRow({ active, title, sub, onClick }) {
  return (
    <button className="press" onClick={onClick} style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 12, background: active ? T.blue + "12" : T.card, border: `1.5px solid ${active ? T.blue : T.sep}`, borderRadius: 14, padding: "14px 16px", cursor: "pointer" }}>
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 15.5, fontWeight: 600, color: T.text }}>{title}</div>
        {sub && <div style={{ fontSize: 13, color: T.dim, marginTop: 2 }}>{sub}</div>}
      </div>
      <span style={{ width: 22, height: 22, borderRadius: 11, border: `2px solid ${active ? T.blue : T.fill2}`, background: active ? T.blue : "transparent", display: "grid", placeItems: "center", flexShrink: 0 }}>{active && <ICheck size={14} color="#fff" sw={3} />}</span>
    </button>
  );
}

/* ================================================================== */
/*  PROFILE SETUP — who the user is; drives tax + education            */
/* ================================================================== */
const EMPLOYMENT_OPTS = [
  { id: "w2", title: "Employee (W-2)", sub: "Taxes taken out of your paycheck" },
  { id: "1099", title: "Contractor (1099)", sub: "You handle your own taxes" },
  { id: "self", title: "Self-employed / business owner", sub: "You run your own thing" },
  { id: "mix", title: "A mix", sub: "Employed plus something on the side" },
];
const ACCOUNT_OPTS = [
  { id: "401k", label: "401(k)" }, { id: "457", label: "457(b)" }, { id: "ira", label: "IRA / Roth" },
  { id: "hsa", label: "HSA" }, { id: "brokerage", label: "Brokerage" }, { id: "none", label: "None yet" },
];
function ProfileSetup({ config, onComplete, embedded, onClose }) {
  const p0 = config.profile || {};
  const [employment, setEmployment] = useState(p0.employment || "w2");
  const [sideBusiness, setSideBusiness] = useState(!!p0.sideBusiness);
  const [accounts, setAccounts] = useState(Array.isArray(p0.accounts) ? p0.accounts : []);
  const [ownsHome, setOwnsHome] = useState(!!p0.ownsHome);
  const toggleAcct = (id) => setAccounts((a) => {
    if (id === "none") return a.includes("none") ? [] : ["none"];
    const without = a.filter((x) => x !== "none");
    return without.includes(id) ? without.filter((x) => x !== id) : [...without, id];
  });
  const finish = () => onComplete({ ...config, profile: { employment, sideBusiness, accounts, ownsHome, profiled: true } });

  const body = (
    <div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
      <div>
        <div style={hTitle}>First, a bit about you</div>
        <div style={hSub}>This tailors your tax tips and what Waffle teaches you — so you only see what actually applies. Takes 20 seconds.</div>
      </div>

      <div>
        <div style={lbl}>How do you earn?</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 9, marginTop: 4 }}>
          {EMPLOYMENT_OPTS.map((o) => <OptionRow key={o.id} active={employment === o.id} onClick={() => setEmployment(o.id)} title={o.title} sub={o.sub} />)}
        </div>
      </div>

      <div style={{ ...card, padding: "16px 18px", display: "flex", alignItems: "center", gap: 14 }}>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 15.5, fontWeight: 600 }}>Side gig or business?</div>
          <div style={{ fontSize: 13, color: T.dim, marginTop: 2, lineHeight: 1.4 }}>Freelance, 1099 work, anything you owe taxes on yourself.</div>
        </div>
        <Toggle on={sideBusiness} onChange={() => setSideBusiness((v) => !v)} />
      </div>

      <div>
        <div style={lbl}>Accounts you have <span style={{ color: T.faint, fontWeight: 500, textTransform: "none" }}>· tap any</span></div>
        <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginTop: 4 }}>
          {ACCOUNT_OPTS.map((a) => <Chip key={a.id} active={accounts.includes(a.id)} onClick={() => toggleAcct(a.id)} label={a.label} />)}
        </div>
      </div>

      <div style={{ ...card, padding: "16px 18px", display: "flex", alignItems: "center", gap: 14 }}>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 15.5, fontWeight: 600 }}>Do you own a home?</div>
          <div style={{ fontSize: 13, color: T.dim, marginTop: 2, lineHeight: 1.4 }}>Unlocks equity-related tips down the road.</div>
        </div>
        <Toggle on={ownsHome} onChange={() => setOwnsHome((v) => !v)} />
      </div>
    </div>
  );

  if (embedded) {
    return (
      <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 60, background: T.overlay, 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(20px + env(safe-area-inset-bottom))", maxHeight: "94vh", overflowY: "auto" }}>
          <div style={{ width: 38, height: 5, borderRadius: 3, background: T.grip, margin: "0 auto 16px" }} />
          {body}
          <button className="press" onClick={finish} style={{ ...pill, width: "100%", marginTop: 22 }}>Save</button>
        </div>
      </div>
    );
  }
  return (
    <div className="back" style={{ position: "fixed", inset: 0, zIndex: 70, background: T.bg, display: "flex", justifyContent: "center" }}>
      <div className="vin" style={{ width: "100%", maxWidth: 480, display: "flex", flexDirection: "column", height: "100%" }}>
        <div style={{ flex: 1, overflowY: "auto", padding: "calc(env(safe-area-inset-top) + 28px) 20px 8px" }}>{body}</div>
        <div style={{ padding: "10px 20px calc(18px + env(safe-area-inset-bottom))", borderTop: `1px solid ${T.sep}`, background: T.bg }}>
          <button className="press" onClick={finish} style={{ ...pill, width: "100%" }}>Continue</button>
        </div>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  SYRUP WELCOME — conversational onboarding, replaces ProfileSetup  */
/* ================================================================== */
function SyrupWelcome({ config, saveConfig, onComplete }) {
  const [msgs, setMsgs] = useState([]);
  const [pending, setPending] = useState(false);
  const [input, setInput] = useState("");
  const scrollRef = useRef(null);
  const started = useRef(false);

  const welcomeSystem = `You are Syrup, the AI money coach inside Waffle. Right now you're meeting a NEW user for the first time. Your job is to learn about them through a warm, natural conversation — NOT an interrogation.

Your goals (in order):
1. Learn what they do for work (employment type, job title)
2. Learn how their income works (steady paycheck? freelance? tips? seasonal?)
3. Learn what matters most to them financially right now (saving for something? paying off debt? just trying not to overspend?)
4. Learn any pain points (taxes are confusing? income is unpredictable? no savings?)

Rules:
- Be warm, casual, and brief. 2-3 sentences max per reply.
- Ask only ONE question per reply.
- Make them feel heard — reflect back what they say before asking the next thing.
- Use **bold** for emphasis sparingly.
- After each reply, emit a learn_about_user action with what you've learned so far.
- Do NOT propose budgets, goals, or plan changes yet — that comes next.
- After 3-4 exchanges when you have a good picture, say something like "I've got a solid picture — let's build your plan around this." and DON'T ask another question.

SILENT action format (the user never sees this):
[[ACTION]]{"type":"learn_about_user","updates":{"occupation":"...","incomeType":"...","painPoints":[...],"priorities":[...]},"summary":"Updated profile"}[[/ACTION]]

Start by introducing yourself warmly and asking what they do for work. Keep it human.`;

  useEffect(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [msgs, pending]);

  const send = async (text) => {
    if (!text.trim() || pending) return;
    const history = msgs.map((m) => ({ role: m.role === "user" ? "user" : "assistant", content: m.text }));
    setMsgs((m) => [...m, { role: "user", text: text.trim() }]);
    setInput("");
    setPending(true);
    try {
      const raw = await callClaudeMessages([...history, { role: "user", content: text.trim() }], welcomeSystem);
      const { prose, actions } = extractAction(raw);
      // Apply silent profile updates
      actions.filter((a) => a.type === "learn_about_user" && a.updates).forEach((sa) => {
        const profile = { ...(config.profile || {}), ...sa.updates, profiled: true };
        saveConfig({ ...config, profile });
      });
      setMsgs((m) => [...m, { role: "assistant", text: prose || "Got it!" }]);
    } catch (e) {
      setMsgs((m) => [...m, { role: "assistant", text: "Hmm, had trouble connecting. Try again?" }]);
    } finally { setPending(false); }
  };

  // Auto-start the conversation
  useEffect(() => {
    if (started.current) return;
    started.current = true;
    (async () => {
      setPending(true);
      try {
        const raw = await callClaudeMessages([{ role: "user", content: "Hi, I just signed up!" }], welcomeSystem);
        const { prose } = extractAction(raw);
        setMsgs([{ role: "assistant", text: prose || "Hey! I'm Syrup, your money coach. What do you do for work?" }]);
      } catch (_) {
        setMsgs([{ role: "assistant", text: "Hey! 👋 I'm Syrup, your money coach. Tell me a bit about yourself — what do you do for work?" }]);
      } finally { setPending(false); }
    })();
  }, []);

  const canContinue = msgs.filter((m) => m.role === "user").length >= 2;

  return (
    <div className="back" style={{ position: "fixed", inset: 0, zIndex: 70, background: T.bg, display: "flex", justifyContent: "center" }}>
      <div className="vin" style={{ width: "100%", maxWidth: 480, display: "flex", flexDirection: "column", height: "100%" }}>
        {/* Header */}
        <div style={{ padding: "calc(env(safe-area-inset-top) + 20px) 20px 12px", textAlign: "center" }}>
          <div style={{ width: 52, height: 52, borderRadius: 26, background: T.waffle + "26", display: "grid", placeItems: "center", margin: "0 auto 10px" }}><ISpark size={26} color={T.waffle} /></div>
          <div style={{ fontSize: 24, fontWeight: 800, letterSpacing: "-0.02em" }}>Meet Syrup</div>
          <div style={{ fontSize: 14, color: T.dim, marginTop: 4 }}>Your AI money coach. Let's get to know each other.</div>
        </div>

        {/* Chat */}
        <div ref={scrollRef} style={{ flex: 1, overflowY: "auto", padding: "8px 18px", display: "flex", flexDirection: "column", gap: 10 }}>
          {msgs.map((m, i) => (
            <div key={i} className="msgin" style={{ display: "flex", justifyContent: m.role === "user" ? "flex-end" : "flex-start" }}>
              <div style={{ maxWidth: "86%", padding: "10px 13px", borderRadius: 16, fontSize: 14.5, lineHeight: 1.5, whiteSpace: "pre-wrap", background: m.role === "user" ? T.blue : T.fill, color: m.role === "user" ? "#fff" : T.text, borderBottomRightRadius: m.role === "user" ? 5 : 16, borderBottomLeftRadius: m.role === "user" ? 16 : 5 }}>
                {m.role === "user" ? m.text : <RichText text={m.text} />}
              </div>
            </div>
          ))}
          {pending && (
            <div className="msgin" style={{ display: "flex", justifyContent: "flex-start" }}>
              <div style={{ padding: "12px 14px", borderRadius: 16, borderBottomLeftRadius: 5, background: T.fill, display: "flex", gap: 5 }}>
                {[0, 1, 2].map((d) => <span key={d} className="dot" style={{ width: 7, height: 7, borderRadius: 4, background: T.faint, animationDelay: `${d * 0.16}s` }} />)}
              </div>
            </div>
          )}
        </div>

        {/* Input + continue */}
        <div style={{ padding: "10px 18px calc(16px + env(safe-area-inset-bottom))", borderTop: `1px solid ${T.sep}`, background: T.bg }}>
          <div style={{ display: "flex", gap: 8, alignItems: "flex-end", marginBottom: canContinue ? 10 : 0 }}>
            <textarea value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(input); } }} placeholder="Tell Syrup about yourself…" rows={1} style={{ flex: 1, resize: "none", background: T.fill, border: "1px solid transparent", borderRadius: 14, padding: "11px 14px", color: T.text, fontSize: 15, lineHeight: 1.4, maxHeight: 100 }} />
            <button className="press" onClick={() => send(input)} disabled={pending || !input.trim()} aria-label="Send" style={{ width: 44, height: 44, borderRadius: 14, border: "none", cursor: input.trim() ? "pointer" : "default", background: input.trim() ? T.blue : T.fill2, color: input.trim() ? "#fff" : T.faint, display: "grid", placeItems: "center", flexShrink: 0 }}>
              {pending ? <ISpin size={18} className="spin" color={input.trim() ? "#fff" : T.faint} /> : <ISend size={19} sw={2} color={input.trim() ? "#fff" : T.faint} />}
            </button>
          </div>
          {canContinue && (
            <button className="press" onClick={onComplete} style={{ ...pill, width: "100%", opacity: 1 }}>Continue to plan setup →</button>
          )}
        </div>
      </div>
    </div>
  );
}

function Onboarding({ config, metrics, onClose, onComplete }) {
  const essentialCats = (config.categories || []).filter((c) => c.essential);
  // Welcome slides (0,1) then inputs (2,3,4,5) then read (6)
  const LAST = 6;
  const [step, setStep] = useState(0);
  const [lean, setLean] = useState(metrics.incLow ? String(Math.round(metrics.incLow)) : "");
  const [typical, setTypical] = useState(metrics.projIncome ? String(Math.round(metrics.projIncome)) : "");
  const [ess, setEss] = useState(() => Object.fromEntries(essentialCats.map((c) => [c.name, String(config.budgets[c.name] || 0)])));
  const [savingsPct, setSavingsPct] = useState(config.plan && config.plan.savingsPct ? config.plan.savingsPct : 15);
  const [bufferMonths, setBufferMonths] = useState(config.plan && config.plan.bufferMonths ? config.plan.bufferMonths : 3);
  const existingGoal = (config.goals || [])[0];
  const [goalName, setGoalName] = useState(existingGoal ? existingGoal.name : "");
  const [goalTarget, setGoalTarget] = useState(existingGoal && existingGoal.target ? String(existingGoal.target) : "");
  const [note, setNote] = useState(""); const [noteState, setNoteState] = useState("idle");

  const leanN = Number(lean) || 0, typN = Number(typical) || 0;
  const essFloor = Object.values(ess).reduce((a, b) => a + (Number(b) || 0), 0);
  const planIncome = leanN || typN || 0;
  const savingsTarget = Math.round(planIncome * savingsPct / 100);
  const flexAvail = Math.max(0, planIncome - essFloor - savingsTarget);
  const tooHigh = planIncome > 0 && essFloor > planIncome;

  const runNote = async () => {
    setNoteState("loading");
    try {
      const prompt = `You are Syrup, Waffle's money coach. A user with variable, irregular income just set up their plan. Numbers: lean-month income $${leanN}, fixed essentials $${essFloor}/mo, saving ${savingsPct}% (about $${savingsTarget}/mo), leaving $${flexAvail} flexible, buffer goal ${bufferMonths} months of essentials. ${tooHigh ? "IMPORTANT: their essentials are HIGHER than a lean month — flag this gently as the first thing to fix." : ""} Write at most 2 sentences, under 45 words, warm but honest, speak to "you", and put **bold** on the single most important number or move. No greeting, no preamble.`;
      setNote(await callClaude(prompt)); setNoteState("done");
    } catch (e) { setNoteState("error"); }
  };
  useEffect(() => { if (step === LAST && noteState === "idle") runNote(); }, [step]);

  const finish = () => {
    const newBudgets = { ...config.budgets };
    Object.entries(ess).forEach(([k, v]) => { newBudgets[k] = Number(v) || 0; });
    const plan = { ...(config.plan || {}), basis: "conservative", leanIncome: leanN, typicalIncome: typN, savingsPct, bufferMonths, bufferSaved: (config.plan && config.plan.bufferSaved) || 0 };
    const nameT = goalName.trim(); const targetN = parseFloat(goalTarget) || 0;
    let goals = config.goals || []; let primaryGoalId = config.primaryGoalId;
    if (nameT && targetN > 0) {
      if (existingGoal) {
        goals = goals.map((g) => g.id === existingGoal.id ? { ...g, name: nameT, target: targetN } : g);
      } else {
        const id = "g" + Date.now().toString(36);
        goals = [{ id, name: nameT, target: targetN, saved: 0 }, ...goals];
        primaryGoalId = primaryGoalId || id;
      }
    } else if (!goals.length) {
      // Strongly suggest, but don't force — warn once on skip.
      if (!window.confirm("Heads up — without a goal, Waffle can't show you what you're saving toward or how close you are. Add one now? (Tap Cancel to skip for now.)")) {
        // user chose to skip; proceed without a goal
      } else {
        return; // stay on the step so they can fill it in
      }
    }
    onComplete({ ...config, budgets: newBudgets, plan, goals, primaryGoalId, onboarded: true });
  };
  const next = () => (step < LAST ? setStep(step + 1) : finish());
  const back = () => step > 0 && setStep(step - 1);
  const canNext = step !== 3 || planIncome > 0; // income step requires a number
  const fallbackNote = tooHigh
    ? `Heads up — your essentials (${fmt(essFloor)}) run higher than a lean month (${fmt(planIncome)}). Closing that gap comes before saving.`
    : `Saving ${savingsPct}% of a lean month sets aside about ${fmt(savingsTarget)}, leaving ${fmt(flexAvail)} to flex. Good months can do more.`;

  // Live preview shown from the income step onward, so the plan visibly builds.
  const showLive = step >= 3 && step <= 5 && planIncome > 0;
  const livePreview = (
    <div className="vin" style={{ ...card, padding: "16px 18px", marginTop: 18 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 12 }}>
        <span style={{ fontSize: 12.5, color: T.faint, fontWeight: 700, letterSpacing: ".04em" }}>YOUR PLAN SO FAR</span>
        <span style={{ fontSize: 13, color: T.dim, fontVariantNumeric: "tabular-nums" }}>{fmt(planIncome)}/mo</span>
      </div>
      <StackBar parts={[{ label: "Essentials", value: essFloor, color: T.blue }, { label: "Savings", value: savingsTarget, color: T.green }, { label: "Flex", value: Math.max(0, flexAvail), color: T.waffle }]} />
      <div style={{ display: "flex", gap: 14, marginTop: 12, flexWrap: "wrap" }}>
        <LiveBit color={T.blue} label="Essentials" amount={essFloor} />
        {step >= 4 && <LiveBit color={T.green} label="Savings" amount={savingsTarget} />}
        {step >= 4 && <LiveBit color={T.waffle} label="Flex" amount={Math.max(0, flexAvail)} />}
      </div>
    </div>
  );

  return (
    <div className="back" style={{ position: "fixed", inset: 0, zIndex: 70, background: T.bg, display: "flex", justifyContent: "center" }}>
      <div className="vin" style={{ width: "100%", maxWidth: 480, display: "flex", flexDirection: "column", height: "100%" }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "calc(env(safe-area-inset-top) + 18px) 18px 6px" }}>
          <div style={{ display: "flex", gap: 6 }}>
            {[0, 1, 2, 3, 4, 5, 6].map((d) => <span key={d} style={{ width: d === step ? 20 : 7, height: 7, borderRadius: 4, background: d <= step ? T.ink : T.fill2, transition: "all .25s" }} />)}
          </div>
          <button className="press" onClick={onClose} style={{ background: "none", border: "none", color: T.dim, fontSize: 14, fontWeight: 600, cursor: "pointer" }}>Skip</button>
        </div>

        <div key={step} className="pushin" style={{ flex: 1, overflowY: "auto", padding: "18px 20px 8px" }}>
          {step === 0 && (
            <div style={{ paddingTop: 24 }}>
              <div style={{ width: 64, height: 64, borderRadius: 32, background: T.waffle + "26", display: "grid", placeItems: "center", marginBottom: 22 }}><ISpark size={32} color={T.waffle} /></div>
              <div style={{ fontSize: 30, fontWeight: 800, letterSpacing: "-0.02em", marginBottom: 12 }}>Welcome to Waffle</div>
              <div style={{ fontSize: 16.5, color: T.dim, lineHeight: 1.55 }}>Most budgeting apps assume a steady paycheck. Waffle is built for income that moves — freelance, commission, tips, seasonal work, side gigs, or just months that don't look alike. It plans around your <b style={{ color: T.text }}>lean</b> months so a slow stretch never breaks you.</div>
            </div>
          )}
          {step === 1 && (
            <div style={{ paddingTop: 24 }}>
              <div style={{ fontSize: 30, fontWeight: 800, letterSpacing: "-0.02em", marginBottom: 12 }}>This starts empty</div>
              <div style={{ fontSize: 16.5, color: T.dim, lineHeight: 1.55, marginBottom: 18 }}>No fake demo data — it's yours from the first number. Answer a few quick questions and watch your plan build itself as you go. Then you'll log real income and spending, and everything fills in.</div>
              <div style={{ ...card, padding: "16px 18px", display: "flex", gap: 12, alignItems: "center" }}>
                <ISpark size={20} color={T.orange} />
                <div style={{ fontSize: 14, color: T.dim, lineHeight: 1.45 }}>Takes about a minute. You can change any of it later.</div>
              </div>
            </div>
          )}
          {step === 2 && (
            <div>
              <div style={hTitle}>What's your money for?</div>
              <div style={hSub}>The split you're about to build feeds your goals. Don't overthink it — you'll set actual goals next, and Syrup can help anytime.</div>
              <div style={{ ...card, padding: "18px 20px" }}>
                <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
                  <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}><span style={{ width: 11, height: 11, borderRadius: 6, background: T.blue, marginTop: 4, flexShrink: 0 }} /><div><div style={{ fontSize: 15.5, fontWeight: 600 }}>Essentials</div><div style={{ fontSize: 13, color: T.dim }}>The fixed must-pays. Funded first, every month.</div></div></div>
                  <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}><span style={{ width: 11, height: 11, borderRadius: 6, background: T.green, marginTop: 4, flexShrink: 0 }} /><div><div style={{ fontSize: 15.5, fontWeight: 600 }}>Savings</div><div style={{ fontSize: 13, color: T.dim }}>Set aside before you can spend it.</div></div></div>
                  <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}><span style={{ width: 11, height: 11, borderRadius: 6, background: T.waffle, marginTop: 4, flexShrink: 0 }} /><div><div style={{ fontSize: 15.5, fontWeight: 600 }}>Flex</div><div style={{ fontSize: 13, color: T.dim }}>Whatever's left — yours to spend freely.</div></div></div>
                </div>
              </div>
            </div>
          )}
          {step === 3 && (
            <div>
              <div style={hTitle}>What does a month bring in?</div>
              <div style={hSub}>Give a low end and a typical end. We plan off the low one to keep you safe.</div>
              <Field label="A lean month"><div style={{ position: "relative" }}><span style={dollar}>$</span><input value={lean} onChange={(e) => setLean(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="0" inputMode="decimal" style={{ ...inp, paddingLeft: 26 }} /></div></Field>
              <Field label="A typical month"><div style={{ position: "relative" }}><span style={dollar}>$</span><input value={typical} onChange={(e) => setTypical(e.target.value.replace(/[^0-9.]/g, ""))} placeholder="0" inputMode="decimal" style={{ ...inp, paddingLeft: 26 }} /></div></Field>
              {showLive && livePreview}
            </div>
          )}
          {step === 4 && (
            <div>
              <div style={hTitle}>Your fixed monthly costs</div>
              <div style={hSub}>The must-pays — rent, car, insurance, utilities, a groceries floor.</div>
              {essentialCats.length ? essentialCats.map((c) => (
                <div key={c.name} style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 12 }}>
                  <span style={{ width: 11, height: 11, borderRadius: 6, background: c.color, flexShrink: 0 }} />
                  <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500 }}>{c.name}</span>
                  <div style={{ position: "relative", width: 120 }}><span style={{ position: "absolute", left: 12, top: 11, color: T.faint, fontSize: 14 }}>$</span><input value={ess[c.name]} onChange={(e) => setEss((m) => ({ ...m, [c.name]: cleanNumStr(e.target.value) }))} inputMode="decimal" style={{ ...inp, padding: "10px 12px 10px 22px", textAlign: "right" }} /></div>
                </div>
              )) : <div style={{ fontSize: 14, color: T.faint }}>No categories marked essential yet — you can set those in the Plan tab.</div>}
              <div style={{ display: "flex", justifyContent: "space-between", marginTop: 14, paddingTop: 14, borderTop: `1px solid ${T.sep}`, fontSize: 15.5, fontWeight: 700 }}><span>Essentials floor</span><span style={{ fontVariantNumeric: "tabular-nums" }}>{fmt(essFloor)}</span></div>
              {showLive && livePreview}
            </div>
          )}
          {step === 5 && (
            <div>
              <div style={hTitle}>How hard do you want to save?</div>
              <div style={hSub}>A slice of every dollar that lands, set aside first. Watch the green grow.</div>
              <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
                <OptionRow active={savingsPct === 8} onClick={() => setSavingsPct(8)} title="Easy does it" sub="8% — gentle, room to breathe" />
                <OptionRow active={savingsPct === 15} onClick={() => setSavingsPct(15)} title="Balanced" sub="15% — steady progress" />
                <OptionRow active={savingsPct === 25} onClick={() => setSavingsPct(25)} title="Aggressive" sub="25% — goals first, lean on flex" />
              </div>
              <div style={{ marginTop: 18 }}>
                <div style={lbl}>Safety cushion</div>
                <div style={{ display: "flex", gap: 8 }}>
                  {[1, 3, 6].map((m) => <button key={m} className="press" onClick={() => setBufferMonths(m)} style={{ flex: 1, border: "none", cursor: "pointer", padding: "12px 0", borderRadius: 12, fontSize: 14, fontWeight: 600, background: bufferMonths === m ? T.ink : T.fill2, color: bufferMonths === m ? T.bg : T.dim }}>{m} mo</button>)}
                </div>
                <div style={{ fontSize: 12.5, color: T.faint, marginTop: 8 }}>Months of essentials kept in reserve for slow stretches.</div>
              </div>
              {showLive && livePreview}
            </div>
          )}
          {step === 6 && (
            <div>
              <div style={hTitle}>Your plan's ready</div>
              <div style={{ ...card, padding: "18px 20px", marginBottom: 14 }}>
                <div style={{ fontSize: 13, color: T.dim, fontWeight: 600 }}>Planning on a lean month</div>
                <div style={{ fontSize: 34, fontWeight: 800, letterSpacing: "-0.02em", marginTop: 2, marginBottom: 14, fontVariantNumeric: "tabular-nums" }}>{fmt(planIncome)}</div>
                <StackBar parts={[{ label: "Essentials", value: essFloor, color: T.blue }, { label: "Savings", value: savingsTarget, color: T.green }, { label: "Flex", value: flexAvail, color: T.waffle }]} />
                <div style={{ display: "flex", flexDirection: "column", gap: 10, marginTop: 16 }}>
                  <PlanRow color={T.blue} label="Essentials" sub="Funded first" amount={essFloor} pct={planIncome ? Math.round(essFloor / planIncome * 100) : 0} />
                  <PlanRow color={T.green} label="Savings & goals" sub={`${savingsPct}% set aside`} amount={savingsTarget} pct={savingsPct} />
                  <PlanRow color={T.waffle} label="Flex" sub="What's left to spend" amount={flexAvail} pct={planIncome ? Math.round(flexAvail / planIncome * 100) : 0} />
                </div>
              </div>
              <div style={{ ...card, padding: "16px 18px", display: "flex", gap: 10 }}>
                <ISpark size={18} color={tooHigh ? T.red : T.orange} />
                <div style={{ fontSize: 14, lineHeight: 1.5, color: tooHigh ? T.red : T.text }}>
                  {noteState === "loading" ? <span style={{ color: T.dim }}>Reading your numbers…</span> : <RichText text={noteState === "done" && note ? note : fallbackNote} />}
                </div>
              </div>
              <div style={{ ...card, padding: "18px 20px", marginTop: 14 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
                  <IStar size={17} color={T.waffle} fillIn />
                  <span style={{ fontSize: 15.5, fontWeight: 700 }}>Add your first goal</span>
                </div>
                <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.45, marginBottom: 14 }}>This is what makes the plan yours — a trip, an emergency fund, a big purchase. Your savings stack toward it. You can change it anytime.</div>
                <Field label="What are you saving for?"><input value={goalName} onChange={(e) => setGoalName(e.target.value)} placeholder="Emergency fund, vacation, new car…" style={inp} /></Field>
                <Field label="Target amount"><div style={{ position: "relative" }}><span style={dollar}>$</span><input value={goalTarget} onChange={(e) => setGoalTarget(cleanNumStr(e.target.value))} placeholder="5,000" inputMode="numeric" style={{ ...inp, paddingLeft: 26 }} /></div></Field>
              </div>
              <div style={{ fontSize: 13, color: T.faint, lineHeight: 1.5, marginTop: 14, paddingLeft: 2 }}>Then just start logging — your Summary and Income tabs fill in as you go.</div>
            </div>
          )}
        </div>

        <div style={{ display: "flex", gap: 10, padding: "10px 20px calc(18px + env(safe-area-inset-bottom))", borderTop: `1px solid ${T.sep}`, background: T.bg }}>
          {step > 0 && <button className="press" onClick={back} style={{ ...pill, flex: "0 0 auto", background: T.fill2, color: T.text, padding: "15px 22px" }}>Back</button>}
          <button className="press" onClick={next} disabled={!canNext} style={{ ...pill, flex: 1, opacity: canNext ? 1 : 0.5 }}>{step === 0 ? "Get started" : step === LAST ? "Use this plan" : "Next"}</button>
        </div>
      </div>
    </div>
  );
}
function LiveBit({ color, label, amount }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
      <span style={{ width: 9, height: 9, borderRadius: 5, background: color, flexShrink: 0 }} />
      <span style={{ fontSize: 12.5, color: T.dim }}>{label}</span>
      <span style={{ fontSize: 13, fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{fmt(amount)}</span>
    </div>
  );
}
const hTitle = { fontSize: 23, fontWeight: 800, letterSpacing: "-0.02em", marginBottom: 8 };
const hSub = { fontSize: 14.5, color: T.dim, lineHeight: 1.5, marginBottom: 18 };
const dollar = { position: "absolute", left: 14, top: 13, color: T.faint, fontWeight: 600 };
const stepBtn = { width: 28, height: 28, borderRadius: 14, border: `1px solid ${T.sep}`, background: T.card, color: T.text, fontSize: 18, fontWeight: 600, cursor: "pointer", lineHeight: 1, display: "grid", placeItems: "center", flexShrink: 0 };

/* ================================================================== */
/*  CONTRIBUTION — confirm money into the savings pool (waterfall)     */
/* ================================================================== */
function ContributionSheet({ config, metrics, defaultAmount, assignMode, onClose, onApply }) {
  const unassigned = config.savingsUnassigned || 0;
  const goalIds = goalOrder(config);
  const [amount, setAmount] = useState(defaultAmount ? String(defaultAmount) : "");
  const [dest, setDest] = useState(assignMode ? (goalIds[0] || "") : ""); // assign requires a goal; contribute defaults to cascade
  let amt = parseFloat(amount) || 0;
  if (assignMode) amt = Math.min(amt, unassigned); // can't assign more than is unassigned
  const { adds, leftover } = distributeSavings(amt, config, dest || undefined);
  const poolNow = (config.goals || []).reduce((a, g) => a + g.saved, 0) + (config.savingsUnassigned || 0);
  const destGoal = (config.goals || []).find((g) => g.id === dest);
  const canApply = assignMode ? (amt > 0 && !!dest) : amt > 0;
  return (
    <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 58, background: T.overlay, 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: "92vh", overflowY: "auto" }}>
        <div style={{ width: 38, height: 5, borderRadius: 3, background: T.grip, margin: "0 auto 12px" }} />
        <div style={{ fontSize: 22, fontWeight: 800, letterSpacing: "-0.02em", marginBottom: 4 }}>{assignMode ? "Assign savings" : "Add to savings"}</div>
        <div style={{ fontSize: 13.5, color: T.dim, marginBottom: 16, lineHeight: 1.45 }}>{assignMode ? `Move part of your ${fmt(unassigned)} unassigned savings into a goal.` : "Money you've actually set aside. Send it to one goal, or let it cascade from your primary down."}</div>
        <Field label="Amount">
          <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" autoFocus style={{ ...inp, paddingLeft: 26, fontVariantNumeric: "tabular-nums" }} /></div>
          {assignMode ? <div style={{ fontSize: 12.5, color: T.faint, marginTop: 8 }}>Up to {fmt(unassigned)} available to assign.</div>
            : (defaultAmount > 0 && <div style={{ fontSize: 12.5, color: T.faint, marginTop: 8 }}>Suggested from your plan — change it to whatever you really moved.</div>)}
        </Field>

        <div style={{ marginBottom: 16 }}>
          <div style={lbl}>{assignMode ? "Into goal" : "Send to"}</div>
          <div style={{ display: "flex", gap: 7, flexWrap: "wrap", marginTop: 2 }}>
            {!assignMode && <Chip active={!dest} onClick={() => setDest("")} label="Auto · cascade" />}
            {goalIds.map((id) => { const g = (config.goals || []).find((x) => x.id === id); return g ? <Chip key={id} active={dest === id} onClick={() => setDest(id)} label={g.name} /> : null; })}
          </div>
          {destGoal && (destGoal.target - destGoal.saved) > 0 && amt > (destGoal.target - destGoal.saved) && (
            <div style={{ fontSize: 12.5, color: T.faint, marginTop: 9 }}>{destGoal.name} only needs {fmt(destGoal.target - destGoal.saved)} to hit its target — the rest {assignMode ? "stays unassigned" : "waits in unassigned"}.</div>
          )}
        </div>

        {amt > 0 && (
          <div style={{ ...card, padding: "14px 16px", marginBottom: 16 }}>
            <div style={{ fontSize: 12.5, color: T.faint, fontWeight: 600, marginBottom: 10, letterSpacing: ".03em" }}>WHERE IT GOES</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
              {adds.length ? adds.map((a) => (
                <div key={a.id} style={{ display: "flex", justifyContent: "space-between", fontSize: 14.5 }}><span>{a.name}</span><span style={{ fontWeight: 700, color: T.green, fontVariantNumeric: "tabular-nums" }}>+{fmt(a.amount)}</span></div>
              )) : <div style={{ fontSize: 14, color: T.faint }}>{dest ? "This goal is already full." : "No open goals — it'll wait in unassigned."}</div>}
              {!assignMode && leftover > 0 && <div style={{ display: "flex", justifyContent: "space-between", fontSize: 14.5, color: T.dim }}><span>Unassigned</span><span style={{ fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>+{fmt(leftover)}</span></div>}
            </div>
            {!assignMode && <div style={{ display: "flex", justifyContent: "space-between", marginTop: 12, paddingTop: 10, borderTop: `1px solid ${T.sep}`, fontSize: 13.5, color: T.dim }}><span>Pool after</span><span style={{ fontWeight: 700, color: T.text, fontVariantNumeric: "tabular-nums" }}>{fmt(poolNow + amt)}</span></div>}
            {assignMode && <div style={{ display: "flex", justifyContent: "space-between", marginTop: 12, paddingTop: 10, borderTop: `1px solid ${T.sep}`, fontSize: 13.5, color: T.dim }}><span>Unassigned after</span><span style={{ fontWeight: 700, color: T.text, fontVariantNumeric: "tabular-nums" }}>{fmt(Math.max(0, unassigned - amt))}</span></div>}
          </div>
        )}

        <button className="press" onClick={() => onApply(amt, dest || undefined)} disabled={!canApply} style={{ ...pill, width: "100%", opacity: canApply ? 1 : 0.5 }}>{assignMode ? (amt ? `Assign ${fmt(amt)}` : "Assign savings") : (amt ? `Add ${fmt(amt)}` : "Add to savings")}</button>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  EDIT — change a logged expense or income entry                     */
/* ================================================================== */
function EditSheet({ kind, rec, config, onClose, onSave, onDelete }) {
  const catNames = (config.categories || []).map((c) => c.name);
  const [who, setWho] = useState(rec.who || "");
  const [source, setSource] = useState(rec.source || "");
  const [amount, setAmount] = useState(rec.amount != null ? String(rec.amount) : "");
  const [date, setDate] = useState(rec.date || new Date().toISOString().slice(0, 10));
  const [category, setCategory] = useState(catNames.includes(rec.category) ? rec.category : (catNames[0] || "Other"));
  const [sub, setSub] = useState(rec.sub || "");
  const subList = SUBS[category] || [];
  const save = () => {
    const amt = parseFloat(amount); if (!amt) return;
    if (kind === "income") { if (!source.trim()) return; onSave({ ...rec, source: source.trim(), amount: amt, date }); }
    else { if (!who.trim()) return; onSave({ ...rec, who: who.trim(), amount: amt, date, category, sub: sub || undefined }); }
  };
  return (
    <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 58, background: T.overlay, 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: T.grip, margin: "0 auto 12px" }} />
        <div style={{ fontSize: 22, fontWeight: 800, letterSpacing: "-0.02em", marginBottom: 16 }}>Edit {kind === "income" ? "income" : "expense"}</div>
        {rec.recId && <div style={{ fontSize: 12.5, color: T.faint, marginTop: -10, marginBottom: 14 }}>From a recurring entry — editing changes just this one. Stop the series in Budgets → Recurring.</div>}
        {kind === "income" ? (
          <Field label="Source"><input value={source} onChange={(e) => setSource(e.target.value)} style={inp} /></Field>
        ) : (
          <Field label="Merchant"><input value={who} onChange={(e) => setWho(e.target.value)} 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, ""))} 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>
        {kind !== "income" && (
          <>
            <Field label="Category"><select value={category} onChange={(e) => { setCategory(e.target.value); setSub(""); }} style={inp}>{catNames.map((cn) => <option key={cn} value={cn}>{cn}</option>)}</select></Field>
            {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>
            )}
          </>
        )}
        <button className="press" onClick={save} style={{ ...pill, width: "100%", marginTop: 4 }}>Save changes</button>
        <button className="press" onClick={() => onDelete(rec.id)} style={{ width: "100%", marginTop: 10, background: "none", border: "none", color: T.red, fontSize: 14.5, fontWeight: 600, cursor: "pointer", padding: 8 }}>Delete this entry</button>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  NET WORTH — structure built, gated behind a blurred preview        */
/* ================================================================== */
const SAMPLE_ASSETS = [
  { name: "Checking & savings", amount: 14200, color: T.green },
  { name: "Retirement (401k / IRA)", amount: 68500, color: T.blue },
  { name: "Brokerage", amount: 22300, color: T.purple || "#5856D6" },
  { name: "Home", amount: 415000, color: T.waffle },
  { name: "Vehicle", amount: 18000, color: T.orange },
];
const SAMPLE_LIABS = [
  { name: "Mortgage", amount: 298000, color: T.red },
  { name: "Auto loan", amount: 11200, color: T.red },
  { name: "Credit cards", amount: 2400, color: T.red },
];
function NetWorthView({ onLearn }) {
  const assets = SAMPLE_ASSETS.reduce((a, b) => a + b.amount, 0);
  const liabs = SAMPLE_LIABS.reduce((a, b) => a + b.amount, 0);
  const net = assets - liabs;
  const Blurred = ({ children }) => <div style={{ filter: "blur(7px)", pointerEvents: "none", userSelect: "none" }} aria-hidden="true">{children}</div>;
  return (
    <div style={{ position: "relative" }}>
      <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
        <Blurred>
          <div style={{ ...card, padding: "22px" }}>
            <div style={{ fontSize: 13, color: T.dim, fontWeight: 600 }}>Net worth</div>
            <div style={{ fontSize: 42, fontWeight: 800, letterSpacing: "-0.03em", marginTop: 4, fontVariantNumeric: "tabular-nums" }}>{fmt(net)}</div>
            <div style={{ display: "flex", gap: 16, marginTop: 10 }}>
              <span style={{ fontSize: 13.5, color: T.dim }}>Assets <b style={{ color: T.green }}>{fmt(assets)}</b></span>
              <span style={{ fontSize: 13.5, color: T.dim }}>Debts <b style={{ color: T.red }}>{fmt(liabs)}</b></span>
            </div>
          </div>
        </Blurred>
        <Blurred>
          <Group label="Assets">
            {SAMPLE_ASSETS.map((a, i) => (
              <Row key={a.name} last={i === SAMPLE_ASSETS.length - 1}>
                <span style={{ width: 11, height: 11, borderRadius: 6, background: a.color, flexShrink: 0 }} />
                <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500 }}>{a.name}</span>
                <span style={{ fontSize: 14.5, fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>{fmt(a.amount)}</span>
              </Row>
            ))}
          </Group>
        </Blurred>
        <Blurred>
          <Group label="Liabilities">
            {SAMPLE_LIABS.map((a, i) => (
              <Row key={a.name} last={i === SAMPLE_LIABS.length - 1}>
                <span style={{ width: 11, height: 11, borderRadius: 6, background: a.color, flexShrink: 0 }} />
                <span style={{ flex: 1, fontSize: 15.5, fontWeight: 500 }}>{a.name}</span>
                <span style={{ fontSize: 14.5, fontWeight: 600, color: T.red, fontVariantNumeric: "tabular-nums" }}>−{fmt(a.amount)}</span>
              </Row>
            ))}
          </Group>
        </Blurred>
      </div>

      {/* Coming-soon overlay */}
      <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
        <div className="pop" style={{ ...card, padding: "24px 22px", textAlign: "center", maxWidth: 340, width: "100%" }}>
          <div style={{ width: 52, height: 52, borderRadius: 26, background: T.waffle + "26", display: "grid", placeItems: "center", margin: "0 auto 14px" }}><ILock size={24} color={T.waffle} /></div>
          <div style={{ fontSize: 12, fontWeight: 700, color: T.waffle, letterSpacing: ".06em", marginBottom: 6 }}>COMING SOON</div>
          <div style={{ fontSize: 20, fontWeight: 800, letterSpacing: "-0.01em", marginBottom: 8 }}>See your whole picture</div>
          <div style={{ fontSize: 14.5, color: T.dim, lineHeight: 1.55, marginBottom: 18 }}>Track every account, asset, and debt in one place — and watch your real net worth move. Unlocks when you connect your accounts, with secure read-only bank links.</div>
          <button className="press" onClick={onLearn} style={{ ...pill, width: "100%", background: T.fill2, color: T.text }}>Meanwhile, learn the moves that build it</button>
        </div>
      </div>
    </div>
  );
}

/* ================================================================== */
/*  LEARN — the finance gems library                                   */
/* ================================================================== */
const GEM_ICON = { ISpark, ITrend, ICheck, IWallet, IChart, ISliders, IHome };
function LearnView({ config, saveConfig, onCoach, onAskGem }) {
  const [open, setOpen] = useState(null);
  const prefs = config.prefs || DEFAULT_CONFIG.prefs;
  const read = prefs.gemsRead || [];
  const isRead = (id) => read.includes(id);
  const toggleRead = (id) => {
    const next = isRead(id) ? read.filter((x) => x !== id) : [...read, id];
    saveConfig({ ...config, prefs: { ...prefs, gemsRead: next } });
  };
  const visibleGems = gemsForProfile(config.profile);
  const total = visibleGems.length, done = visibleGems.filter((g) => isRead(g.id)).length;
  const pct = total ? Math.round((done / total) * 100) : 0;

  return (
    <div className="stagger" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
      {/* Progress header */}
      <div style={{ ...card, padding: "18px 20px" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", marginBottom: 4 }}>
          <span style={{ fontSize: 16, fontWeight: 800, letterSpacing: "-0.01em" }}>Build your money game</span>
          <span style={{ fontSize: 13, color: T.dim, fontVariantNumeric: "tabular-nums" }}>{done}/{total}</span>
        </div>
        <div style={{ fontSize: 13, color: T.dim, lineHeight: 1.45, marginBottom: 12 }}>Short, practical principles that build wealth beyond saving. Tap one, learn it, take the first step.</div>
        <div style={{ height: 7, borderRadius: 4, background: T.fill2, overflow: "hidden" }}><div className="bar" style={{ height: "100%", width: `${pct}%`, background: T.waffle, borderRadius: 4 }} /></div>
      </div>

      {/* Paths */}
      {LEARN_PATHS.map((p) => {
        const PIcon = GEM_ICON[p.icon] || ISpark;
        const gems = visibleGems.filter((g) => g.path === p.id);
        if (!gems.length) return null;
        const pDone = gems.filter((g) => isRead(g.id)).length;
        return (
          <div key={p.id}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "6px 4px 10px" }}>
              <span style={{ width: 30, height: 30, borderRadius: 15, background: T.waffle + "1A", display: "grid", placeItems: "center", flexShrink: 0 }}><PIcon size={16} color={T.waffle} /></span>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 15, fontWeight: 700 }}>{p.title}</div>
                <div style={{ fontSize: 12.5, color: T.faint }}>{p.sub}</div>
              </div>
              <span style={{ fontSize: 12, color: T.faint, fontVariantNumeric: "tabular-nums" }}>{pDone}/{gems.length}</span>
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              {gems.map((g) => {
                const Icon = GEM_ICON[g.icon] || ISpark;
                const isOpen = open === g.id; const done = isRead(g.id);
                return (
                  <div key={g.id} style={{ ...card, padding: "15px 17px", border: done ? `1px solid ${T.green}55` : "1px solid transparent" }}>
                    <div className="press" onClick={() => setOpen(isOpen ? null : g.id)} style={{ display: "flex", alignItems: "flex-start", gap: 12, cursor: "pointer" }}>
                      <span style={{ width: 34, height: 34, borderRadius: 17, background: done ? T.green + "1A" : T.waffle + "1A", display: "grid", placeItems: "center", flexShrink: 0 }}>{done ? <ICheck size={17} color={T.green} sw={2.4} /> : <Icon size={17} color={T.waffle} />}</span>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: 15, fontWeight: 700, lineHeight: 1.3 }}>{g.title}</div>
                        <div style={{ fontSize: 13, color: T.dim, marginTop: 3, lineHeight: 1.4 }}>{g.short}</div>
                      </div>
                      <span style={{ transform: isOpen ? "rotate(90deg)" : "none", transition: "transform .2s", flexShrink: 0, marginTop: 8 }}><IChevR size={17} color={T.faint} sw={2} /></span>
                    </div>
                    {isOpen && (
                      <div className="vin" style={{ marginTop: 12, paddingTop: 12, borderTop: `1px solid ${T.sep}` }}>
                        <div style={{ fontSize: 14, color: T.text, lineHeight: 1.6 }}>{g.body}</div>
                        <div style={{ marginTop: 12, padding: "12px 14px", borderRadius: 12, background: T.fill2 }}>
                          <div style={{ fontSize: 11.5, fontWeight: 700, color: T.faint, letterSpacing: ".04em", marginBottom: 5 }}>YOUR FIRST STEP</div>
                          <div style={{ fontSize: 13.5, color: T.text, lineHeight: 1.5 }}>{g.step}</div>
                        </div>
                        <div style={{ display: "flex", gap: 8, marginTop: 12 }}>
                          <button className="press" onClick={() => onAskGem(g)} style={{ ...pillSm, background: T.fill2, color: T.text, flex: 1 }}>Ask Syrup</button>
                          <button className="press" onClick={() => toggleRead(g.id)} style={{ ...pillSm, background: done ? T.fill2 : T.green, color: done ? T.dim : "#fff", flex: 1 }}>{done ? "Mark unread" : "Got it ✓"}</button>
                        </div>
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        );
      })}

      <div style={{ ...card, padding: "16px 18px", display: "flex", alignItems: "center", gap: 13, cursor: "pointer" }} className="press" onClick={onCoach}>
        <span style={{ width: 38, height: 38, borderRadius: 19, background: T.orange + "1A", display: "grid", placeItems: "center", flexShrink: 0 }}><ISpark size={19} color={T.orange} /></span>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 15.5, fontWeight: 600 }}>Ask Syrup anything</div>
          <div style={{ fontSize: 13, color: T.dim }}>It tailors any of these to your real numbers</div>
        </div>
        <IChevR size={18} color={T.faint} sw={2} />
      </div>
      <div style={{ fontSize: 11.5, color: T.faint, lineHeight: 1.5, padding: "4px 4px 0", textAlign: "center" }}>
        Educational only — not financial, tax, or legal advice. For decisions specific to you, talk to a qualified professional.
      </div>
    </div>
  );
}

/* ---- atoms ---- */
/* Bottom sheet with swipe-to-dismiss. Drag down past a threshold (or flick) to close;
   otherwise it springs back. Wraps the shared .back/.sheet pattern so every sheet gets
   the same smooth behavior. `scroll` = true makes the body scrollable (most sheets);
   `flush` = true removes default body padding for full-height layouts (e.g. chat). */
function Sheet({ onClose, children, maxHeight = "92vh", scroll = true, padCss = "10px 18px calc(22px + env(safe-area-inset-bottom))", zIndex = 58, title, right }) {
  const sheetRef = useRef(null); const bodyRef = useRef(null);
  const startY = useRef(null); const startT = useRef(0); const dragging = useRef(false);
  const fromBody = useRef(false); const committed = useRef(false); const cur = useRef(0);

  const setTransform = (px) => { // write straight to the DOM — no React render during the gesture
    const el = sheetRef.current; if (!el) return;
    cur.current = px;
    el.style.transition = "none";
    el.style.transform = px ? `translateY(${px}px)` : "";
  };
  const begin = (e, body) => {
    startY.current = e.touches[0].clientY; startT.current = Date.now();
    dragging.current = true; fromBody.current = body; committed.current = !body; cur.current = 0;
  };
  const move = (e) => {
    if (!dragging.current || startY.current == null) return;
    const dy = e.touches[0].clientY - startY.current;
    if (fromBody.current) {
      const atTop = !bodyRef.current || bodyRef.current.scrollTop <= 0;
      if (!committed.current) {
        if (atTop && dy > 24) committed.current = true;
        else { if (cur.current) setTransform(0); return; }
      }
      if (!atTop) { committed.current = false; if (cur.current) setTransform(0); return; }
    }
    if (dy > 8) setTransform(dy - 8); else if (cur.current) setTransform(0);
  };
  const end = () => {
    if (!dragging.current) return;
    const dt = Date.now() - startT.current; const px = cur.current; const velocity = px / Math.max(1, dt);
    dragging.current = false; committed.current = false;
    const el = sheetRef.current;
    if (px > 150 || (velocity > 0.8 && px > 60)) {
      if (el) { el.style.transition = "transform .25s cubic-bezier(.32,.72,0,1)"; el.style.transform = "translateY(100%)"; }
      setTimeout(onClose, 180);
    } else if (el) {
      el.style.transition = "transform .34s cubic-bezier(.32,.72,0,1)"; el.style.transform = "";
      cur.current = 0;
    }
  };

  return (
    <div className="back" onClick={onClose} style={{ position: "fixed", inset: 0, zIndex, background: T.overlay, backdropFilter: "blur(2px)", display: "flex", alignItems: "flex-end", justifyContent: "center" }}>
      <div ref={sheetRef} className="sheet" onClick={(e) => e.stopPropagation()}
        style={{ width: "100%", maxWidth: 480, background: T.bg, borderRadius: "28px 28px 0 0", padding: padCss, maxHeight, display: "flex", flexDirection: "column", willChange: "transform" }}>
        <div onTouchStart={(e) => begin(e, false)} onTouchMove={move} onTouchEnd={end} style={{ flexShrink: 0, paddingBottom: 4, cursor: "grab", touchAction: "none" }}>
          <div style={{ width: 40, height: 5, borderRadius: 3, background: T.grip, margin: "0 auto 8px" }} />
          {(title || right) && (
            <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "2px 4px 8px" }}>
              <span style={{ fontSize: 22, fontWeight: 800, letterSpacing: "-0.02em" }}>{title}</span>
              {right}
            </div>
          )}
        </div>
        <div ref={bodyRef} onTouchStart={(e) => begin(e, true)} onTouchMove={move} onTouchEnd={end} style={{ overflowY: scroll ? "auto" : "visible", flex: scroll ? 1 : "0 1 auto", minHeight: 0 }}>{children}</div>
      </div>
    </div>
  );
}
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 ? T.card : "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: 600, background: active ? T.blue : T.fill2, color: active ? "#fff" : T.text }}>{label}</button>; }
function Tab({ icon: Icon, label, active, onClick, tour }) { return <button data-tour={tour} 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 7px", transition: "color .2s", flex: 1, minWidth: 0 }}><Icon size={23} sw={active ? 2.2 : 1.9} /><span style={{ fontSize: 10, fontWeight: 600 }}>{label}</span></button>; }
function Toggle({ on, onChange }) { return <button onClick={() => onChange(!on)} aria-label="toggle" style={{ width: 50, height: 30, borderRadius: 15, border: "none", cursor: "pointer", background: on ? T.green : T.fill2, position: "relative", transition: "background .2s", flexShrink: 0 }}><span style={{ position: "absolute", top: 3, left: on ? 23 : 3, width: 24, height: 24, borderRadius: 12, background: "#fff", boxShadow: "0 1px 3px rgba(0,0,0,.2)", transition: "left .2s" }} /></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: T.bg, border: "none", borderRadius: 14, padding: "15px", fontSize: 16, fontWeight: 600, cursor: "pointer" };
const pillSm = { background: T.ink, color: T.bg, border: "none", borderRadius: 10, padding: "7px 14px", fontSize: 13, fontWeight: 600, cursor: "pointer" };
const dashedBtn = { display: "flex", alignItems: "center", justifyContent: "center", gap: 7, background: T.fill, border: `1px dashed ${T.faint}`, borderRadius: 14, padding: "13px", color: T.dim, fontSize: 14.5, fontWeight: 600, cursor: "pointer", width: "100%" };
/* Keep the shared style objects in sync when the palette repoints (dark mode). */
function refreshStyleTokens() {
  card.background = T.card;
  grpLbl.color = T.faint; lbl.color = T.dim;
  inp.background = T.fill; inp.color = T.text;
  pill.background = T.ink; pill.color = T.bg;
  pillSm.background = T.ink; pillSm.color = T.bg;
  dashedBtn.background = T.fill; dashedBtn.border = `1px dashed ${T.faint}`; dashedBtn.color = T.dim;
}


/* ================================================================== */
/*  AUTH GATE                                                          */
/* ================================================================== */
function InstallGuide({ onContinue }) {
  const isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent);
  const isAndroid = /Android/.test(navigator.userAgent);
  const CREAM = "#FBF6ED", GOLD = "#F2A93C", COCOA = "#211A12", DIMC = "#8A7E6E";

  return (
    <div style={{ minHeight: "100vh", background: CREAM, color: COCOA, fontFamily: SF, display: "flex", flexDirection: "column", justifyContent: "center", padding: "24px 22px calc(24px + env(safe-area-inset-bottom))", position: "relative", overflow: "hidden" }}>
      <div style={{ position: "absolute", top: -160, left: "50%", transform: "translateX(-50%)", width: 520, height: 360, background: "radial-gradient(ellipse at center, rgba(242,169,60,.30), rgba(242,169,60,0) 70%)", pointerEvents: "none" }} />
      <div className="vin" style={{ width: "100%", maxWidth: 380, margin: "0 auto", position: "relative" }}>
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", marginBottom: 28 }}>
          <div style={{ width: 76, height: 76, borderRadius: 22, background: `linear-gradient(160deg, #FFC15A, ${GOLD})`, position: "relative", overflow: "hidden", boxShadow: "0 12px 30px rgba(242,169,60,.4)", marginBottom: 16 }}>
            <div style={{ position: "absolute", left: 8, right: 8, height: 3, top: "33%", background: "#E0902A", opacity: .5, borderRadius: 2 }} />
            <div style={{ position: "absolute", left: 8, right: 8, height: 3, top: "63%", background: "#E0902A", opacity: .5, borderRadius: 2 }} />
            <div style={{ position: "absolute", top: 8, bottom: 8, width: 3, left: "33%", background: "#E0902A", opacity: .5, borderRadius: 2 }} />
            <div style={{ position: "absolute", top: 8, bottom: 8, width: 3, left: "63%", background: "#E0902A", opacity: .5, borderRadius: 2 }} />
          </div>
          <div style={{ fontSize: 44, fontWeight: 800, letterSpacing: "-0.035em", lineHeight: 1 }}>Waffle</div>
          <div style={{ fontSize: 15, color: DIMC, marginTop: 10, textAlign: "center", fontWeight: 600, maxWidth: 300, lineHeight: 1.5 }}>
            Save Waffle to your home screen for the best experience — it works just like a real app.
          </div>
        </div>

        <div style={{ background: "#fff", borderRadius: 20, padding: "24px 22px", boxShadow: "0 4px 20px rgba(33,26,18,.08)", marginBottom: 20 }}>
          {isIOS ? (<>
            <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 16 }}>How to install on iPhone</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
              <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
                <span style={{ width: 28, height: 28, borderRadius: 14, background: GOLD + "22", color: GOLD, display: "grid", placeItems: "center", fontWeight: 800, fontSize: 13, flexShrink: 0 }}>1</span>
                <div style={{ fontSize: 14.5, lineHeight: 1.5 }}>Tap the <strong>Share</strong> button <span style={{ fontSize: 18, verticalAlign: "middle" }}>⬆</span> at the bottom of Safari</div>
              </div>
              <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
                <span style={{ width: 28, height: 28, borderRadius: 14, background: GOLD + "22", color: GOLD, display: "grid", placeItems: "center", fontWeight: 800, fontSize: 13, flexShrink: 0 }}>2</span>
                <div style={{ fontSize: 14.5, lineHeight: 1.5 }}>Scroll down and tap <strong>"Add to Home Screen"</strong></div>
              </div>
              <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
                <span style={{ width: 28, height: 28, borderRadius: 14, background: GOLD + "22", color: GOLD, display: "grid", placeItems: "center", fontWeight: 800, fontSize: 13, flexShrink: 0 }}>3</span>
                <div style={{ fontSize: 14.5, lineHeight: 1.5 }}>Tap <strong>"Add"</strong> — Waffle will appear on your home screen</div>
              </div>
            </div>
          </>) : isAndroid ? (<>
            <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 16 }}>How to install on Android</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
              <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
                <span style={{ width: 28, height: 28, borderRadius: 14, background: GOLD + "22", color: GOLD, display: "grid", placeItems: "center", fontWeight: 800, fontSize: 13, flexShrink: 0 }}>1</span>
                <div style={{ fontSize: 14.5, lineHeight: 1.5 }}>Tap the <strong>⋮ menu</strong> in the top right of Chrome</div>
              </div>
              <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
                <span style={{ width: 28, height: 28, borderRadius: 14, background: GOLD + "22", color: GOLD, display: "grid", placeItems: "center", fontWeight: 800, fontSize: 13, flexShrink: 0 }}>2</span>
                <div style={{ fontSize: 14.5, lineHeight: 1.5 }}>Tap <strong>"Add to Home screen"</strong> or <strong>"Install app"</strong></div>
              </div>
              <div style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
                <span style={{ width: 28, height: 28, borderRadius: 14, background: GOLD + "22", color: GOLD, display: "grid", placeItems: "center", fontWeight: 800, fontSize: 13, flexShrink: 0 }}>3</span>
                <div style={{ fontSize: 14.5, lineHeight: 1.5 }}>Tap <strong>"Install"</strong> — Waffle will appear on your home screen</div>
              </div>
            </div>
          </>) : (<>
            <div style={{ fontSize: 15, fontWeight: 700, marginBottom: 12 }}>Best on mobile</div>
            <div style={{ fontSize: 14.5, color: DIMC, lineHeight: 1.5 }}>Open this link on your phone and save it to your home screen. Waffle is designed for mobile — it works just like a native app.</div>
          </>)}
        </div>

        <button className="press" onClick={onContinue} style={{ width: "100%", padding: "16px", borderRadius: 16, border: "none", cursor: "pointer", background: COCOA, color: CREAM, fontSize: 16.5, fontWeight: 800, fontFamily: SF, boxShadow: "0 8px 20px rgba(33,26,18,.18)" }}>Continue to Waffle →</button>

        <div style={{ fontSize: 12, color: DIMC, textAlign: "center", marginTop: 16, lineHeight: 1.5 }}>You can always add it later from your browser menu.</div>
      </div>
    </div>
  );
}

function AuthGate({ onAuthed }) {
  const [mode, setMode] = useState("signup");
  const [email, setEmail] = useState(""); const [pw, setPw] = useState("");
  const [busy, setBusy] = useState(false); const [err, setErr] = useState(""); const [notice, setNotice] = useState("");

  const submit = async () => {
    setErr(""); setNotice("");
    if (!email.trim() || !pw) { setErr("Enter your email and password."); return; }
    if (mode === "signup" && pw.length < 6) { setErr("Password needs at least 6 characters."); return; }
    if (!sb) { setErr("Sign-in isn't configured. Check the connection settings."); return; }
    setBusy(true);
    try {
      if (mode === "signup") {
        const { data, error } = await sb.auth.signUp({ email: email.trim(), password: pw });
        if (error) throw error;
        if (data.session) onAuthed(data.session);
        else { setNotice("Check your email to confirm, then sign in."); setMode("signin"); }
      } else {
        const { data, error } = await sb.auth.signInWithPassword({ email: email.trim(), password: pw });
        if (error) throw error;
        onAuthed(data.session);
      }
    } catch (e) { setErr((e && e.message) || "Something went wrong. Try again."); }
    finally { setBusy(false); }
  };

  const signup = mode === "signup";
  const CREAM = "#FBF6ED", GOLD = "#F2A93C", GOLDD = "#E0902A", COCOA = "#211A12", DIMC = "#8A7E6E";
  const brandInp = { width: "100%", boxSizing: "border-box", padding: "15px 16px", borderRadius: 14, border: "1.5px solid #EADFcc", background: "#fff", color: COCOA, fontSize: 16, fontFamily: SF, outline: "none", WebkitAppearance: "none" };
  const brandLbl = { fontSize: 12, fontWeight: 800, letterSpacing: ".1em", textTransform: "uppercase", color: DIMC, marginBottom: 7 };

  return (
    <div style={{ minHeight: "100vh", background: CREAM, color: COCOA, fontFamily: SF, display: "flex", flexDirection: "column", justifyContent: "center", padding: "24px 22px calc(24px + env(safe-area-inset-bottom))", position: "relative", overflow: "hidden" }}>
      <div style={{ position: "absolute", top: -160, left: "50%", transform: "translateX(-50%)", width: 520, height: 360, background: "radial-gradient(ellipse at center, rgba(242,169,60,.30), rgba(242,169,60,0) 70%)", pointerEvents: "none" }} />
      <div style={{ position: "absolute", inset: 0, opacity: 0.025, pointerEvents: "none", backgroundImage: `linear-gradient(${COCOA} 1px, transparent 1px), linear-gradient(90deg, ${COCOA} 1px, transparent 1px)`, backgroundSize: "30px 30px", maskImage: "radial-gradient(ellipse at center, #000 40%, transparent 85%)", WebkitMaskImage: "radial-gradient(ellipse at center, #000 40%, transparent 85%)" }} />

      <div className="vin" style={{ width: "100%", maxWidth: 380, margin: "0 auto", position: "relative" }}>
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", marginBottom: 30 }}>
          <div style={{ width: 76, height: 76, borderRadius: 22, background: `linear-gradient(160deg, #FFC15A, ${GOLD})`, position: "relative", overflow: "hidden", boxShadow: "0 12px 30px rgba(242,169,60,.4)", marginBottom: 16 }}>
            <div style={{ position: "absolute", left: 8, right: 8, height: 3, top: "33%", background: GOLDD, opacity: .5, borderRadius: 2 }} />
            <div style={{ position: "absolute", left: 8, right: 8, height: 3, top: "63%", background: GOLDD, opacity: .5, borderRadius: 2 }} />
            <div style={{ position: "absolute", top: 8, bottom: 8, width: 3, left: "33%", background: GOLDD, opacity: .5, borderRadius: 2 }} />
            <div style={{ position: "absolute", top: 8, bottom: 8, width: 3, left: "63%", background: GOLDD, opacity: .5, borderRadius: 2 }} />
          </div>
          <div style={{ fontSize: 44, fontWeight: 800, letterSpacing: "-0.035em", lineHeight: 1 }}>Waffle</div>
          <div style={{ fontSize: 19, color: COCOA, marginTop: 12, textAlign: "center", fontWeight: 800, letterSpacing: "-0.01em" }}>
            {signup ? "Stack it up." : "Welcome back."}
          </div>
          {signup && <div style={{ fontSize: 14.5, color: DIMC, marginTop: 5, textAlign: "center", fontWeight: 600, maxWidth: 290, lineHeight: 1.4 }}>
            Stack your savings like waffles — built for income that changes month to month.
          </div>}
        </div>

        <div style={{ marginBottom: 13 }}>
          <div style={brandLbl}>Email</div>
          <input value={email} onChange={(e) => setEmail(e.target.value)} type="email" autoComplete="email" inputMode="email" placeholder="you@email.com" style={brandInp} />
        </div>
        <div style={{ marginBottom: 18 }}>
          <div style={brandLbl}>Password</div>
          <input value={pw} onChange={(e) => setPw(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") submit(); }} type="password" autoComplete={signup ? "new-password" : "current-password"} placeholder={signup ? "At least 6 characters" : "••••••••"} style={brandInp} />
        </div>

        {err && <div style={{ fontSize: 13.5, color: "#C0392B", marginBottom: 12, fontWeight: 600 }}>{err}</div>}
        {notice && <div style={{ fontSize: 13.5, color: "#1E8E3E", marginBottom: 12, fontWeight: 600 }}>{notice}</div>}

        <button className="press" onClick={submit} disabled={busy} style={{ width: "100%", padding: "16px", borderRadius: 16, border: "none", cursor: "pointer", background: COCOA, color: CREAM, fontSize: 16.5, fontWeight: 800, fontFamily: SF, opacity: busy ? 0.6 : 1, boxShadow: "0 8px 20px rgba(33,26,18,.18)" }}>{busy ? "…" : signup ? "Create my account" : "Sign in"}</button>

        <button className="press" onClick={() => { setMode(signup ? "signin" : "signup"); setErr(""); setNotice(""); }} style={{ width: "100%", marginTop: 16, background: "none", border: "none", color: GOLDD, fontSize: 14.5, fontWeight: 700, cursor: "pointer", fontFamily: SF }}>
          {signup ? "Already have an account? Sign in" : "New here? Create an account"}
        </button>

        {signup && <div style={{ fontSize: 12, color: DIMC, textAlign: "center", marginTop: 22, lineHeight: 1.5 }}>Free to start · Private · No bank connection needed</div>}
      </div>
    </div>
  );
}

function Root() {
  const [session, setSession] = useState(null);
  const [ready, setReady] = useState(false);
  const isStandalone = window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone;
  const [installSeen, setInstallSeen] = useState(() => isStandalone || localStorage.getItem("waffle_install_seen") === "1");

  useEffect(() => {
    if (!sb) { setReady(true); return; }
    sb.auth.getSession().then(({ data }) => { setSession(data.session || null); setReady(true); });
    const { data: sub } = sb.auth.onAuthStateChange((_e, s) => setSession(s));
    return () => { if (sub && sub.subscription) sub.subscription.unsubscribe(); };
  }, []);

  const signOut = async () => { if (sb) { try { await sb.auth.signOut(); } catch (_) {} } setSession(null); };

  if (!ready) return <div style={{ minHeight: "100vh", background: T.bg, color: T.faint, fontFamily: SF, display: "grid", placeItems: "center" }}>Loading…</div>;
  if (!sb) return (
    <div style={{ minHeight: "100vh", background: T.bg, color: T.text, fontFamily: SF, display: "grid", placeItems: "center", padding: 24, textAlign: "center" }}>
      <div style={{ maxWidth: 340 }}>
        <div style={{ fontSize: 28, fontWeight: 800, marginBottom: 8 }}>Waffle</div>
        <div style={{ fontSize: 14.5, color: T.dim, lineHeight: 1.5, marginBottom: 18 }}>Couldn't reach the sync service — this is usually a network hiccup or an ad/script blocker on the connection script.</div>
        <button className="press" onClick={() => window.location.reload()} style={{ ...pill, padding: "12px 24px" }}>Reload</button>
      </div>
    </div>
  );
  if (!installSeen) return <InstallGuide onContinue={() => { localStorage.setItem("waffle_install_seen", "1"); setInstallSeen(true); }} />;
  if (!session) return <AuthGate onAuthed={setSession} />;
  return <App key={session.user.id} session={session} onSignOut={signOut} />;
}

applyTheme("auto");
ReactDOM.createRoot(document.getElementById("root")).render(<Root />);