// ============================================================
//  IN TIME  v1.4
//  A calm savings goal app for two.
//  See CLAUDE.md for full architecture context before editing.
//  Next major feature: consider splitting into multiple .jsx files.
// ============================================================

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

const VERSION = '1.5';
const CHANGELOG = [
  { ver: '1.5', date: '2026-05-17', notes: 'Critical fix: fbWrite was using set() on the household root, wiping wheel/spins data on every goal sync — spin-once enforcement broke because alreadySpun always reset to false after a write. Changed to update() so wheel path is never touched. Also fixed: initFirebase race condition (shared promise), setSyncStatus before skipNext guard, removed double skipNext increment, React hooks violation in SpinWheelView, nav bar opaque.' },
  { ver: '1.4', date: '2026-05-16', notes: 'Firebase Auth (passwordless email sign-in). Per-user identity locked to device via UID. Daily Spin Wheel with per-user enforcement, Play Again mechanic, streak tracking, partner result visibility, and spin history. Wheel config editable in settings. Bug fixes: Firebase listener race condition, skipNext counter, widget balance, streak year boundary, EditContributionSheet createdAt, sort null-date guards.' },
  { ver: '1.3', date: '2026-05-15', notes: 'Frequency-aware math engine (Monthly/Weekly/Biweekly/Bi-monthly/Daily/None+deadline). Two-date system (projected finish + target). Current balance pre-load on goal creation. Backdated contributions. Adjustment contribution type for interest/market changes. Goals ticker at top. Monthly contribution meter (planned + bonus). Tappable Remaining stat for quick add. Partner activity feed on dashboard. Switch / delete household. PWA splash screen. Mug centering fix. Pause visibility. Drag-to-reorder goals. Streak badge upgrade. What-If math corrected (Option A: extra on top of current).' },
  { ver: '1.2', date: '2026-05-15', notes: 'Delete goals, monthly goal strip, milestone badge clipping fix, PWA safe-area, all-time total, last contribution card, monthly bar chart, on-track indicator, shared goal note, enhanced 100% celebration.' },
  { ver: '1.1', date: '2026-05-15', notes: 'Firebase sync, milestone badges on mug, What If on dashboard, activity filters, live clock, countdown timer, math rewrite.' },
  { ver: '1.0', date: '2026-05-14', notes: 'Initial build.' },
];

const LS_KEY        = 'intime_v1';
const LS_HOUSEHOLD  = 'intime_household';
const LS_AUTH_EMAIL = 'intime_auth_email';

const DEFAULT_SETTINGS = { partner1: 'Ben', partner2: 'Elizabeth', currency: '$', hideBalances: false };

const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 6);

// ── Frequency definitions ────────────────────────────────────
const FREQ = {
  monthly:   { label: 'Monthly',    daysPerPeriod: 30.44, periodsPerMonth: 1 },
  weekly:    { label: 'Weekly',     daysPerPeriod: 7,     periodsPerMonth: 4.345 },
  biweekly:  { label: 'Biweekly',   daysPerPeriod: 14,    periodsPerMonth: 2.173 },
  bimonthly: { label: 'Bi-monthly', daysPerPeriod: 15.22, periodsPerMonth: 2 },
  daily:     { label: 'Daily',      daysPerPeriod: 1,     periodsPerMonth: 30.44 },
  none:      { label: 'Random',     daysPerPeriod: null,  periodsPerMonth: null },
};

const makeGoal = (o = {}) => ({
  id: uid(),
  name: 'Emergency Fund',
  target: 3500,
  currentBalance: 0,
  color: '#C8956D',
  icon: '☕',
  startDate: new Date().toISOString().slice(0, 10),
  targetDate: (() => { const d = new Date(); d.setMonth(d.getMonth() + 7); return d.toISOString().slice(0, 10); })(),
  frequency: 'monthly',
  contributionAmount: 400,
  paused: false,
  note: '',
  order: 0,
  milestones: [
    { id: uid(), pct: 25,  label: 'Foundation set', reward: '' },
    { id: uid(), pct: 50,  label: 'Halfway home',   reward: '' },
    { id: uid(), pct: 75,  label: 'Within reach',   reward: '' },
    { id: uid(), pct: 100, label: 'Full reserve',   reward: '' },
  ],
  contributions: [],
  ...o,
});

const GOAL_COLORS = ['#C8956D','#8B5E3C','#7A8B5E','#5E7A8B','#8B5E7A','#B85850','#5E8B7A','#6B5E8B'];
const GOAL_ICONS  = ['☕','🏔️','⛽','🛡️','✈️','🎓','🏠','🚗','💊','🎯'];

// ── Firebase ────────────────────────────────────────────────
let fbApp = null, fbDb = null, fbCache = {}, fbInitPromise = null;
const getFB = async n => {
  if (fbCache[n]) return fbCache[n];
  fbCache[n] = await import(`https://www.gstatic.com/firebasejs/10.12.0/firebase-${n}.js`);
  return fbCache[n];
};
async function initFirebase() {
  if (fbInitPromise) return fbInitPromise;
  const cfg = window.__FIREBASE_CONFIG__;
  if (!cfg || cfg.apiKey === 'PASTE_YOUR_API_KEY') return false;
  fbInitPromise = (async () => {
    try {
      const { initializeApp } = await getFB('app');
      const { getDatabase }   = await getFB('database');
      fbApp = initializeApp(cfg);
      fbDb  = getDatabase(fbApp);
      return true;
    } catch (e) { console.warn('Firebase init failed', e); fbInitPromise = null; return false; }
  })();
  return fbInitPromise;
}
async function fbWrite(hid, data) {
  if (!fbDb) return;
  try { const { ref, update } = await getFB('database'); await update(ref(fbDb, `households/${hid}`), data); }
  catch (e) { console.warn(e); }
}
async function fbListen(hid, cb) {
  if (!fbDb) return () => {};
  try {
    const { ref, onValue, off } = await getFB('database');
    const r = ref(fbDb, `households/${hid}`);
    onValue(r, s => { const v = s.val(); if (v) cb(v); });
    return () => off(r);
  } catch { return () => {}; }
}
async function fbDelete(hid) {
  if (!fbDb) return;
  try { const { ref, remove } = await getFB('database'); await remove(ref(fbDb, `households/${hid}`)); }
  catch (e) { console.warn(e); }
}

// ── Firebase Auth ────────────────────────────────────────────
let fbAuthInstance = null;
async function initAuth() {
  if (fbAuthInstance) return fbAuthInstance;
  const ok = await initFirebase();
  if (!ok) return null;
  try {
    const { getAuth } = await getFB('auth');
    fbAuthInstance = getAuth(fbApp);
    return fbAuthInstance;
  } catch(e) { console.warn('Auth init failed', e); return null; }
}

async function sendMagicLink(email) {
  const auth = await initAuth();
  if (!auth) throw new Error('Firebase Auth not available. Check your Firebase config.');
  const { sendSignInLinkToEmail } = await getFB('auth');
  await sendSignInLinkToEmail(auth, email, {
    url: window.location.origin + window.location.pathname,
    handleCodeInApp: true,
  });
  try { localStorage.setItem(LS_AUTH_EMAIL, email); } catch {}
}

async function completeMagicLink() {
  const auth = await initAuth();
  if (!auth) return null;
  const { isSignInWithEmailLink, signInWithEmailLink } = await getFB('auth');
  if (!isSignInWithEmailLink(auth, window.location.href)) return null;
  let email;
  try { email = localStorage.getItem(LS_AUTH_EMAIL); } catch {}
  if (!email) email = window.prompt('Please confirm your email to finish signing in:');
  if (!email) return null;
  try {
    const result = await signInWithEmailLink(auth, email, window.location.href);
    try { localStorage.removeItem(LS_AUTH_EMAIL); } catch {}
    window.history.replaceState({}, document.title, window.location.pathname);
    return result.user;
  } catch(e) { console.warn('Magic link completion failed', e); return null; }
}

async function signOutUser() {
  const auth = await initAuth();
  if (!auth) return;
  const { signOut } = await getFB('auth');
  await signOut(auth);
}

async function fbWriteMember(hid, uid, data) {
  if (!fbDb) return;
  try {
    const { ref, set } = await getFB('database');
    await set(ref(fbDb, `households/${hid}/members/${uid}`), data);
  } catch(e) { console.warn(e); }
}

async function fbWriteWheel(hid, data) {
  if (!fbDb) return;
  try {
    const { ref, set } = await getFB('database');
    await set(ref(fbDb, `households/${hid}/wheel`), data);
  } catch(e) { console.warn(e); }
}

async function fbListenWheel(hid, cb) {
  if (!fbDb) return () => {};
  try {
    const { ref, onValue, off } = await getFB('database');
    const r = ref(fbDb, `households/${hid}/wheel`);
    onValue(r, s => cb(s.val()));
    return () => off(r);
  } catch { return () => {}; }
}

// ── Local storage ───────────────────────────────────────────
const loadLocal = () => { try { const r = localStorage.getItem(LS_KEY); return r ? JSON.parse(r) : null; } catch { return null; } };
const saveLocal = s  => { try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch {} };
const getHHId   = () => { try { return localStorage.getItem(LS_HOUSEHOLD) || ''; } catch { return ''; } };
const setHHId   = id => { try { localStorage.setItem(LS_HOUSEHOLD, id); } catch {} };
const clearHHId = () => { try { localStorage.removeItem(LS_HOUSEHOLD); } catch {} };

// ── Spin Wheel ───────────────────────────────────────────────
const DEFAULT_SEGMENTS = [
  { id:'s1',  label:'Starbucks',       sub:'Drink up to $6',   emoji:'☕',  type:'win',     color:'#00704A', weight:1, enabled:true },
  { id:'s2',  label:"McDonald's",      sub:'Item up to $5',    emoji:'🍔',  type:'win',     color:'#DA291C', weight:1, enabled:true },
  { id:'s3',  label:'10-Min Back Rub', sub:'Partner owes you', emoji:'💆',  type:'win',     color:'#7A8B5E', weight:1, enabled:true },
  { id:'s4',  label:'Contribute $5',   sub:'Pick a goal',      emoji:'💸',  type:'bad',     color:'#B85850', weight:1, enabled:true },
  { id:'s5',  label:'Contribute $10',  sub:'Pick a goal',      emoji:'💸',  type:'bad',     color:'#8B5E3C', weight:1, enabled:true },
  { id:'s6',  label:'Contribute $5',   sub:'Pick a goal',      emoji:'💸',  type:'bad',     color:'#B85850', weight:1, enabled:true },
  { id:'s7',  label:'Re-Spin',          sub:'Spin again now!',  emoji:'🔄',  type:'neutral', color:'#8A7E72', weight:1, enabled:true },
  { id:'s8',  label:'5-Min Back Rub',  sub:'A little treat',   emoji:'🤲',  type:'win',     color:'#C8956D', weight:1, enabled:true },
  { id:'s9',  label:'Re-Spin',         sub:'Spin again now!',  emoji:'🔄',  type:'neutral', color:'#8A7E72', weight:1, enabled:true },
  { id:'s10', label:'5-Min Back Rub',  sub:'A little treat',   emoji:'🤲',  type:'win',     color:'#C8956D', weight:1, enabled:true },
  { id:'s11', label:'Re-Spin',         sub:'Spin again now!',  emoji:'🔄',  type:'neutral', color:'#8A7E72', weight:1, enabled:true },
  { id:'s12', label:'5-Min Back Rub',  sub:'A little treat',   emoji:'🤲',  type:'win',     color:'#C8956D', weight:1, enabled:true },
  { id:'s13', label:'Re-Spin',         sub:'Spin again now!',  emoji:'🔄',  type:'neutral', color:'#8A7E72', weight:1, enabled:true },
  { id:'s14', label:'5-Min Back Rub',  sub:'A little treat',   emoji:'🤲',  type:'win',     color:'#C8956D', weight:1, enabled:true },
  { id:'s15', label:'Play Tomorrow',   sub:'Free pass today',  emoji:'📅',  type:'neutral', color:'#5E7A8B', weight:1, enabled:true },
  { id:'s16', label:'5-Min Back Rub',  sub:'A little treat',   emoji:'🤲',  type:'win',     color:'#C8956D', weight:1, enabled:true },
  { id:'s17', label:'Play Tomorrow',   sub:'Free pass today',  emoji:'📅',  type:'neutral', color:'#5E7A8B', weight:1, enabled:true },
  { id:'s18', label:'5-Min Back Rub',  sub:'A little treat',   emoji:'🤲',  type:'win',     color:'#C8956D', weight:1, enabled:true },
  { id:'s19', label:'Play Tomorrow',   sub:'Free pass today',  emoji:'📅',  type:'neutral', color:'#5E7A8B', weight:1, enabled:true },
  { id:'s20', label:'Play Tomorrow',   sub:'Free pass today',  emoji:'📅',  type:'neutral', color:'#5E7A8B', weight:1, enabled:true },
];

const spinTodayKey = () => new Date().toLocaleDateString('en-CA');

function pickWeightedSegment(segments) {
  const pool = segments.filter(s => s.enabled !== false).flatMap(s => Array(s.weight || 1).fill(s));
  if (!pool.length) return segments[0];
  return pool[Math.floor(Math.random() * pool.length)];
}

function calcSpinStreak(spins, name) {
  let streak = 0;
  const today = new Date();
  for (let i = 0; i < 365; i++) {
    const d = new Date(today);
    d.setDate(d.getDate() - i);
    if (spins?.[d.toLocaleDateString('en-CA')]?.[name]) streak++;
    else if (i > 0) break;
  }
  return streak;
}

const firebaseConfigured = () => {
  const c = window.__FIREBASE_CONFIG__;
  return !!(c && c.apiKey && c.apiKey !== 'PASTE_YOUR_API_KEY');
};

// ============================================================
//  MATH ENGINE
//  All dates are JS Date objects. All sums are floats.
//  Frequency-aware. Two-date system. Backdate-safe.
// ============================================================
function getDailyRate(goal) {
  if (goal.paused) return 0;
  const f = FREQ[goal.frequency];
  if (!f || f.daysPerPeriod === null) return 0; // none/random
  return goal.contributionAmount / f.daysPerPeriod;
}

function getMonthlyEquivalent(goal) {
  if (goal.paused) return 0;
  const f = FREQ[goal.frequency];
  if (!f || f.periodsPerMonth === null) return 0;
  return goal.contributionAmount * f.periodsPerMonth;
}

// Sum of all contributions; adjustments may be negative
function sumContributions(contributions) {
  return (contributions || []).reduce((s, c) => s + (c.amount || 0), 0);
}

// Sum of contributions dated within a given YYYY-MM calendar month
function sumMonth(contributions, ymKey) {
  return (contributions || [])
    .filter(c => (c.date || '').slice(0, 7) === ymKey)
    .reduce((s, c) => s + (c.amount || 0), 0);
}

function ymKeyToday() { return new Date().toISOString().slice(0, 7); }

// Core goal calculation — returns everything the UI needs
function calcGoal(goal) {
  const balance      = (goal.currentBalance || 0) + sumContributions(goal.contributions);
  const target       = goal.target || 0;
  const remaining    = Math.max(0, target - balance);
  const pct          = target > 0 ? Math.min(100, (balance / target) * 100) : 0;
  const complete     = balance >= target && target > 0;

  const dailyRate    = getDailyRate(goal);
  const monthlyEq    = getMonthlyEquivalent(goal);

  // Days until target date (can be negative if overdue)
  const today        = new Date();
  today.setHours(0, 0, 0, 0);
  const targetDateD  = goal.targetDate ? new Date(goal.targetDate) : null;
  if (targetDateD) targetDateD.setHours(0, 0, 0, 0);
  const daysToTarget = targetDateD ? Math.ceil((targetDateD - today) / 864e5) : null;

  // Projected finish (based on current rate)
  let daysToFinish, projectedDate;
  if (complete) {
    daysToFinish  = 0;
    projectedDate = today;
  } else if (dailyRate > 0) {
    daysToFinish  = Math.ceil(remaining / dailyRate);
    projectedDate = new Date(today.getTime() + daysToFinish * 864e5);
  } else {
    // None/random or paused — no projected date
    daysToFinish  = null;
    projectedDate = null;
  }

  // Behind / ahead comparison vs target date
  let status = 'unknown';     // 'ahead' | 'ontrack' | 'late' | 'complete' | 'paused' | 'unknown'
  let daysDiff = null;        // negative = late, positive = ahead
  if (complete) {
    status = 'complete';
  } else if (goal.paused) {
    status = 'paused';
  } else if (daysToFinish !== null && daysToTarget !== null) {
    daysDiff = daysToTarget - daysToFinish;
    if (daysDiff > 0)      status = 'ahead';
    else if (daysDiff === 0) status = 'ontrack';
    else                    status = 'late';
  }

  // Suggested monthly to hit target date (for None/random with deadline)
  let suggestedMonthly = null;
  if (daysToTarget !== null && daysToTarget > 0 && remaining > 0) {
    suggestedMonthly = (remaining / daysToTarget) * 30.44;
  }

  // Monthly meter — actual contributions this calendar month
  const thisMonthKey      = ymKeyToday();
  const thisMonthTotal    = sumMonth(goal.contributions, thisMonthKey);
  // The "planned" portion for the meter is the monthly equivalent of the recurring contribution.
  // If frequency is none/random with a deadline, use suggested monthly. Otherwise 0.
  let monthlyPlanned = monthlyEq;
  if (monthlyPlanned === 0 && suggestedMonthly !== null) monthlyPlanned = suggestedMonthly;
  const monthlyFilled  = Math.min(thisMonthTotal, monthlyPlanned);
  const monthlyBonus   = Math.max(0, thisMonthTotal - monthlyPlanned);
  const monthlyPct     = monthlyPlanned > 0 ? Math.min(100, (thisMonthTotal / monthlyPlanned) * 100) : 0;

  return {
    balance, target, remaining, pct, complete,
    dailyRate, monthlyEq,
    daysToTarget, targetDate: targetDateD,
    daysToFinish, projectedDate,
    status, daysDiff,
    suggestedMonthly,
    thisMonthTotal, monthlyPlanned, monthlyFilled, monthlyBonus, monthlyPct,
  };
}

// What If math — Option A: extraMonthly is ADDED on top of current rate
function calcWhatIf(goal, baseCalc, extraMonthly, oneTime) {
  const adjRemaining = Math.max(0, baseCalc.remaining - oneTime);
  const today = new Date(); today.setHours(0, 0, 0, 0);

  // Current daily rate + extra (extra is always converted from monthly)
  const adjDailyRate = baseCalc.dailyRate + (extraMonthly / 30.44);

  let adjDaysToFinish, adjProjectedDate;
  if (adjRemaining === 0) {
    adjDaysToFinish  = 0;
    adjProjectedDate = today;
  } else if (adjDailyRate > 0) {
    adjDaysToFinish  = Math.ceil(adjRemaining / adjDailyRate);
    adjProjectedDate = new Date(today.getTime() + adjDaysToFinish * 864e5);
  } else {
    adjDaysToFinish  = null;
    adjProjectedDate = null;
  }

  const daysSaved = (baseCalc.daysToFinish !== null && adjDaysToFinish !== null)
    ? baseCalc.daysToFinish - adjDaysToFinish
    : null;

  // Status vs target
  let adjStatus = 'unknown', adjDaysDiff = null;
  if (adjRemaining === 0) {
    adjStatus = 'complete';
  } else if (adjDaysToFinish !== null && baseCalc.daysToTarget !== null) {
    adjDaysDiff = baseCalc.daysToTarget - adjDaysToFinish;
    if (adjDaysDiff > 0)      adjStatus = 'ahead';
    else if (adjDaysDiff === 0) adjStatus = 'ontrack';
    else                       adjStatus = 'late';
  }

  return { adjRemaining, adjDailyRate, adjDaysToFinish, adjProjectedDate, daysSaved, adjStatus, adjDaysDiff };
}

function weeksInYear(yr) {
  const dec28 = new Date(yr, 11, 28);
  return Math.floor((dec28 - new Date(yr, 0, 1)) / (7 * 864e5)) + 1;
}

// Streak: count of distinct ISO weeks with a contribution, plus current streak
function calcStreak(contributions) {
  if (!contributions || !contributions.length) return { total: 0, current: 0, longest: 0 };
  const weekKeys = new Set();
  contributions.forEach(c => {
    const d = new Date(c.date);
    const yr = d.getFullYear();
    const wk = Math.floor((d - new Date(yr, 0, 1)) / (7 * 864e5));
    weekKeys.add(`${yr}-${wk}`);
  });
  const sorted = Array.from(weekKeys).sort();
  // Current streak — count consecutive weeks ending at this week (or last week)
  const now = new Date();
  const curYr = now.getFullYear();
  const curWk = Math.floor((now - new Date(curYr, 0, 1)) / (7 * 864e5));
  let current = 0;
  for (let i = 0; i < 200; i++) {
    let yr = curYr, wk = curWk - i;
    while (wk < 0) { yr -= 1; wk += weeksInYear(yr); }
    if (weekKeys.has(`${yr}-${wk}`)) current++;
    else if (i > 1) break; // allow 1 grace week (this week not yet)
  }
  // Longest streak
  let longest = 0, run = 0, prev = null;
  for (const k of sorted) {
    const [y, w] = k.split('-').map(Number);
    const idx = y * 53 + w;
    if (prev !== null && idx === prev + 1) run++;
    else run = 1;
    if (run > longest) longest = run;
    prev = idx;
  }
  return { total: weekKeys.size, current, longest };
}

// Monthly chart — last 6 calendar months from this month
function getMonthlyData(contributions) {
  const map = {};
  (contributions || []).forEach(c => {
    const k = (c.date || '').slice(0, 7);
    if (k) map[k] = (map[k] || 0) + (c.amount || 0);
  });
  return Array.from({ length: 6 }, (_, i) => {
    const d = new Date();
    d.setDate(1);
    d.setMonth(d.getMonth() - (5 - i));
    const k = d.toISOString().slice(0, 7);
    return { key: k, label: d.toLocaleDateString('en-US', { month: 'short' }), amount: map[k] || 0 };
  });
}

// Relative time, e.g. "2h ago", "yesterday", "3 days ago"
function relTime(dateStr) {
  if (!dateStr) return '';
  const then = new Date(dateStr);
  const now  = new Date();
  const diffMs = now - then;
  if (diffMs < 0) return then.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
  const m = Math.floor(diffMs / 60000);
  if (m < 1)  return 'just now';
  if (m < 60) return `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h ago`;
  const d = Math.floor(h / 24);
  if (d === 1) return 'yesterday';
  if (d < 7)   return `${d}d ago`;
  return then.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}

// ── Icons ───────────────────────────────────────────────────
const I = {
  Home:      p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 11.5L12 4l9 7.5V20a1 1 0 01-1 1h-5v-6h-6v6H4a1 1 0 01-1-1z"/></svg>,
  Target:    p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1" fill="currentColor"/></svg>,
  Activity:  p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12h4l3-8 4 16 3-8h4"/></svg>,
  Gear:      p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>,
  Plus:      p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14"/></svg>,
  Moon:      p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M20 14A8 8 0 1110 4a7 7 0 0010 10z"/></svg>,
  Sun:       p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>,
  Check:     p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 13l4 4L19 7"/></svg>,
  Flame:     p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"><path d="M12 3s4 4 4 9a4 4 0 01-8 0c0-2 1-3 1-3s-1 4 2 4-1-6 1-10z"/></svg>,
  X:         p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>,
  Edit:      p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>,
  Trash:     p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg>,
  ChevDown:  p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 9l6 6 6-6"/></svg>,
  ChevUp:    p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 15l-6-6-6 6"/></svg>,
  Link:      p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>,
  Cloud:     p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M18 10h-1.26A8 8 0 109 20h9a5 5 0 000-10z"/></svg>,
  TrendUp:   p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M23 6l-9.5 9.5-5-5L1 18"/><path d="M17 6h6v6"/></svg>,
  TrendDown: p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M23 18l-9.5-9.5-5 5L1 6"/><path d="M17 18h6v-6"/></svg>,
  Pause:     p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>,
  Drag:      p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><circle cx="9" cy="6" r="1.2"/><circle cx="15" cy="6" r="1.2"/><circle cx="9" cy="12" r="1.2"/><circle cx="15" cy="12" r="1.2"/><circle cx="9" cy="18" r="1.2"/><circle cx="15" cy="18" r="1.2"/></svg>,
  Spark:     p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2v4M12 18v4M4.9 4.9l2.8 2.8M16.3 16.3l2.8 2.8M2 12h4M18 12h4M4.9 19.1l2.8-2.8M16.3 7.7l2.8-2.8"/></svg>,
  Spin:      p => <svg {...p} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><line x1="12" y1="2" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="22"/><line x1="2" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="22" y2="12"/></svg>,
};

// ── Hooks ───────────────────────────────────────────────────
function useClock() {
  const [now, setNow] = useState(new Date());
  useEffect(() => { const t = setInterval(() => setNow(new Date()), 30000); return () => clearInterval(t); }, []);
  return now;
}

function useCountdown(targetDate) {
  const [left, setLeft] = useState({ d: 0, h: 0, m: 0, overdue: false });
  useEffect(() => {
    if (!targetDate) return;
    const calc = () => {
      const diffRaw = new Date(targetDate) - new Date();
      const overdue = diffRaw < 0;
      const diff = Math.abs(diffRaw);
      setLeft({
        d: Math.floor(diff / 864e5),
        h: Math.floor((diff % 864e5) / 36e5),
        m: Math.floor((diff % 36e5) / 6e4),
        overdue,
      });
    };
    calc();
    const t = setInterval(calc, 60000);
    return () => clearInterval(t);
  }, [targetDate]);
  return left;
}

// ============================================================
//  Splash Screen — shown on cold launch until app ready
// ============================================================
function Splash({ T }) {
  return (
    <div style={{
      position:'fixed', inset:0, zIndex:9999,
      background:`linear-gradient(160deg, ${T.bg} 0%, ${T.surface} 100%)`,
      display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center',
      animation:'fadeOut 0.4s ease 0.6s forwards',
    }}>
      <div style={{ animation:'splashPop 0.7s cubic-bezier(.22,1.4,.36,1)' }}>
        <MugIcon size={72} color={T.accent}/>
      </div>
      <div style={{
        fontFamily:"'Playfair Display',Georgia,serif", fontSize:24, fontWeight:600,
        letterSpacing:'0.18em', marginTop:18, color:T.text,
        animation:'splashFade 0.6s ease 0.2s both',
      }}>IN TIME</div>
      <div style={{
        fontSize:11, color:T.textMute, marginTop:6, letterSpacing:'0.1em',
        animation:'splashFade 0.6s ease 0.35s both',
      }}>a quiet place to save together</div>
    </div>
  );
}

// ============================================================
//  APP
// ============================================================
function App() {
  const saved = useRef(loadLocal());
  const [dark, setDark]                 = useState(saved.current?.dark ?? false);
  const [settings, setSettings]         = useState(saved.current?.settings ?? { ...DEFAULT_SETTINGS });
  const [goals, setGoals]               = useState(saved.current?.goals ?? []);
  const [tab, setTab]                   = useState('home');
  const [activeGoalId, setActiveGoalId] = useState(saved.current?.goals?.[0]?.id || null);
  const [sheet, setSheet]               = useState(null);
  const [celebrate, setCelebrate]       = useState(null);
  const [touchStart, setTouchStart]     = useState(null);
  const [householdId, setHouseholdIdState] = useState(getHHId());
  const [syncStatus, setSyncStatus]     = useState('offline');
  const [showSplash, setShowSplash]     = useState(true);
  const [authUser, setAuthUser]       = useState(null);
  const [authLoading, setAuthLoading] = useState(firebaseConfigured());
  const [members, setMembers]         = useState({});
  const [wheel, setWheel]             = useState(null);
  const writeTimer    = useRef(null);
  const skipNext      = useRef(0); // counter: skip this many incoming Firebase events
  const now           = useClock();

  // Hide splash after 1s
  useEffect(() => { const t = setTimeout(() => setShowSplash(false), 1000); return () => clearTimeout(t); }, []);

  // Firebase Auth — complete magic link if present, then listen for auth state
  useEffect(() => {
    if (!firebaseConfigured()) return;
    let cancelled = false;
    let unsubAuth = () => {};
    (async () => {
      try { await completeMagicLink(); } catch(e) { console.warn('magic link:', e); }
      const auth = await initAuth();
      if (cancelled || !auth) { setAuthLoading(false); return; }
      const { onAuthStateChanged } = await getFB('auth');
      const fn = onAuthStateChanged(auth, user => {
        if (!cancelled) { setAuthUser(user); setAuthLoading(false); }
      });
      if (cancelled) { fn(); return; }
      unsubAuth = fn;
    })();
    return () => { cancelled = true; unsubAuth(); };
  }, []);

  // Firebase listener
  useEffect(() => {
    if (!householdId) { setSyncStatus('offline'); return; }
    let unsub = () => {};
    let cancelled = false;
    setSyncStatus('connecting');
    (async () => {
      const ok = await initFirebase();
      if (cancelled || !ok) { if (!cancelled) setSyncStatus('offline'); return; }
      const unsubFn = await fbListen(householdId, data => {
        setSyncStatus('live');
        if (skipNext.current > 0) { skipNext.current -= 1; return; }
        if (data.goals) setGoals(data.goals);
        if (data.settings) setSettings(prev => ({ ...prev, ...data.settings }));
        if (data.members) setMembers(data.members);
      });
      if (cancelled) { unsubFn(); return; }
      unsub = unsubFn;
    })();
    return () => { cancelled = true; unsub(); };
  }, [householdId]);

  // Persist (debounced)
  useEffect(() => {
    saveLocal({ dark, settings, goals });
    if (householdId && fbDb) {
      clearTimeout(writeTimer.current);
      writeTimer.current = setTimeout(() => {
        writeTimer.current = null;
        skipNext.current += 1;
        fbWrite(householdId, { goals, settings, updatedAt: new Date().toISOString() });
      }, 800);
    }
  }, [dark, settings, goals, householdId]);

  // Wheel listener — separate Firebase path so it never conflicts with goal sync
  useEffect(() => {
    if (!householdId) { setWheel(null); return; }
    let cancelled = false, unsub = () => {};
    (async () => {
      const ok = await initFirebase();
      if (cancelled || !ok) return;
      const fn = await fbListenWheel(householdId, data => {
        if (cancelled) return;
        const w = data || { segments: DEFAULT_SEGMENTS, spins: {} };
        // Migrate: back rub segments were incorrectly typed as 'neutral'
        if (w.segments) w.segments = w.segments.map(s =>
          s.label === '5-Min Back Rub' && s.type === 'neutral' ? { ...s, type: 'win' } : s
        );
        setWheel(w);
      });
      if (cancelled) { fn(); return; }
      unsub = fn;
    })();
    return () => { cancelled = true; unsub(); };
  }, [householdId]);

  const recordSpin = useCallback((seg, who) => {
    const todayKey = spinTodayKey();
    const spinData = { segId: seg.id, label: seg.label, emoji: seg.emoji, type: seg.type, sub: seg.sub, ts: new Date().toISOString(), who };
    setWheel(prev => {
      const next = {
        segments: prev?.segments || DEFAULT_SEGMENTS,
        spins: { ...(prev?.spins || {}), [todayKey]: { ...((prev?.spins || {})[todayKey] || {}), [who]: spinData } },
      };
      if (householdId) fbWriteWheel(householdId, next);
      return next;
    });
  }, [householdId]);

  const claimMemberName = useCallback(async name => {
    if (!authUser || !householdId) return;
    const data = { name, joinedAt: new Date().toISOString() };
    setMembers(prev => ({ ...prev, [authUser.uid]: data }));
    await fbWriteMember(householdId, authUser.uid, data);
  }, [authUser, householdId]);

  // Sorted goals — by `order`, then created order
  const sortedGoals = useMemo(() => {
    return [...goals].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
  }, [goals]);

  const activeGoal = sortedGoals.find(g => g.id === activeGoalId) || sortedGoals[0] || null;
  const goalIdx    = sortedGoals.findIndex(g => g.id === activeGoal?.id);

  const updateGoal = useCallback((id, upd) => {
    setGoals(prev => prev.map(g => g.id === id ? (typeof upd === 'function' ? upd(g) : { ...g, ...upd }) : g));
  }, []);

  const addContribution = useCallback((goalId, entry) => {
    updateGoal(goalId, g => {
      const oldBal = (g.currentBalance || 0) + sumContributions(g.contributions);
      const newBal = oldBal + entry.amount;
      const oldPct = g.target > 0 ? (oldBal / g.target) * 100 : 0;
      const newPct = g.target > 0 ? (newBal / g.target) * 100 : 0;
      const crossed = (g.milestones || []).filter(m => oldPct < m.pct && newPct >= m.pct);
      const newEntry = {
        id: uid(),
        date: entry.date || new Date().toISOString().slice(0, 10),
        ...entry,
      };
      const contributions = [newEntry, ...(g.contributions || [])]
        .sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0));
      const updated = { ...g, contributions };
      if (crossed.length) setTimeout(() => setCelebrate({
        milestone: crossed[crossed.length - 1], goal: updated, full: newPct >= 100,
      }), 300);
      return updated;
    });
  }, [updateGoal]);

  const updateContribution = useCallback((goalId, contId, patch) => {
    updateGoal(goalId, g => ({
      ...g,
      contributions: (g.contributions || [])
        .map(c => c.id === contId ? { ...c, ...patch } : c)
        .sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0)),
    }));
  }, [updateGoal]);

  const deleteContribution = useCallback((goalId, contId) => {
    updateGoal(goalId, g => ({ ...g, contributions: (g.contributions || []).filter(c => c.id !== contId) }));
  }, [updateGoal]);

  const deleteGoal = useCallback(id => {
    setGoals(prev => {
      const next = prev.filter(g => g.id !== id);
      if (activeGoalId === id) setActiveGoalId(next[0]?.id || null);
      return next;
    });
  }, [activeGoalId]);

  const reorderGoal = useCallback((id, dir) => {
    setGoals(prev => {
      const sorted = [...prev].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
      const idx = sorted.findIndex(g => g.id === id);
      if (idx === -1) return prev;
      const targetIdx = idx + dir;
      if (targetIdx < 0 || targetIdx >= sorted.length) return prev;
      [sorted[idx], sorted[targetIdx]] = [sorted[targetIdx], sorted[idx]];
      // re-assign order
      return sorted.map((g, i) => ({ ...g, order: i }));
    });
  }, []);

  // Swipe between goals on dashboard
  const handleTouchStart = e => { if (tab === 'home' && sortedGoals.length > 1) setTouchStart(e.touches[0].clientX); };
  const handleTouchEnd   = e => {
    if (touchStart === null || tab !== 'home' || sortedGoals.length <= 1) return;
    const diff = touchStart - e.changedTouches[0].clientX;
    if (Math.abs(diff) > 60) {
      const next = Math.max(0, Math.min(sortedGoals.length - 1, goalIdx + (diff > 0 ? 1 : -1)));
      setActiveGoalId(sortedGoals[next].id);
    }
    setTouchStart(null);
  };

  const connectHousehold = id => { setHHId(id); setHouseholdIdState(id); fbInited = false; fbDb = null; };
  const generateHouseholdId = () => {
    const id = `${(settings.partner1||'a').toLowerCase()}-${(settings.partner2||'b').toLowerCase()}-${uid().slice(0,4)}`;
    connectHousehold(id);
    setTimeout(() => { if (fbDb) fbWrite(id, { goals, settings, updatedAt: new Date().toISOString() }); }, 2000);
    return id;
  };
  const leaveHousehold = () => { clearHHId(); setHouseholdIdState(''); setSyncStatus('offline'); };
  const deleteHousehold = async () => {
    if (householdId) await fbDelete(householdId);
    leaveHousehold();
  };

  const T   = dark ? darkT : lightT;
  const C   = settings.currency;
  const fmt = n => `${C}${Math.abs(n).toLocaleString('en-US', { maximumFractionDigits: 0 })}`;
  const hide = v => settings.hideBalances ? '•••' : v;

  // Auth gates — only when Firebase is configured
  const myName = authUser ? (members[authUser.uid]?.name || null) : null;
  if (authLoading) return <LoadingScreen T={T}/>;
  if (firebaseConfigured() && !authUser) return <LoginScreen T={T}/>;
  if (authUser && householdId && !myName) {
    return <NamePickScreen T={T} settings={settings} onPick={claimMemberName}/>;
  }

  return (
    <div style={{ background:T.bg, color:T.text, minHeight:'100dvh', fontFamily:"'DM Sans','Nunito Sans',-apple-system,sans-serif" }}
      onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>
      <style>{CSS(T)}</style>
      <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=Playfair+Display:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet"/>

      {showSplash && <Splash T={T}/>}

      <div className="shell">
        <header className="topbar">
          <div className="brand">
            <MugIcon size={26} color={T.accent}/>
            <div>
              <div className="brand-name">IN TIME</div>
              <div className="brand-sub">{settings.partner1} & {settings.partner2}</div>
            </div>
          </div>
          <div style={{ display:'flex', alignItems:'center', gap:6 }}>
            <div className="topbar-clock">{now.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit'})}</div>
            <div className={`sync-dot ${syncStatus}`}/>
            <button className="icon-btn" onClick={() => setDark(!dark)}>{dark ? <I.Sun width={16} height={16}/> : <I.Moon width={16} height={16}/>}</button>
            <button className="icon-btn" onClick={() => setSheet({ type:'settings' })}><I.Gear width={16} height={16}/></button>
          </div>
        </header>

        {/* Goals ticker — only on dashboard, when 2+ goals */}
        {tab === 'home' && sortedGoals.length >= 2 && (
          <GoalsTicker goals={sortedGoals} activeId={activeGoal?.id} setActive={id => setActiveGoalId(id)} fmt={fmt} T={T}/>
        )}

        <main className="page">
          {tab === 'home' && activeGoal && (
            <Dashboard goal={activeGoal} goals={sortedGoals} goalIdx={goalIdx} fmt={fmt} hide={hide} T={T} settings={settings} setSheet={setSheet} updateGoal={updateGoal} addContribution={addContribution} C={C}/>
          )}
          {tab === 'home' && !sortedGoals.length && (
            <EmptyState msg="No goals yet." sub="Create your first savings goal to get started." T={T} action={() => setSheet({ type:'newGoal' })}/>
          )}
          {tab === 'goals' && (
            <GoalsList goals={sortedGoals} fmt={fmt} hide={hide} T={T} settings={settings}
              setActiveGoal={id => { setActiveGoalId(id); setTab('home'); }}
              setSheet={setSheet} onDelete={deleteGoal} onReorder={reorderGoal}/>
          )}
          {tab === 'activity' && (
            <ActivityView goals={sortedGoals} fmt={fmt} T={T} settings={settings}
              onEdit={(gid, c) => setSheet({ type:'editContribution', goalId:gid, cont:c })}/>
          )}
          {tab === 'spin' && (
            <SpinWheelView
              wheel={wheel}
              myName={myName || settings.partner1}
              settings={settings}
              T={T}
              C={C}
              householdId={householdId}
              onSpinComplete={recordSpin}
              onLogPenalty={() => { setTab('home'); setSheet({ type:'addContribution', goalId:activeGoal?.id }); }}
            />
          )}
        </main>

        {tab === 'home' && activeGoal && (
          <button className="fab" style={{ background:`linear-gradient(135deg, ${activeGoal.color}, ${activeGoal.color}CC)` }}
            onClick={() => setSheet({ type:'addContribution', goalId:activeGoal.id })}>
            <I.Plus width={22} height={22}/>
          </button>
        )}

        <nav className="bottom-nav">
          {[{id:'home',icon:I.Home,label:'Home'},{id:'goals',icon:I.Target,label:'Goals'},{id:'activity',icon:I.Activity,label:'Activity'},{id:'spin',icon:I.Spin,label:'Spin'}].map(n => (
            <button key={n.id} className={`nav-btn ${tab===n.id?'active':''}`}
              onClick={() => setTab(n.id)} style={tab===n.id?{color:activeGoal?.color||T.accent}:{}}>
              <n.icon width={20} height={20}/><span>{n.label}</span>
            </button>
          ))}
        </nav>
      </div>

      {sheet?.type === 'addContribution' && (
        <AddContributionSheet goalId={sheet.goalId} prefillAmount={sheet.prefillAmount}
          settings={settings} T={T} C={C} onClose={() => setSheet(null)}
          onAdd={e => { addContribution(sheet.goalId, e); setSheet(null); }}/>
      )}
      {sheet?.type === 'editContribution' && (
        <EditContributionSheet cont={sheet.cont} settings={settings} T={T} C={C}
          onClose={() => setSheet(null)}
          onSave={u => { updateContribution(sheet.goalId, sheet.cont.id, u); setSheet(null); }}
          onDelete={() => { deleteContribution(sheet.goalId, sheet.cont.id); setSheet(null); }}/>
      )}
      {sheet?.type === 'newGoal' && (
        <GoalSheet T={T} C={C} settings={settings} onClose={() => setSheet(null)}
          onSave={g => {
            const ng = makeGoal({ ...g, order: sortedGoals.length });
            setGoals(prev => [...prev, ng]); setActiveGoalId(ng.id); setTab('home'); setSheet(null);
          }}/>
      )}
      {sheet?.type === 'editGoal' && (
        <GoalSheet T={T} C={C} settings={settings} goal={goals.find(g => g.id === sheet.goalId)}
          onClose={() => setSheet(null)}
          onSave={u => { updateGoal(sheet.goalId, g => ({ ...g, ...u })); setSheet(null); }}/>
      )}
      {sheet?.type === 'settings' && (
        <SettingsSheet T={T} settings={settings} dark={dark} setDark={setDark}
          householdId={householdId} connectHousehold={connectHousehold}
          generateHouseholdId={generateHouseholdId} leaveHousehold={leaveHousehold} deleteHousehold={deleteHousehold}
          syncStatus={syncStatus} onClose={() => setSheet(null)}
          onSave={s => { setSettings(s); setSheet(null); }}
          wheel={wheel} onSaveWheel={nw => { setWheel(nw); if (householdId) fbWriteWheel(householdId, nw); }}
          authUser={authUser} myName={myName}
          onSignOut={async () => { await signOutUser(); setSheet(null); }}/>
      )}

      {celebrate && <Celebration milestone={celebrate.milestone} goal={celebrate.goal} full={celebrate.full} T={T} onClose={() => setCelebrate(null)}/>}
    </div>
  );
}
window.__InTimeApp = App;

// ============================================================
//  COFFEE MUG — centered properly. ViewBox is 310×310, ring at (155,155).
//  The mug glyph is centered at (105, 90) of the viewBox.
//  We translate the mug group so the mug center sits on the ring center.
// ============================================================
function CoffeeMug({ pct, color, size = 220, T, milestones = [] }) {
  const fillH      = (Math.min(100, pct) / 100) * 120;
  const steamOp    = pct > 5 ? Math.min(0.6, pct / 100) : 0;
  const CX = 155, CY = 155, R = 108, circ = 2 * Math.PI * R;

  // Mug local coords: body from (21,33) to (121, 145), handle out to ~150.
  // Mug glyph bounding box: x [21..150], y [33..160]. Center = ~(85, 96).
  // We translate the mug so its center sits on (CX, CY).
  const MUG_CENTER_X = 85;
  const MUG_CENTER_Y = 96;
  const tx = CX - MUG_CENTER_X;
  const ty = CY - MUG_CENTER_Y;

  const badges = milestones.slice().sort((a, b) => a.pct - b.pct).map(m => {
    const angle = ((m.pct / 100) * 360 - 90) * (Math.PI / 180);
    return { ...m, x: CX + Math.cos(angle) * R, y: CY + Math.sin(angle) * R, reached: pct >= m.pct };
  });

  return (
    <div style={{ width: size + 40, margin: '0 auto' }}>
      <svg viewBox="0 0 310 310" style={{ width: size + 40, height: size + 40, display: 'block' }}>
        <defs>
          <linearGradient id="mugBody" x1="0" y1="0" x2="1" y2="1">
            <stop offset="0%" stopColor={T.mugLight}/>
            <stop offset="100%" stopColor={T.mugDark}/>
          </linearGradient>
          <linearGradient id="liquidFill" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor={color} stopOpacity="0.75"/>
            <stop offset="100%" stopColor={color}/>
          </linearGradient>
          <clipPath id="mugClip">
            <rect x={tx + 21} y={ty + 33} width="100" height="112" rx="12"/>
          </clipPath>
        </defs>

        {/* Progress ring */}
        <circle cx={CX} cy={CY} r={R} fill="none" stroke={T.border} strokeWidth="2.5" opacity="0.3"/>
        <circle cx={CX} cy={CY} r={R} fill="none" stroke={color} strokeWidth="3"
          strokeDasharray={`${(pct/100)*circ} ${circ}`} strokeLinecap="round"
          transform={`rotate(-90 ${CX} ${CY})`}
          style={{ transition: 'stroke-dasharray 1.2s cubic-bezier(.22,1,.36,1)' }}/>

        {/* Steam — above the mug */}
        <g opacity={steamOp} transform={`translate(${tx}, ${ty})`}>
          {[55, 78, 101].map((x, i) => (
            <path key={i} d={`M${x} 25 Q${x-6} 10 ${x+2} -2 Q${x+8} -14 ${x} -25`}
              fill="none" stroke={T.textMute} strokeWidth="2" strokeLinecap="round" opacity={0.25 + i*0.08}
              style={{ animation: `steamRise ${2.5 + i*0.4}s ease-in-out infinite`, animationDelay: `${i*0.3}s` }}/>
          ))}
        </g>

        {/* Mug body */}
        <g transform={`translate(${tx}, ${ty})`}>
          <rect x="21" y="33" width="100" height="112" rx="13" fill="url(#mugBody)" stroke={T.border} strokeWidth="1.5"/>
          <path d="M121 53 Q150 53 150 87 Q150 121 121 121" fill="none" stroke={T.border} strokeWidth="5.5" strokeLinecap="round"/>
          <path d="M121 53 Q147 53 147 87 Q147 121 121 121" fill="none" stroke="url(#mugBody)" strokeWidth="3.5" strokeLinecap="round"/>
        </g>

        {/* Liquid (clipped to mug body) */}
        <g clipPath="url(#mugClip)">
          <rect x={tx+21} y={ty+145-fillH} width="100" height={fillH+10} fill="url(#liquidFill)"
            style={{ transition: 'y 1.2s cubic-bezier(.22,1,.36,1), height 1.2s cubic-bezier(.22,1,.36,1)' }}/>
          <ellipse cx={tx+71} cy={ty+145-fillH} rx="47" ry="3" fill={color} opacity="0.3"
            style={{ transition: 'cy 1.2s cubic-bezier(.22,1,.36,1)' }}/>
          {fillH > 20 && <>
            <circle cx={tx+43} cy={ty+145-fillH+6} r="4" fill={color} opacity="0.18"/>
            <circle cx={tx+61} cy={ty+145-fillH+8} r="3" fill={color} opacity="0.13"/>
            <circle cx={tx+83} cy={ty+145-fillH+5} r="5" fill={color} opacity="0.16"/>
            <circle cx={tx+98} cy={ty+145-fillH+9} r="3" fill={color} opacity="0.10"/>
          </>}
        </g>

        {/* Mug rim highlight */}
        <rect x={tx+21} y={ty+33} width="100" height="5" rx="3" fill={T.text} opacity="0.05"/>

        {/* Saucer */}
        <ellipse cx={tx+71} cy={ty+153} rx="66" ry="7"   fill={T.mugDark}  opacity="0.4"/>
        <ellipse cx={tx+71} cy={ty+151} rx="62" ry="5.5" fill={T.mugLight} opacity="0.7"/>

        {/* Milestone badges */}
        {badges.map((m, i) => (
          <g key={m.id || i}>
            <circle cx={m.x} cy={m.y} r="13"
              fill={m.reached ? color : T.bg}
              stroke={m.reached ? color : T.border}
              strokeWidth={m.reached ? 0 : 1.5}
              strokeDasharray={m.reached ? 'none' : '3 2'}/>
            {m.reached
              ? <path d={`M${m.x-5} ${m.y}l3.5 3.5 6-7`} fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
              : <text x={m.x} y={m.y+4} textAnchor="middle" fill={T.textMute} fontSize="7.5" fontWeight="600" fontFamily="DM Sans,sans-serif">{m.pct}%</text>
            }
          </g>
        ))}
      </svg>
    </div>
  );
}

function MugIcon({ size = 24, color }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.5">
      <rect x="4" y="6" width="13" height="13" rx="3"/>
      <path d="M17 9Q21 9 21 13Q21 17 17 17" strokeLinecap="round"/>
      <path d="M7 3v2M10.5 3v2M14 3v2" strokeLinecap="round" opacity="0.5"/>
    </svg>
  );
}

// ============================================================
//  GOALS TICKER — stock-style horizontal scroll
// ============================================================
function GoalsTicker({ goals, activeId, setActive, fmt, T }) {
  return (
    <div className="ticker-wrap">
      <div className="ticker-scroll">
        {goals.map(g => {
          const c = calcGoal(g);
          const isActive = g.id === activeId;
          let statusLabel, statusClass;
          if (c.status === 'complete')      { statusLabel = '✓ Done';                    statusClass = 'good'; }
          else if (c.status === 'paused')   { statusLabel = 'Paused';                    statusClass = 'muted'; }
          else if (c.status === 'ahead')    { statusLabel = `${c.daysDiff}d ahead`;       statusClass = 'good'; }
          else if (c.status === 'ontrack')  { statusLabel = 'On track';                  statusClass = 'good'; }
          else if (c.status === 'late')     { statusLabel = `${Math.abs(c.daysDiff)}d late`; statusClass = 'late'; }
          else                              { statusLabel = '—';                          statusClass = 'muted'; }
          return (
            <button key={g.id} className={`ticker-item ${isActive ? 'active' : ''}`}
              style={isActive ? { borderColor: g.color, background: g.color + '12' } : {}}
              onClick={() => setActive(g.id)}>
              <div className="ticker-icon" style={{ background: g.color + '20', color: g.color }}>{g.icon}</div>
              <div className="ticker-mid">
                <div className="ticker-name">{g.name}</div>
                <div className={`ticker-status ${statusClass}`}>{statusLabel}</div>
              </div>
              <div className="ticker-pct" style={{ color: g.color }}>{c.pct.toFixed(0)}%</div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ============================================================
//  DASHBOARD
// ============================================================
function Dashboard({ goal, goals, goalIdx, fmt, hide, T, settings, setSheet, updateGoal, addContribution, C }) {
  const calc = useMemo(() => calcGoal(goal), [goal]);
  const countdown = useCountdown(goal.targetDate);
  const [showWhatIf, setShowWhatIf] = useState(false);
  const [showChart,  setShowChart]  = useState(false);
  const [editingNote, setEditingNote] = useState(false);
  const [noteVal, setNoteVal] = useState(goal.note || '');
  useEffect(() => { setNoteVal(goal.note || ''); }, [goal.id]);

  const streak = useMemo(() => calcStreak(goal.contributions), [goal.contributions]);
  const monthlyData = useMemo(() => getMonthlyData(goal.contributions), [goal.contributions]);
  const maxMonth = Math.max(...monthlyData.map(m => m.amount), 1);

  // Last 3 contributions for partner activity feed (with createdAt fallback)
  const recentActivity = useMemo(() => {
    return (goal.contributions || []).slice(0, 3);
  }, [goal.contributions]);

  const lastContrib = recentActivity[0];

  const saveNote = () => { updateGoal(goal.id, g => ({ ...g, note: noteVal })); setEditingNote(false); };

  // Status display
  let statusBadge, statusClass;
  if (calc.status === 'complete')        { statusBadge = '🎉  Goal reached!';        statusClass = 'good'; }
  else if (calc.status === 'paused')     { statusBadge = 'Paused';                    statusClass = 'muted'; }
  else if (calc.status === 'ahead')      { statusBadge = `${calc.daysDiff}d ahead`;   statusClass = 'good'; }
  else if (calc.status === 'ontrack')    { statusBadge = 'On track';                  statusClass = 'good'; }
  else if (calc.status === 'late')       { statusBadge = `${Math.abs(calc.daysDiff)}d late`; statusClass = 'late'; }
  else if (calc.dailyRate === 0 && goal.frequency === 'none' && calc.suggestedMonthly === null) {
    statusBadge = 'Set a deadline to plan';
    statusClass = 'muted';
  } else { statusBadge = 'Add a contribution to track'; statusClass = 'muted'; }

  const projectedDateStr = calc.projectedDate
    ? calc.projectedDate.toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' })
    : '—';
  const targetDateStr = calc.targetDate
    ? calc.targetDate.toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' })
    : '—';
  const showTargetDate = calc.status === 'late'; // only show og target when late

  return (
    <div className="stack fade-in">
      {/* Hero */}
      <section className="hero" style={{ borderColor: goal.color + '30' }}>
        {goal.paused && <div className="paused-badge"><I.Pause width={10} height={10}/> Paused</div>}
        <div className="hero-label"><span className="dot" style={{ background: goal.color }}/>{goal.name}</div>
        <div className="hero-balance">
          <span className="hero-amt">{hide(fmt(calc.balance))}</span>
          <span className="hero-of">of {fmt(goal.target)}</span>
        </div>

        <CoffeeMug pct={calc.pct} color={goal.color} T={T} milestones={goal.milestones || []}/>

        <div className="on-track-row">
          <div className={`on-track-badge ${statusClass}`}>
            {calc.status === 'ahead'     && <I.TrendUp   width={12} height={12}/>}
            {calc.status === 'late'      && <I.TrendDown width={12} height={12}/>}
            {calc.status === 'ontrack'   && <I.Check     width={12} height={12}/>}
            {calc.status === 'complete'  && <I.Check     width={12} height={12}/>}
            {calc.status === 'paused'    && <I.Pause     width={12} height={12}/>}
            {statusBadge}
          </div>
        </div>
      </section>

      {/* Two-date strip — projected always, target only when late */}
      {!calc.complete && (
        <section className="dates-strip">
          <div className="dates-col">
            <div className="dates-label">Done by</div>
            <div className="dates-val" style={{ color: goal.color }}>{projectedDateStr}</div>
          </div>
          {showTargetDate && (
            <>
              <div className="dates-sep">·</div>
              <div className="dates-col">
                <div className="dates-label">Wanted by</div>
                <div className="dates-val muted">{targetDateStr}</div>
              </div>
            </>
          )}
          {calc.dailyRate === 0 && goal.frequency === 'none' && calc.suggestedMonthly !== null && (
            <>
              <div className="dates-sep">·</div>
              <div className="dates-col">
                <div className="dates-label">Save</div>
                <div className="dates-val" style={{ color: goal.color }}>{fmt(calc.suggestedMonthly)}/mo</div>
              </div>
            </>
          )}
        </section>
      )}

      {/* Monthly contribution meter */}
      {calc.monthlyPlanned > 0 && !goal.paused && (
        <MonthlyMeter calc={calc} goal={goal} fmt={fmt} T={T} onEdit={() => setSheet({ type:'editGoal', goalId:goal.id })}/>
      )}

      {/* Countdown */}
      {goal.targetDate && (
        <section className="countdown-bar" style={{ borderColor: goal.color + '20' }}>
          <div className="cd-item"><span className="cd-num">{countdown.d}</span><span className="cd-label">days</span></div>
          <div className="cd-sep">:</div>
          <div className="cd-item"><span className="cd-num">{String(countdown.h).padStart(2,'0')}</span><span className="cd-label">hrs</span></div>
          <div className="cd-sep">:</div>
          <div className="cd-item"><span className="cd-num">{String(countdown.m).padStart(2,'0')}</span><span className="cd-label">min</span></div>
          <div className="cd-to">{countdown.overdue ? 'past ' : 'to '}{goal.targetDate && !isNaN(new Date(goal.targetDate)) ? new Date(goal.targetDate).toLocaleDateString('en-US',{month:'short',day:'numeric'}) : ''}</div>
        </section>
      )}

      {/* Stats */}
      <section className="stat-grid">
        <StatCard label="Complete" val={hide(`${calc.pct.toFixed(0)}%`)} sub="of goal"/>
        <StatCard label="Streak" val={`${streak.current}`} sub={`${streak.longest} longest`} icon={<I.Flame width={13} height={13}/>}/>
        <StatCard label="Remaining" val={hide(fmt(calc.remaining))} sub="tap to add" tappable onClick={() => setSheet({ type:'addContribution', goalId:goal.id })}/>
        <StatCard label="Saving" val={calc.dailyRate > 0 ? fmt(calc.monthlyEq) : '—'} sub={calc.dailyRate > 0 ? FREQ[goal.frequency].label.toLowerCase() : goal.frequency === 'none' ? 'random' : 'paused'}/>
      </section>

      {/* Partner activity */}
      {recentActivity.length > 0 && (
        <section className="card">
          <div className="card-head"><h3>Recent activity</h3>{lastContrib && <span className="card-sub">{relTime(lastContrib.createdAt || lastContrib.date)}</span>}</div>
          <div className="activity-feed">
            {recentActivity.map(c => (
              <div key={c.id} className="feed-row" onClick={() => setSheet({ type:'editContribution', goalId:goal.id, cont:c })}>
                <div className={`avatar ${c.who===settings.partner1?'p1':c.who===settings.partner2?'p2':'adj'}`}
                  style={c.who===settings.partner1?{background:goal.color}:c.type==='adjustment'?{}:{}}>
                  {c.type === 'adjustment' ? <I.Spark width={14} height={14}/> : (c.who || '?')[0]}
                </div>
                <div className="feed-mid">
                  <div className="feed-line">
                    <span className="feed-who">{c.type === 'adjustment' ? 'Adjustment' : c.who}</span>
                    <span className="feed-action"> added </span>
                    <span className="feed-amt" style={{ color: goal.color }}>{fmt(c.amount)}</span>
                    {c.type === 'unexpected' && <span className="feed-tag"> · bonus</span>}
                  </div>
                  {c.note && <div className="feed-note">"{c.note}"</div>}
                  <div className="feed-date">{relTime(c.createdAt || c.date)} · {new Date(c.date).toLocaleDateString('en-US',{month:'short',day:'numeric'})}</div>
                </div>
              </div>
            ))}
          </div>
        </section>
      )}

      {/* Milestones */}
      <section className="card">
        <div className="card-head"><h3>Milestones</h3></div>
        <div className="ms-badges">
          {(goal.milestones || []).slice().sort((a, b) => a.pct - b.pct).map(m => (
            <div key={m.id} className={`ms-badge ${calc.pct >= m.pct ? 'reached' : ''}`}
              style={calc.pct >= m.pct ? { background: goal.color + '18', borderColor: goal.color + '50' } : {}}>
              <div className="ms-badge-circle" style={calc.pct >= m.pct ? { background: goal.color, borderColor: goal.color } : {}}>
                {calc.pct >= m.pct ? <I.Check width={11} height={11}/> : <span>{m.pct}%</span>}
              </div>
              <div>
                <div className="ms-badge-label">{m.label || `${m.pct}%`}</div>
                {m.reward && <div className="ms-badge-reward">{m.reward}</div>}
              </div>
            </div>
          ))}
        </div>
      </section>

      {/* Shared note */}
      <section className="card">
        <div className="card-head">
          <h3>Note</h3>
          {!editingNote && <button className="icon-btn" style={{width:28,height:28}} onClick={() => setEditingNote(true)}><I.Edit width={13} height={13}/></button>}
        </div>
        {editingNote ? (
          <div style={{display:'flex',flexDirection:'column',gap:8}}>
            <textarea className="note-input" rows={3} value={noteVal} onChange={e => setNoteVal(e.target.value)} placeholder="A shared note for this goal…" style={{resize:'none',lineHeight:1.5}}/>
            <div style={{display:'flex',gap:8}}>
              <button className="primary-btn" style={{flex:1,padding:10}} onClick={saveNote}>Save</button>
              <button className="secondary-btn" style={{flex:1}} onClick={() => { setNoteVal(goal.note || ''); setEditingNote(false); }}>Cancel</button>
            </div>
          </div>
        ) : (
          <div className="note-display" onClick={() => setEditingNote(true)}>
            {goal.note ? <span style={{fontStyle:'italic',color:T.textMute}}>"{goal.note}"</span> : <span style={{color:T.border}}>Tap to add a shared note…</span>}
          </div>
        )}
      </section>

      {/* Monthly chart */}
      <section className="card">
        <button className="card-head wi-toggle" onClick={() => setShowChart(!showChart)}>
          <h3>Last 6 months</h3>{showChart ? <I.ChevUp width={16} height={16}/> : <I.ChevDown width={16} height={16}/>}
        </button>
        {showChart && (
          <div className="bar-chart">
            {monthlyData.map(m => (
              <div key={m.key} className="bar-col">
                <div className="bar-val">{m.amount > 0 ? fmt(m.amount) : ''}</div>
                <div className="bar-track"><div className="bar-fill" style={{ height: `${(m.amount/maxMonth)*100}%`, background: goal.color }}/></div>
                <div className="bar-label">{m.label}</div>
              </div>
            ))}
          </div>
        )}
      </section>

      {/* What If */}
      <section className="card">
        <button className="card-head wi-toggle" onClick={() => setShowWhatIf(!showWhatIf)}>
          <h3>What if</h3>{showWhatIf ? <I.ChevUp width={16} height={16}/> : <I.ChevDown width={16} height={16}/>}
        </button>
        {showWhatIf && <WhatIfInline goal={goal} calc={calc} fmt={fmt} T={T} C={C} updateGoal={updateGoal}/>}
      </section>

      <div className="spacer"/>
    </div>
  );
}

// Monthly contribution meter
function MonthlyMeter({ calc, goal, fmt, T, onEdit }) {
  const plannedPct = calc.monthlyPlanned > 0 ? Math.min(100, (calc.monthlyFilled / calc.monthlyPlanned) * 100) : 0;
  const bonusPct = calc.monthlyPlanned > 0 ? Math.min(50, (calc.monthlyBonus / calc.monthlyPlanned) * 100) : 0; // bonus shown beyond 100% capped at +50% visual
  const showBonus = calc.monthlyBonus > 0;
  return (
    <section className="monthly-meter" style={{ borderColor: goal.color + '25' }}>
      <div className="mm-top">
        <div className="mm-label">This month</div>
        <button className="mm-edit-btn" onClick={onEdit} aria-label="Edit goal"><I.Edit width={13} height={13}/></button>
      </div>
      <div className="mm-amounts">
        <span className="mm-current" style={{ color: goal.color }}>{fmt(calc.thisMonthTotal)}</span>
        <span className="mm-of">of {fmt(calc.monthlyPlanned)}</span>
        {showBonus && <span className="mm-bonus">+{fmt(calc.monthlyBonus)} bonus</span>}
      </div>
      <div className="mm-track">
        <div className="mm-fill-planned" style={{ width: `${plannedPct}%`, background: goal.color }}/>
        {showBonus && <div className="mm-fill-bonus" style={{ width: `${bonusPct}%`, background: goal.color, left: `${plannedPct}%` }}/>}
      </div>
      <div className="mm-sub">{plannedPct >= 100 ? '✓ Monthly goal met' : `${(100-plannedPct).toFixed(0)}% to monthly goal`}</div>
    </section>
  );
}

function StatCard({ label, val, sub, icon, tappable, onClick }) {
  return (
    <div className={`stat ${tappable ? 'tappable' : ''}`} onClick={tappable ? onClick : undefined}>
      <div className="stat-label">{icon}{label}</div>
      <div className="stat-val">{val}</div>
      <div className="stat-sub">{sub}</div>
    </div>
  );
}

// What If — math via calcWhatIf
function WhatIfInline({ goal, calc, fmt, T, C, updateGoal }) {
  const [extra, setExtra]     = useState(0);
  const [oneTime, setOneTime] = useState(0);
  const wi = useMemo(() => calcWhatIf(goal, calc, extra, oneTime), [goal, calc, extra, oneTime]);

  const currentDateStr = calc.projectedDate
    ? calc.projectedDate.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})
    : '—';
  const adjDateStr = wi.adjProjectedDate
    ? wi.adjProjectedDate.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})
    : '—';
  const changed = extra > 0 || oneTime > 0;

  return (
    <div className="wi-body">
      <div className="wi-comparison">
        <div className="wi-col">
          <div className="wi-col-label">Current</div>
          <div className="wi-col-date">{currentDateStr}</div>
          <div className="wi-col-sub">{calc.dailyRate > 0 ? `${fmt(calc.monthlyEq)}/mo` : 'no recurring'}</div>
        </div>
        <div className="wi-arrow">→</div>
        <div className="wi-col">
          <div className="wi-col-label" style={{color:goal.color}}>Adjusted</div>
          <div className="wi-col-date" style={{color:goal.color}}>{adjDateStr}</div>
          {wi.daysSaved !== null && wi.daysSaved > 0 && (
            <div className="wi-col-saved" style={{color:goal.color}}>{wi.daysSaved}d sooner</div>
          )}
          {wi.adjStatus === 'complete' && wi.adjRemaining === 0 && (
            <div className="wi-col-saved" style={{color:goal.color}}>Done today!</div>
          )}
        </div>
      </div>
      <div className="wi-slider-section">
        <label className="field-label">Extra on top per month</label>
        <div className="slider-row">
          <span className="slider-val" style={{color:goal.color}}>+{fmt(extra)}</span>
          <input type="range" className="slider" min="0" max="500" step="25" value={extra} onChange={e => setExtra(+e.target.value)} style={{accentColor:goal.color}}/>
        </div>
        <div className="slider-marks"><span>{C}0</span><span>{C}250</span><span>{C}500</span></div>
        {extra > 0 && goal.frequency !== 'none' && (
          <button className="apply-btn" style={{background:goal.color}}
            onClick={() => {
              // Convert extra/mo to extra per period for this frequency
              const f = FREQ[goal.frequency];
              const perPeriodExtra = extra / f.periodsPerMonth;
              updateGoal(goal.id, g => ({ ...g, contributionAmount: g.contributionAmount + perPeriodExtra }));
              setExtra(0);
            }}>
            Apply: bump to {fmt(calc.monthlyEq + extra)}/mo
          </button>
        )}
      </div>
      <div className="wi-slider-section">
        <label className="field-label">One-time deposit</label>
        <div className="slider-row">
          <span className="slider-val" style={{color:goal.color}}>{fmt(oneTime)}</span>
          <input type="range" className="slider" min="0" max="2000" step="50" value={oneTime} onChange={e => setOneTime(+e.target.value)} style={{accentColor:goal.color}}/>
        </div>
        <div className="slider-marks"><span>{C}0</span><span>{C}1,000</span><span>{C}2,000</span></div>
      </div>
    </div>
  );
}

// ============================================================
//  GOALS LIST
// ============================================================
function GoalsList({ goals, fmt, hide, T, settings, setActiveGoal, setSheet, onDelete, onReorder }) {
  const [confirmDelete, setConfirmDelete] = useState(null);
  return (
    <div className="stack fade-in">
      <div className="page-head"><h2>Goals</h2><p>Every target, one place.</p></div>
      {!goals.length && <EmptyState msg="No goals yet." sub="Tap below to start." T={T} action={() => setSheet({ type:'newGoal' })}/>}

      {goals.map((g, i) => {
        const c = calcGoal(g);
        const last = (g.contributions || [])[0];
        return (
          <div key={g.id} className="card goal-card" onClick={() => setActiveGoal(g.id)}>
            <div className="goal-top">
              <div className="goal-icon" style={{ background: g.color + '20', color: g.color }}>{g.icon}</div>
              <div className="goal-info">
                <div className="goal-name">
                  {g.name}
                  {g.paused && <span className="paused-sm"><I.Pause width={9} height={9}/> paused</span>}
                </div>
                <div className="goal-nums">{hide(fmt(c.balance))} of {fmt(g.target)}{last ? ` · last ${fmt(last.amount)}` : ''}</div>
              </div>
              <div className="goal-actions" onClick={e => e.stopPropagation()}>
                <div className="reorder-stack">
                  <button className="reorder-btn" disabled={i === 0} onClick={() => onReorder(g.id, -1)}><I.ChevUp width={10} height={10}/></button>
                  <button className="reorder-btn" disabled={i === goals.length - 1} onClick={() => onReorder(g.id, 1)}><I.ChevDown width={10} height={10}/></button>
                </div>
                <button className="sm-btn" onClick={() => setSheet({ type:'editGoal', goalId:g.id })}><I.Edit width={14} height={14}/></button>
                <button className="sm-btn danger" onClick={() => setConfirmDelete(g.id)}><I.Trash width={14} height={14}/></button>
              </div>
            </div>
            <div className="goal-bar-track"><div className="goal-bar-fill" style={{ width:`${c.pct}%`, background: g.color }}/></div>
            <div className="goal-bottom">
              <span>{c.pct.toFixed(0)}%</span>
              <span>
                {c.status === 'complete' ? '✓ Done'
                 : c.status === 'paused' ? 'Paused'
                 : c.status === 'ahead'  ? `${c.daysDiff}d ahead`
                 : c.status === 'ontrack' ? 'On track'
                 : c.status === 'late'   ? `${Math.abs(c.daysDiff)}d late`
                 : '—'}
              </span>
            </div>
            <div className="goal-ms-row">
              {(g.milestones || []).slice().sort((a, b) => a.pct - b.pct).map(m => (
                <div key={m.id} className={`goal-ms-dot ${c.pct >= m.pct ? 'reached' : ''}`}
                  style={c.pct >= m.pct ? { background: g.color, borderColor: g.color } : {}}>
                  {c.pct >= m.pct ? <I.Check width={8} height={8}/> : <span>{m.pct}</span>}
                </div>
              ))}
            </div>
          </div>
        );
      })}
      <button className="add-goal-btn" onClick={() => setSheet({ type:'newGoal' })}><I.Plus width={18} height={18}/> New goal</button>
      <div className="spacer"/>

      {confirmDelete && (
        <div className="sheet-overlay" onClick={() => setConfirmDelete(null)}>
          <div className="sheet" style={{ maxHeight: 'none' }} onClick={e => e.stopPropagation()}>
            <div className="sheet-handle"/>
            <div style={{ padding: '20px 20px calc(32px + env(safe-area-inset-bottom))', display:'flex', flexDirection:'column', gap:12, textAlign:'center' }}>
              <div style={{ fontSize:36 }}>🗑️</div>
              <div style={{ fontFamily:"'Playfair Display',Georgia,serif", fontSize:20, fontWeight:500 }}>Delete goal?</div>
              <div style={{ fontSize:13, color:T.textMute }}>This will permanently delete the goal and all its contributions. This cannot be undone.</div>
              <button className="danger-btn" style={{ marginTop:4 }} onClick={() => { onDelete(confirmDelete); setConfirmDelete(null); }}>Yes, delete</button>
              <button className="secondary-btn" onClick={() => setConfirmDelete(null)}>Cancel</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ============================================================
//  ACTIVITY VIEW
// ============================================================
function ActivityView({ goals, fmt, T, settings, onEdit }) {
  const [fGoal, setFGoal] = useState('all');
  const [fWho,  setFWho]  = useState('all');
  const [fType, setFType] = useState('all');
  const all = useMemo(() => {
    let items = [];
    goals.forEach(g => (g.contributions || []).forEach(c => items.push({ ...c, goalId:g.id, goalName:g.name, goalColor:g.color })));
    items.sort((a, b) => new Date(b.date) - new Date(a.date));
    if (fGoal !== 'all') items = items.filter(i => i.goalId === fGoal);
    if (fWho  !== 'all') items = items.filter(i => i.who === fWho);
    if (fType !== 'all') items = items.filter(i => i.type === fType);
    return items;
  }, [goals, fGoal, fWho, fType]);

  return (
    <div className="stack fade-in">
      <div className="page-head"><h2>Activity</h2><p>Every contribution tracked.</p></div>
      <div className="pill-filters">
        <div className="pill-group">
          {[{v:'all',l:'All goals'},...goals.map(g => ({v:g.id,l:g.icon+' '+g.name}))].map(o =>
            <button key={o.v} className={`pill ${fGoal===o.v?'active':''}`} onClick={() => setFGoal(o.v)}>{o.l}</button>
          )}
        </div>
        <div className="pill-group">
          {[{v:'all',l:'Everyone'},{v:settings.partner1,l:settings.partner1},{v:settings.partner2,l:settings.partner2}].map(o =>
            <button key={o.v} className={`pill ${fWho===o.v?'active':''}`} onClick={() => setFWho(o.v)}>{o.l}</button>
          )}
        </div>
        <div className="pill-group">
          {[{v:'all',l:'All types'},{v:'planned',l:'Planned'},{v:'unexpected',l:'Bonus'},{v:'adjustment',l:'Adjustment'}].map(o =>
            <button key={o.v} className={`pill ${fType===o.v?'active':''}`} onClick={() => setFType(o.v)}>{o.l}</button>
          )}
        </div>
      </div>

      {!all.length && <EmptyState msg="No activity yet." sub="Contributions will appear here." T={T}/>}
      {all.length > 0 && (
        <section className="card">
          {all.map(c => (
            <div key={c.id} className="cont-row" onClick={() => onEdit(c.goalId, c)}>
              <div className={`avatar ${c.who===settings.partner1?'p1':c.who===settings.partner2?'p2':'adj'}`}
                style={c.who===settings.partner1?{background:c.goalColor}:{}}>
                {c.type === 'adjustment' ? <I.Spark width={14} height={14}/> : (c.who || '?')[0]}
              </div>
              <div className="cont-mid">
                <div className="cont-who">
                  {c.type === 'adjustment' ? 'Adjustment' : c.who}
                  <span className="cont-tag"> · {c.goalName}</span>
                  {c.type === 'unexpected' && <span className="cont-tag"> · bonus</span>}
                </div>
                {c.note && <div className="cont-note">"{c.note}"</div>}
                <div className="cont-date">{new Date(c.date).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}</div>
              </div>
              <div className="cont-amt" style={{ color: c.amount < 0 ? T.danger : c.goalColor }}>
                {c.amount < 0 ? '−' : '+'}{fmt(c.amount)}
              </div>
            </div>
          ))}
        </section>
      )}
      <div className="spacer"/>
    </div>
  );
}

function EmptyState({ msg, sub, T, action }) {
  return (
    <div className="empty">
      <MugIcon size={48} color={T.textMute}/>
      <div className="empty-msg">{msg}</div>
      <div className="empty-sub">{sub}</div>
      {action && <button className="primary-btn" style={{ marginTop:16, maxWidth:200 }} onClick={action}>Create goal</button>}
    </div>
  );
}

// ============================================================
//  SHEETS
// ============================================================
function Sheet({ children, onClose, title }) {
  return (
    <div className="sheet-overlay" onClick={onClose}>
      <div className="sheet" onClick={e => e.stopPropagation()}>
        <div className="sheet-handle"/>
        <div className="sheet-header">
          <h3 className="sheet-title">{title}</h3>
          <button className="icon-btn" onClick={onClose}><I.X width={16} height={16}/></button>
        </div>
        <div className="sheet-body">{children}</div>
      </div>
    </div>
  );
}

function AddContributionSheet({ goalId, prefillAmount, settings, T, C, onClose, onAdd }) {
  const [amount, setAmount] = useState(prefillAmount ? String(prefillAmount) : '');
  const [who,    setWho]    = useState(settings.partner1);
  const [type,   setType]   = useState('planned');
  const [note,   setNote]   = useState('');
  const [date,   setDate]   = useState(new Date().toISOString().slice(0, 10));
  const qa = [{l:'Cashback',a:25},{l:'Overtime',a:100},{l:'Marketplace',a:75},{l:'Tax refund',a:500}];

  return (
    <Sheet onClose={onClose} title="Add contribution">
      <div className="amt-wrap">
        <span className="amt-cur">{C}</span>
        <input className="amt-input" type="number" inputMode="decimal" value={amount}
          onChange={e => setAmount(e.target.value)} placeholder="0" autoFocus/>
      </div>

      <label className="field-label">Who</label>
      <div className="who-row">
        {[settings.partner1, settings.partner2].map(p => (
          <button key={p} className={`who-btn ${who===p?'active':''}`} onClick={() => setWho(p)}>
            <span className={`avatar sm ${p===settings.partner1?'p1':'p2'}`}>{p[0]}</span>{p}
          </button>
        ))}
      </div>

      <label className="field-label">Type</label>
      <div className="type-row3">
        {[{v:'planned',l:'Planned'},{v:'unexpected',l:'Bonus'},{v:'adjustment',l:'Adjustment'}].map(t => (
          <button key={t.v} className={`type-btn ${type===t.v?'active':''}`} onClick={() => setType(t.v)}>{t.l}</button>
        ))}
      </div>
      {type === 'adjustment' && (
        <div className="hint-row">Adjustments sync your real account balance — interest, market changes, fees. Can be negative.</div>
      )}
      {type === 'unexpected' && (
        <div className="quick-adds">{qa.map(q => <button key={q.l} className="quick-add" onClick={() => setAmount(String(q.a))}>{q.l}</button>)}</div>
      )}

      <label className="field-label">Date</label>
      <input className="note-input" type="date" value={date} onChange={e => setDate(e.target.value)}/>

      <label className="field-label">Note (optional)</label>
      <input className="note-input" value={note} onChange={e => setNote(e.target.value)} placeholder="e.g. tax refund, interest"/>

      <button className="primary-btn" onClick={() => {
        const n = parseFloat(amount);
        if (!n || isNaN(n)) return;
        // Adjustments can be negative; user can prefix amount with - for negative.
        const signed = type === 'adjustment' ? n : Math.abs(n);
        onAdd({ amount: signed, who: type === 'adjustment' ? null : who, type, note: note || null, date, createdAt: new Date().toISOString() });
      }}>
        Add{amount ? ` ${C}${amount}` : ''}
      </button>
    </Sheet>
  );
}

function EditContributionSheet({ cont, settings, T, C, onClose, onSave, onDelete }) {
  const [amount, setAmount] = useState(String(cont.amount));
  const [who,    setWho]    = useState(cont.who || settings.partner1);
  const [type,   setType]   = useState(cont.type || 'planned');
  const [note,   setNote]   = useState(cont.note || '');
  const [date,   setDate]   = useState(cont.date);

  return (
    <Sheet onClose={onClose} title="Edit contribution">
      <div className="amt-wrap">
        <span className="amt-cur">{C}</span>
        <input className="amt-input" type="number" inputMode="decimal" value={amount} onChange={e => setAmount(e.target.value)}/>
      </div>
      {type !== 'adjustment' && (
        <>
          <label className="field-label">Who</label>
          <div className="who-row">
            {[settings.partner1, settings.partner2].map(p => (
              <button key={p} className={`who-btn ${who===p?'active':''}`} onClick={() => setWho(p)}>{p}</button>
            ))}
          </div>
        </>
      )}
      <label className="field-label">Type</label>
      <div className="type-row3">
        {[{v:'planned',l:'Planned'},{v:'unexpected',l:'Bonus'},{v:'adjustment',l:'Adjustment'}].map(t => (
          <button key={t.v} className={`type-btn ${type===t.v?'active':''}`} onClick={() => setType(t.v)}>{t.l}</button>
        ))}
      </div>
      <label className="field-label">Date</label>
      <input className="note-input" type="date" value={date} onChange={e => setDate(e.target.value)}/>
      <label className="field-label">Note (optional)</label>
      <input className="note-input" value={note} onChange={e => setNote(e.target.value)}/>
      <button className="primary-btn" onClick={() => {
        const n = parseFloat(amount);
        if (isNaN(n)) return;
        const signed = type === 'adjustment' ? n : Math.abs(n);
        onSave({ amount: signed, who: type === 'adjustment' ? null : who, type, note: note || null, date, createdAt: cont.createdAt || new Date().toISOString() });
      }}>Save</button>
      <button className="danger-btn" onClick={onDelete}><I.Trash width={14} height={14}/> Delete</button>
    </Sheet>
  );
}

// ============================================================
//  GOAL CREATE / EDIT SHEET
// ============================================================
function GoalSheet({ T, C, settings, goal, onClose, onSave }) {
  const isEdit = !!goal;
  const [name, setName]                       = useState(goal?.name || '');
  const [target, setTarget]                   = useState(goal?.target?.toString() || '3500');
  const [currentBalance, setCurrentBalance]   = useState(goal?.currentBalance?.toString() || '0');
  const [color, setColor]                     = useState(goal?.color || GOAL_COLORS[0]);
  const [icon, setIcon]                       = useState(goal?.icon || '☕');
  const [targetDate, setTargetDate]           = useState(goal?.targetDate || (() => { const d = new Date(); d.setMonth(d.getMonth() + 7); return d.toISOString().slice(0, 10); })());
  const [frequency, setFrequency]             = useState(goal?.frequency || 'monthly');
  const [contributionAmount, setContributionAmount] = useState(goal?.contributionAmount?.toString() || '400');
  const [paused, setPaused]                   = useState(goal?.paused || false);
  const [ms, setMs] = useState(goal?.milestones ? goal.milestones.map(m => ({ ...m })) : [
    { id:uid(), pct:25,  label:'Foundation set', reward:'' },
    { id:uid(), pct:50,  label:'Halfway home',   reward:'' },
    { id:uid(), pct:75,  label:'Within reach',   reward:'' },
    { id:uid(), pct:100, label:'Full reserve',   reward:'' },
  ]);

  const addMs    = () => setMs([...ms, { id:uid(), pct:50, label:'', reward:'' }]);
  const remMs    = id => setMs(ms.filter(m => m.id !== id));
  const updMs    = (id, f, v) => setMs(ms.map(m => m.id === id ? { ...m, [f]: v } : m));

  // Live preview: where will this finish?
  const previewCalc = useMemo(() => {
    const t  = parseFloat(target) || 0;
    const cb = parseFloat(currentBalance) || 0;
    const ca = parseFloat(contributionAmount) || 0;
    const tempGoal = { target:t, currentBalance:cb, frequency, contributionAmount:ca, contributions:[], targetDate, paused };
    return calcGoal(tempGoal);
  }, [target, currentBalance, contributionAmount, frequency, targetDate, paused]);

  const previewDateStr = previewCalc.projectedDate
    ? previewCalc.projectedDate.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})
    : null;

  const handleSave = () => {
    const t  = parseFloat(target);
    const cb = parseFloat(currentBalance) || 0;
    const ca = parseFloat(contributionAmount) || 0;
    if (!name.trim() || !t || t <= 0) return;
    onSave({ name: name.trim(), target: t, currentBalance: cb, color, icon, targetDate, frequency, contributionAmount: ca, paused, milestones: ms });
  };

  return (
    <Sheet onClose={onClose} title={isEdit ? 'Edit goal' : 'New goal'}>
      <label className="field-label">Name</label>
      <input className="note-input" value={name} onChange={e => setName(e.target.value)} placeholder="Emergency Fund"/>

      <label className="field-label">Target amount</label>
      <div className="amt-wrap sm">
        <span className="amt-cur">{C}</span>
        <input className="amt-input sm" type="number" value={target} onChange={e => setTarget(e.target.value)}/>
      </div>

      <label className="field-label">Current balance{!isEdit && ' (already saved)'}</label>
      <div className="amt-wrap sm">
        <span className="amt-cur">{C}</span>
        <input className="amt-input sm" type="number" value={currentBalance} onChange={e => setCurrentBalance(e.target.value)}/>
      </div>
      {!isEdit && parseFloat(currentBalance) > 0 && (
        <div className="hint-row">Pre-loads the mug. Won't appear in activity feed.</div>
      )}

      <label className="field-label">Contribution frequency</label>
      <div className="freq-row">
        {Object.entries(FREQ).map(([k, v]) => (
          <button key={k} className={`freq-btn ${frequency===k?'active':''}`} onClick={() => setFrequency(k)}>{v.label}</button>
        ))}
      </div>

      {frequency !== 'none' && (
        <>
          <label className="field-label">Amount per {FREQ[frequency].label.toLowerCase().replace('-','‑')}</label>
          <div className="amt-wrap sm">
            <span className="amt-cur">{C}</span>
            <input className="amt-input sm" type="number" value={contributionAmount} onChange={e => setContributionAmount(e.target.value)}/>
          </div>
        </>
      )}

      <label className="field-label">{frequency === 'none' ? 'Deadline (optional)' : 'Target date'}</label>
      <input className="note-input" type="date" value={targetDate} onChange={e => setTargetDate(e.target.value)}/>

      {/* Live preview */}
      {previewCalc.target > 0 && (
        <div className="preview-card">
          <div className="preview-label">Projected finish</div>
          <div className="preview-row">
            {previewDateStr ? (
              <>
                <div className="preview-date" style={{ color }}>{previewDateStr}</div>
                {previewCalc.status === 'late' && (
                  <div className="preview-late">{Math.abs(previewCalc.daysDiff)}d late</div>
                )}
                {previewCalc.status === 'ahead' && (
                  <div className="preview-ahead">{previewCalc.daysDiff}d ahead</div>
                )}
                {previewCalc.status === 'complete' && (
                  <div className="preview-ahead">Already complete</div>
                )}
              </>
            ) : (
              <>
                {previewCalc.suggestedMonthly !== null ? (
                  <div className="preview-date">Save ~{C}{Math.ceil(previewCalc.suggestedMonthly).toLocaleString()}/mo to hit deadline</div>
                ) : (
                  <div className="preview-date muted">Add contributions to project a finish date</div>
                )}
              </>
            )}
          </div>
        </div>
      )}

      <label className="field-label">Color</label>
      <div className="color-row">
        {GOAL_COLORS.map(c => <button key={c} className={`color-btn ${color===c?'active':''}`} style={{ background:c }} onClick={() => setColor(c)}/>)}
      </div>

      <label className="field-label">Icon</label>
      <div className="icon-row">
        {GOAL_ICONS.map(ic => <button key={ic} className={`icon-pick ${icon===ic?'active':''}`} onClick={() => setIcon(ic)}>{ic}</button>)}
      </div>

      <div style={{ marginTop:8 }}>
        <label className="field-label">Milestones</label>
        {ms.slice().sort((a, b) => a.pct - b.pct).map(m => (
          <div key={m.id} className="ms-edit">
            <div className="ms-edit-top">
              <div className="ms-pct-wrap">
                <input className="ms-pct" type="number" min="1" max="100" value={m.pct} onChange={e => updMs(m.id, 'pct', +e.target.value)}/><span>%</span>
              </div>
              <button className="sm-btn danger" onClick={() => remMs(m.id)}><I.Trash width={14} height={14}/></button>
            </div>
            <input className="note-input" value={m.label} onChange={e => updMs(m.id, 'label', e.target.value)} placeholder="Label"/>
            <input className="note-input" value={m.reward} onChange={e => updMs(m.id, 'reward', e.target.value)} placeholder="Reward"/>
          </div>
        ))}
        <button className="add-ms-btn" onClick={addMs}><I.Plus width={14} height={14}/> Add milestone</button>
      </div>

      {isEdit && (
        <div className="toggle-row">
          <div>
            <div className="toggle-title">{paused ? 'Goal paused' : 'Goal active'}</div>
            <div className="toggle-sub">{paused ? 'Projected date frozen' : 'Recurring contributions counted'}</div>
          </div>
          <button className={`toggle ${paused?'on':''}`} onClick={() => setPaused(!paused)}><span className="toggle-knob"/></button>
        </div>
      )}

      <button className="primary-btn" onClick={handleSave}>{isEdit ? 'Save' : 'Create goal'}</button>
    </Sheet>
  );
}

// ============================================================
//  SETTINGS SHEET
// ============================================================
function SettingsSheet({ T, settings, dark, setDark, householdId, connectHousehold, generateHouseholdId, leaveHousehold, deleteHousehold, syncStatus, onClose, onSave, wheel, onSaveWheel, authUser, myName, onSignOut }) {
  const [s, setS]               = useState({ ...settings });
  const [showCL, setShowCL]     = useState(false);
  const [joinCode, setJoinCode] = useState('');
  const [confirmLeave, setConfirmLeave]   = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [switching, setSwitching]         = useState(false);
  const [showWheel, setShowWheel]         = useState(false);
  const [localSegs, setLocalSegs]         = useState((wheel?.segments || DEFAULT_SEGMENTS).map(sg => ({ ...sg })));

  const updSeg = (i, key, val) => setLocalSegs(prev => prev.map((sg, idx) => idx === i ? { ...sg, [key]: val } : sg));
  const saveWheelCfg = () => {
    const next = { ...(wheel || { spins:{} }), segments: localSegs };
    onSaveWheel(next);
    setShowWheel(false);
  };

  return (
    <Sheet onClose={onClose} title="Settings">
      <label className="field-label">Partner 1</label>
      <input className="note-input" value={s.partner1} onChange={e => setS({ ...s, partner1: e.target.value })}/>
      <label className="field-label">Partner 2</label>
      <input className="note-input" value={s.partner2} onChange={e => setS({ ...s, partner2: e.target.value })}/>
      <label className="field-label">Currency symbol</label>
      <input className="note-input" value={s.currency} onChange={e => setS({ ...s, currency: e.target.value })} style={{ maxWidth: 80 }}/>
      <div className="toggle-row">
        <div><div className="toggle-title">Hide balances</div><div className="toggle-sub">Privacy mode</div></div>
        <button className={`toggle ${s.hideBalances?'on':''}`} onClick={() => setS({ ...s, hideBalances: !s.hideBalances })}><span className="toggle-knob"/></button>
      </div>
      <div className="toggle-row">
        <div><div className="toggle-title">Dark mode</div></div>
        <button className={`toggle ${dark?'on':''}`} onClick={() => setDark(!dark)}><span className="toggle-knob"/></button>
      </div>

      <div className="sync-section">
        <label className="field-label">Household sync</label>
        {householdId && !switching ? (
          <div className="sync-info">
            <div className="sync-status-row">
              <I.Cloud width={14} height={14}/>
              <span className="sync-code">{householdId}</span>
              <span className={`sync-badge ${syncStatus}`}>{syncStatus==='live'?'Live':syncStatus==='connecting'?'...':'Offline'}</span>
            </div>
            <div className="sync-hint">Share this code with {s.partner2 || 'your partner'} to sync devices.</div>
            <div className="sync-actions">
              <button className="secondary-btn compact" onClick={() => setSwitching(true)}><I.Link width={12} height={12}/> Switch</button>
              <button className="secondary-btn compact" onClick={() => setConfirmLeave(true)}>Leave</button>
              <button className="secondary-btn compact danger-outline" onClick={() => setConfirmDelete(true)}><I.Trash width={12} height={12}/> Delete</button>
            </div>
          </div>
        ) : (
          <div className="sync-setup">
            <button className="secondary-btn" onClick={() => { generateHouseholdId(); setSwitching(false); }}>
              <I.Link width={14} height={14}/> Create new household
            </button>
            <div className="sync-or">or join existing</div>
            <div className="sync-join-row">
              <input className="note-input" value={joinCode} onChange={e => setJoinCode(e.target.value)} placeholder="Enter household code"/>
              <button className="secondary-btn compact" onClick={() => { if (joinCode.trim()) { connectHousehold(joinCode.trim()); setSwitching(false); }}}>Join</button>
            </div>
            {switching && <button className="secondary-btn" onClick={() => setSwitching(false)}>Cancel</button>}
          </div>
        )}
      </div>

      {/* Wheel config */}
      <div>
        <button className="link-btn" onClick={() => setShowWheel(!showWheel)}>
          {showWheel ? '▾' : '▸'} Spin wheel segments
        </button>
        {showWheel && (
          <div style={{ marginTop:10, display:'flex', flexDirection:'column', gap:8 }}>
            {localSegs.map((seg, i) => (
              <div key={seg.id} style={{ background:T.bg, border:`1px solid ${T.border}`, borderRadius:12, padding:'10px 12px', display:'flex', gap:8, alignItems:'flex-start' }}>
                <input
                  style={{ fontSize:20, width:36, background:'transparent', border:'none', outline:'none', textAlign:'center', flexShrink:0, padding:0 }}
                  value={seg.emoji} onChange={e => updSeg(i,'emoji',e.target.value)}
                />
                <div style={{ flex:1, minWidth:0, display:'flex', flexDirection:'column', gap:4 }}>
                  <input className="note-input" style={{ padding:'6px 10px', fontSize:13 }} value={seg.label} onChange={e => updSeg(i,'label',e.target.value)}/>
                  <input className="note-input" style={{ padding:'5px 10px', fontSize:11 }} value={seg.sub} onChange={e => updSeg(i,'sub',e.target.value)}/>
                  <div style={{ display:'flex', alignItems:'center', gap:6, marginTop:2 }}>
                    <span style={{ fontSize:10, color:T.textMute, fontWeight:500, textTransform:'uppercase', letterSpacing:'0.08em' }}>Weight</span>
                    {[1,2,3,4,5].map(w => (
                      <button key={w} onClick={() => updSeg(i,'weight',w)}
                        style={{ width:22, height:22, borderRadius:6, border:`1.5px solid ${(seg.weight||1)>=w?T.accent:T.border}`, background:(seg.weight||1)>=w?`${T.accent}20`:'transparent', fontSize:10, fontWeight:600, color:(seg.weight||1)>=w?T.accent:T.textMute }}>
                        {w}
                      </button>
                    ))}
                  </div>
                </div>
                <button className={`toggle ${seg.enabled!==false?'on':''}`} style={{ flexShrink:0, marginTop:2 }} onClick={() => updSeg(i,'enabled',seg.enabled===false)}>
                  <span className="toggle-knob"/>
                </button>
              </div>
            ))}
            <button className="secondary-btn" onClick={saveWheelCfg}>Save wheel config</button>
          </div>
        )}
      </div>

      {/* Auth / sign out */}
      {authUser && (
        <div style={{ background:T.bg, border:`1px solid ${T.border}`, borderRadius:14, padding:'12px 14px' }}>
          <div style={{ fontSize:11, textTransform:'uppercase', letterSpacing:'0.1em', color:T.textMute, fontWeight:500, marginBottom:6 }}>Account</div>
          <div style={{ fontSize:13, marginBottom:4 }}>Signed in as <strong>{myName || 'unknown'}</strong></div>
          <div style={{ fontSize:11, color:T.textMute, marginBottom:10, wordBreak:'break-all' }}>{authUser.email}</div>
          <button className="secondary-btn danger-outline" style={{ width:'100%', justifyContent:'center' }} onClick={onSignOut}>Sign out</button>
        </div>
      )}

      <button className="link-btn" style={{ marginTop:4 }} onClick={() => setShowCL(!showCL)}>Version history</button>
      {showCL && (
        <div className="changelog">
          {CHANGELOG.map(c => (
            <div key={c.ver} className="cl-entry">
              <div className="cl-ver">v{c.ver} <span className="cl-date">{c.date}</span></div>
              <div className="cl-notes">{c.notes}</div>
            </div>
          ))}
        </div>
      )}

      <button className="primary-btn" onClick={() => onSave(s)}>Save</button>
      <div className="version-tag">In Time v{VERSION}</div>

      {/* Leave confirm */}
      {confirmLeave && (
        <div className="sheet-overlay" onClick={() => setConfirmLeave(false)}>
          <div className="sheet" style={{ maxHeight:'none' }} onClick={e => e.stopPropagation()}>
            <div className="sheet-handle"/>
            <div style={{ padding:'20px 20px calc(32px + env(safe-area-inset-bottom))', display:'flex', flexDirection:'column', gap:12, textAlign:'center' }}>
              <div style={{ fontSize:36 }}>👋</div>
              <div style={{ fontFamily:"'Playfair Display',Georgia,serif", fontSize:20, fontWeight:500 }}>Leave household?</div>
              <div style={{ fontSize:13, color:T.textMute }}>You'll stop syncing with this household but the data will remain for your partner.</div>
              <button className="danger-btn" onClick={() => { leaveHousehold(); setConfirmLeave(false); onClose(); }}>Leave</button>
              <button className="secondary-btn" onClick={() => setConfirmLeave(false)}>Cancel</button>
            </div>
          </div>
        </div>
      )}

      {/* Delete confirm */}
      {confirmDelete && (
        <div className="sheet-overlay" onClick={() => setConfirmDelete(false)}>
          <div className="sheet" style={{ maxHeight:'none' }} onClick={e => e.stopPropagation()}>
            <div className="sheet-handle"/>
            <div style={{ padding:'20px 20px calc(32px + env(safe-area-inset-bottom))', display:'flex', flexDirection:'column', gap:12, textAlign:'center' }}>
              <div style={{ fontSize:36 }}>⚠️</div>
              <div style={{ fontFamily:"'Playfair Display',Georgia,serif", fontSize:20, fontWeight:500 }}>Delete household?</div>
              <div style={{ fontSize:13, color:T.textMute }}>This permanently deletes ALL goals and contributions from cloud sync. Your partner will lose access too. Local data on your devices stays.</div>
              <button className="danger-btn" onClick={async () => { await deleteHousehold(); setConfirmDelete(false); onClose(); }}>Yes, delete forever</button>
              <button className="secondary-btn" onClick={() => setConfirmDelete(false)}>Cancel</button>
            </div>
          </div>
        </div>
      )}
    </Sheet>
  );
}

// ============================================================
//  CELEBRATION
// ============================================================
function Celebration({ milestone, goal, full, T, onClose }) {
  useEffect(() => { const t = setTimeout(onClose, full ? 8000 : 5000); return () => clearTimeout(t); }, [onClose, full]);
  const count = full ? 60 : 28;
  const dots = Array.from({ length: count }, (_, i) => ({
    left: Math.random() * 100,
    delay: Math.random() * (full ? 1.2 : 0.8),
    duration: (full ? 2.5 : 2) + Math.random() * 1.5,
    color: [goal.color,'#D4A574','#8B5E3C','#E8D4BC','#7A8B5E','#fff'][i % 6],
    size: full ? 5 + Math.random() * 10 : 6 + Math.random() * 6,
  }));
  return (
    <div className="celeb-overlay" onClick={onClose}>
      {dots.map((d, i) => (
        <div key={i} className="confetti" style={{
          left: `${d.left}%`, background: d.color,
          width: d.size, height: d.size * (full ? 2 : 1.5),
          borderRadius: full ? 1 : 2,
          animationDelay: `${d.delay}s`, animationDuration: `${d.duration}s`,
        }}/>
      ))}
      <div className="celeb-card">
        {full ? (
          <>
            <div style={{ fontSize:52, marginBottom:8 }}>🎉</div>
            <div className="celeb-pct" style={{ color:goal.color, fontSize:40 }}>Goal reached!</div>
            <div className="celeb-title">{goal.name}</div>
            <div style={{ fontSize:13, color:T.textMute, marginTop:8 }}>You did it together.</div>
          </>
        ) : (
          <>
            <div className="celeb-pct" style={{ color:goal.color }}>{milestone.pct}%</div>
            <div className="celeb-title">{milestone.label || `${milestone.pct}% reached`}</div>
            {milestone.reward && <div className="celeb-reward">Reward: {milestone.reward}</div>}
          </>
        )}
      </div>
    </div>
  );
}

// ============================================================
//  AUTH SCREENS
// ============================================================
function LoadingScreen({ T }) {
  return (
    <div style={{ background:T.bg, minHeight:'100dvh', display:'flex', alignItems:'center', justifyContent:'center' }}>
      <style>{CSS(T)}</style>
      <MugIcon size={52} color={T.accent}/>
    </div>
  );
}

function LoginScreen({ T }) {
  const [email, setEmail]     = useState('');
  const [sent, setSent]       = useState(false);
  const [loading, setLoading] = useState(false);
  const [error, setError]     = useState('');

  const send = async () => {
    const e = email.trim();
    if (!e) return;
    setLoading(true); setError('');
    try { await sendMagicLink(e); setSent(true); }
    catch(err) { setError(err.message || 'Failed to send. Check your Firebase config.'); }
    finally { setLoading(false); }
  };

  return (
    <div className="auth-wrap">
      <style>{CSS(T)}</style>
      <div className="auth-card">
        <div className="auth-brand">
          <MugIcon size={52} color={T.accent}/>
          <div className="auth-title">In Time</div>
          <div className="auth-sub">{sent ? 'Check your inbox' : 'Sign in to continue'}</div>
        </div>

        {sent ? (
          <div className="auth-inner" style={{ textAlign:'center' }}>
            <div style={{ fontSize:48, marginBottom:4 }}>📬</div>
            <div style={{ fontFamily:"'Playfair Display',Georgia,serif", fontSize:18, fontWeight:500 }}>Link sent to</div>
            <div style={{ fontWeight:600, fontSize:14 }}>{email}</div>
            <div style={{ fontSize:12, color:T.textMute, lineHeight:1.6, marginTop:4 }}>
              Tap the link in your email on any device to sign in. Check spam if it doesn't arrive. It expires in 1 hour.
            </div>
            <button className="primary-btn" style={{ marginTop:4 }} onClick={send} disabled={loading}>
              {loading ? 'Sending…' : 'Resend link'}
            </button>
            <button className="secondary-btn" onClick={() => { setSent(false); setEmail(''); }}>
              Use a different email
            </button>
          </div>
        ) : (
          <div className="auth-inner">
            <div>
              <label className="field-label">Email address</label>
              <input
                className="note-input"
                type="email"
                inputMode="email"
                autoComplete="email"
                value={email}
                onChange={e => setEmail(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && send()}
                placeholder="you@email.com"
                style={{ marginTop:6 }}
              />
            </div>
            {error && (
              <div style={{ fontSize:12, color:T.danger, background:`${T.danger}10`, padding:'8px 12px', borderRadius:8, lineHeight:1.5 }}>{error}</div>
            )}
            <button className="primary-btn" onClick={send} disabled={loading || !email.trim()}>
              {loading ? 'Sending…' : 'Send sign-in link'}
            </button>
          </div>
        )}
        <div style={{ fontSize:11, color:T.textMute, textAlign:'center', lineHeight:1.6 }}>
          No password needed. A secure link is emailed to you each time.
        </div>
      </div>
    </div>
  );
}

function NamePickScreen({ T, settings, onPick }) {
  const [picking, setPicking] = useState(false);
  const pick = async name => { setPicking(true); await onPick(name); };

  return (
    <div className="auth-wrap">
      <style>{CSS(T)}</style>
      <div className="auth-card" style={{ textAlign:'center' }}>
        <div className="auth-brand">
          <MugIcon size={52} color={T.accent}/>
          <div className="auth-title">Who are you?</div>
          <div className="auth-sub" style={{ marginBottom:8 }}>
            This device will be permanently linked to your name.
          </div>
        </div>
        <div style={{ display:'flex', flexDirection:'column', gap:10 }}>
          {[settings.partner1, settings.partner2].filter(Boolean).map(name => (
            <button
              key={name}
              className="secondary-btn"
              style={{ padding:'16px 20px', fontSize:16, fontWeight:600, justifyContent:'center' }}
              disabled={picking}
              onClick={() => pick(name)}
            >
              {name}
            </button>
          ))}
        </div>
        <div style={{ fontSize:11, color:T.textMute, marginTop:16, lineHeight:1.6 }}>
          You can change this later in settings if needed.
        </div>
      </div>
    </div>
  );
}

// ============================================================
//  SPIN WHEEL
// ============================================================
function SpinWheel({ segments, T, canSpin, onResult }) {
  const [spinning, setSpinning] = useState(false);
  const svgRef      = useRef(null);
  const startRotRef = useRef(0);
  const animRef     = useRef(null);
  const N     = segments.length || 20;
  const SLICE = 360 / N;

  useEffect(() => () => { if (animRef.current) cancelAnimationFrame(animRef.current); }, []);

  const doSpin = () => {
    if (spinning || !canSpin || !segments.length) return;
    setSpinning(true);

    const winner     = pickWeightedSegment(segments);
    const winnerIdx  = segments.findIndex(s => s.id === winner.id);
    const extraSpins = 5 + Math.floor(Math.random() * 4);
    const segCenter  = winnerIdx * SLICE + SLICE / 2;
    const targetRot  = (360 - (segCenter % 360)) % 360;
    const curNorm    = ((startRotRef.current % 360) + 360) % 360;
    let delta        = targetRot - curNorm;
    if (delta <= 0) delta += 360;
    const totalRot   = startRotRef.current + extraSpins * 360 + delta;
    const duration   = 4500 + Math.random() * 1000;
    const startTime  = performance.now();
    const fromRot    = startRotRef.current;
    const easeOut    = t => 1 - Math.pow(1 - t, 4);

    const animate = now => {
      const p   = Math.min((now - startTime) / duration, 1);
      const rot = fromRot + (totalRot - fromRot) * easeOut(p);
      if (svgRef.current) svgRef.current.style.transform = `rotate(${rot}deg)`;
      if (p < 1) {
        animRef.current = requestAnimationFrame(animate);
      } else {
        startRotRef.current = totalRot;
        setSpinning(false);
        onResult(winner);
      }
    };
    animRef.current = requestAnimationFrame(animate);
  };

  const cx = 150, cy = 150, r = 146;
  const slicePaths = segments.map((seg, i) => {
    const sa = (i * SLICE - 90) * (Math.PI / 180);
    const ea = ((i + 1) * SLICE - 90) * (Math.PI / 180);
    const x1 = cx + r * Math.cos(sa), y1 = cy + r * Math.sin(sa);
    const x2 = cx + r * Math.cos(ea), y2 = cy + r * Math.sin(ea);
    const ma = ((i + 0.5) * SLICE - 90) * (Math.PI / 180);
    const tr = r * 0.68;
    const tx = cx + tr * Math.cos(ma), ty = cy + tr * Math.sin(ma);
    const ta = (i + 0.5) * SLICE - 90;
    return (
      <g key={seg.id}>
        <path
          d={`M${cx},${cy} L${x1.toFixed(2)},${y1.toFixed(2)} A${r},${r} 0 0,1 ${x2.toFixed(2)},${y2.toFixed(2)} Z`}
          fill={i % 2 === 0 ? seg.color : seg.color + 'CC'}
          stroke={T.surface} strokeWidth="1.5"
        />
        <text
          x={tx.toFixed(2)} y={ty.toFixed(2)}
          textAnchor="middle" dominantBaseline="middle"
          transform={`rotate(${ta.toFixed(2)},${tx.toFixed(2)},${ty.toFixed(2)})`}
          fontSize={N > 15 ? '13' : '15'} fontFamily="DM Sans,sans-serif"
          style={{ pointerEvents:'none' }}
        >{seg.emoji}</text>
      </g>
    );
  });

  return (
    <div className="wheel-wrap">
      <div className="wheel-container">
        <div className="wheel-pointer" style={{ borderTopColor:T.accentDeep }}/>
        {canSpin && !spinning && (
          <div className="wheel-pulse" style={{ background:`radial-gradient(circle,${T.accent}22 0%,transparent 70%)` }}/>
        )}
        <svg ref={svgRef} width="300" height="300" viewBox="0 0 300 300" style={{ display:'block' }}>
          <defs><filter id="wsh"><feDropShadow dx="0" dy="3" stdDeviation="6" floodOpacity="0.12"/></filter></defs>
          <g filter="url(#wsh)">
            {slicePaths}
            <circle cx={cx} cy={cy} r="27" fill={T.surface} stroke={T.border} strokeWidth="2"/>
            <circle cx={cx} cy={cy} r="13" fill={T.accent}/>
          </g>
        </svg>
      </div>
      <button
        className="primary-btn"
        style={{ maxWidth:260, opacity:(!canSpin||spinning)?0.6:1 }}
        disabled={!canSpin || spinning}
        onClick={doSpin}
      >
        {spinning ? 'Spinning…' : '🎰  Spin the wheel'}
      </button>
    </div>
  );
}

function SpinResultCard({ seg, T, isNew, onLogPenalty }) {
  const isWin  = seg.type === 'win';
  const isBad  = seg.type === 'bad';
  const color  = isWin ? T.good : isBad ? T.danger : T.textMute;
  const badge  = isWin ? '🎉 You won!' : isBad ? '💸 Oof.' : '😐 Neutral';
  return (
    <div className="card spin-result" style={{ textAlign:'center' }}>
      <div className="spin-type-badge" style={{ background:`${color}15`, color }}>{badge}</div>
      <div style={{ fontSize:52, lineHeight:1, margin:'8px 0 6px' }}>{seg.emoji}</div>
      <div style={{ fontFamily:"'Playfair Display',Georgia,serif", fontSize:22, fontWeight:500, marginBottom:4 }}>{seg.label}</div>
      <div style={{ fontSize:13, color:T.textMute, lineHeight:1.5 }}>{seg.sub}</div>
      {isBad && onLogPenalty && (
        <button className="danger-btn" style={{ marginTop:12 }} onClick={onLogPenalty}>
          💸 Log this contribution now
        </button>
      )}
    </div>
  );
}

function SpinWheelView({ wheel, myName, settings, T, C, onSpinComplete, onLogPenalty, householdId }) {
  const [finalResult,   setFinalResult]   = useState(null);
  const [playAgainUsed, setPlayAgainUsed] = useState(false);
  const [playAgainFlash,setPlayAgainFlash]= useState(false);

  if (!householdId) return (
    <div className="stack fade-in">
      <div className="page-head"><h2>Daily Spin</h2><p>Connect a household to use the spin wheel.</p></div>
      <div className="empty"><div style={{ fontSize:36 }}>🔗</div><div className="empty-msg">No household</div><div className="empty-sub">Set up household sync in Settings to start spinning.</div></div>
    </div>
  );

  if (wheel === null) return (
    <div className="stack fade-in">
      <div className="page-head"><h2>Daily Spin</h2></div>
      <div style={{ textAlign:'center', padding:'60px 0', color:T.textMute, fontSize:13 }}>Loading…</div>
    </div>
  );

  const segments      = (wheel?.segments || DEFAULT_SEGMENTS).filter(s => s.enabled !== false);
  const spins         = wheel?.spins || {};
  const todayKey      = spinTodayKey();
  const myTodaySpin   = spins[todayKey]?.[myName];
  const alreadySpun   = !!myTodaySpin;
  const partnerName   = myName === settings.partner1 ? settings.partner2 : settings.partner1;
  const partnerSpin   = spins[todayKey]?.[partnerName];
  const myStreak      = calcSpinStreak(spins, myName);
  const partnerStreak = calcSpinStreak(spins, partnerName);

  const myHistory = Object.entries(spins)
    .filter(([, s]) => s[myName])
    .sort(([a], [b]) => b.localeCompare(a))
    .slice(0, 7)
    .map(([date, s]) => ({ date, ...s[myName] }));

  const canSpin = !alreadySpun && !finalResult;

  const handleResult = seg => {
    if (seg.label === 'Re-Spin' && !playAgainUsed) {
      setPlayAgainUsed(true);
      setPlayAgainFlash(true);
      setTimeout(() => setPlayAgainFlash(false), 1600);
      return;
    }
    setFinalResult(seg);
    onSpinComplete(seg, myName);
  };

  const resultToShow = alreadySpun ? myTodaySpin : finalResult;

  return (
    <div className="stack fade-in">
      <div className="page-head">
        <h2>Daily Spin</h2>
        <p>One spin a day · playing as <strong>{myName}</strong></p>
      </div>

      {(myStreak > 0 || partnerStreak > 0) && (
        <div style={{ display:'flex', justifyContent:'center', gap:8, flexWrap:'wrap' }}>
          {myStreak > 0 && <div className="spin-streak">🔥 {myName} · {myStreak}-day streak</div>}
          {partnerStreak > 0 && <div className="spin-streak" style={{ opacity:0.7 }}>🔥 {partnerName} · {partnerStreak}-day streak</div>}
        </div>
      )}

      {playAgainFlash && (
        <div className="spin-play-again">🔄 Re-Spin!</div>
      )}

      {!alreadySpun && !finalResult && (
        <SpinWheel segments={segments} T={T} canSpin={canSpin} onResult={handleResult}/>
      )}

      {resultToShow && (
        <SpinResultCard seg={resultToShow} T={T} isNew={!!finalResult && !alreadySpun} onLogPenalty={onLogPenalty}/>
      )}

      {(alreadySpun || finalResult) && (
        <div className="card" style={{ textAlign:'center', padding:'14px 16px' }}>
          <div style={{ fontSize:12, color:T.textMute }}>⏰ Come back tomorrow for your next spin</div>
        </div>
      )}

      {partnerSpin && (
        <div className="spin-partner-card">
          <span style={{ fontSize:28 }}>{partnerSpin.emoji}</span>
          <div style={{ flex:1 }}>
            <div style={{ fontSize:10, textTransform:'uppercase', letterSpacing:'0.1em', color:T.textMute, fontWeight:500, marginBottom:2 }}>{partnerName}'s spin today</div>
            <div style={{ fontWeight:500, fontSize:14 }}>{partnerSpin.label}</div>
            <div style={{ fontSize:12, color:T.textMute }}>{partnerSpin.sub}</div>
          </div>
          <div style={{ fontSize:11, fontWeight:700, color: partnerSpin.type==='win'?T.good:partnerSpin.type==='bad'?T.danger:T.textMute, flexShrink:0 }}>
            {partnerSpin.type === 'win' ? 'Win' : partnerSpin.type === 'bad' ? 'Pay up' : '—'}
          </div>
        </div>
      )}

      {myHistory.length > 0 && (
        <div className="card">
          <div className="card-head"><h3>Your history</h3></div>
          <div className="activity-feed">
            {myHistory.map((entry, i) => {
              const c = entry.type==='win'?T.good:entry.type==='bad'?T.danger:T.textMute;
              return (
                <div key={i} className="feed-row" style={{ cursor:'default' }}>
                  <div className="spin-history-emoji" style={{ background:`${c}18` }}>{entry.emoji}</div>
                  <div className="feed-mid">
                    <div className="feed-line">
                      <span className="feed-who">{entry.label}</span>
                      <span className="feed-action"> · {entry.sub}</span>
                    </div>
                    <div className="feed-date">{new Date(entry.date + 'T12:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'})}</div>
                  </div>
                  <div style={{ fontSize:10, fontWeight:700, color:c, flexShrink:0 }}>
                    {entry.type==='win'?'Win':entry.type==='bad'?'Pay up':'—'}
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

// ── Themes ──────────────────────────────────────────────────
const lightT = { bg:'#F5F0E8', surface:'#FBF7F1', surfaceAlt:'#FFF', text:'#2A2420', textMute:'#8A7E72', border:'#E5DCCE', accent:'#C8956D', accentDeep:'#8B5E3C', accentSoft:'#E8D4BC', danger:'#B85850', good:'#7A8B5E', mugLight:'#F0EAE0', mugDark:'#E2D8CA' };
const darkT  = { bg:'#1A1614', surface:'#221C18', surfaceAlt:'#2B2420', text:'#F0E8DC', textMute:'#8A7E72', border:'#3A312A', accent:'#D4A574', accentDeep:'#A87648', accentSoft:'#3A2E24', danger:'#C97168', good:'#9CB07A', mugLight:'#3A312A', mugDark:'#2B2420' };

// ── CSS ─────────────────────────────────────────────────────
const CSS = T => `
  * { box-sizing:border-box; -webkit-tap-highlight-color:transparent; margin:0; }
  body { margin:0; }
  button { font-family:inherit; cursor:pointer; border:none; background:none; color:inherit; }
  input, textarea, select { font-family:inherit; }

  .shell { max-width:440px; margin:0 auto; min-height:100dvh; padding-bottom:calc(100px + env(safe-area-inset-bottom)); }
  .topbar { position:sticky; top:0; z-index:20; background:${T.bg}E8; backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px); padding:calc(14px + env(safe-area-inset-top)) 18px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid ${T.border}60; }
  .bottom-nav { position:fixed; bottom:0; left:50%; transform:translateX(-50%); width:100%; max-width:440px; display:grid; grid-template-columns:repeat(4,1fr); background:${T.bg}; border-top:1px solid ${T.border}; padding:8px 8px calc(8px + env(safe-area-inset-bottom)); z-index:15; }
  .fab { position:fixed; bottom:calc(86px + env(safe-area-inset-bottom)); z-index:16; width:52px; height:52px; border-radius:50%; color:white; display:flex; align-items:center; justify-content:center; box-shadow:0 6px 20px rgba(0,0,0,0.2); transition:transform 0.15s; right:calc(50% - 220px + 16px); }
  .fab:active { transform:scale(0.94); }
  @media(max-width:440px) { .fab { right:18px; } }

  .fade-in { animation:fadeIn 0.4s ease-out; }
  @keyframes fadeIn { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
  @keyframes fadeOut { to { opacity:0; visibility:hidden; } }
  @keyframes splashPop { from { transform:scale(0.6); opacity:0; } to { transform:scale(1); opacity:1; } }
  @keyframes splashFade { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
  @keyframes steamRise { 0%,100% { transform:translateY(0); opacity:0.25; } 50% { transform:translateY(-6px); opacity:0.5; } }
  @keyframes pulse { 0%,100% { opacity:1; } 50% { opacity:0.3; } }

  .brand { display:flex; align-items:center; gap:10px; }
  .brand-name { font-family:'Playfair Display',Georgia,serif; font-weight:600; font-size:16px; letter-spacing:0.14em; }
  .brand-sub { font-size:10.5px; color:${T.textMute}; margin-top:1px; }
  .topbar-clock { font-size:11px; color:${T.textMute}; font-weight:500; }
  .sync-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
  .sync-dot.live { background:${T.good}; box-shadow:0 0 6px ${T.good}80; }
  .sync-dot.connecting { background:${T.accent}; animation:pulse 1.5s infinite; }
  .sync-dot.offline { background:${T.border}; }
  .icon-btn { width:36px; height:36px; border-radius:50%; background:${T.surface}; display:flex; align-items:center; justify-content:center; transition:background 0.2s; flex-shrink:0; }

  /* ── Ticker ───────────────────────────────────────── */
  .ticker-wrap { padding:10px 0 0; background:${T.bg}; border-bottom:1px solid ${T.border}40; position:sticky; top:calc(54px + env(safe-area-inset-top)); z-index:18; }
  .ticker-scroll { display:flex; gap:8px; overflow-x:auto; padding:4px 16px 12px; scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch; scrollbar-width:none; }
  .ticker-scroll::-webkit-scrollbar { display:none; }
  .ticker-item { display:flex; align-items:center; gap:9px; padding:8px 12px 8px 8px; background:${T.surface}; border:1.5px solid ${T.border}; border-radius:14px; flex-shrink:0; scroll-snap-align:start; transition:all 0.2s; min-width:160px; }
  .ticker-item.active { box-shadow:0 2px 10px rgba(0,0,0,0.05); }
  .ticker-icon { width:28px; height:28px; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:14px; flex-shrink:0; }
  .ticker-mid { display:flex; flex-direction:column; align-items:flex-start; min-width:0; flex:1; }
  .ticker-name { font-weight:500; font-size:12px; max-width:100px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .ticker-status { font-size:10px; font-weight:500; margin-top:1px; }
  .ticker-status.good { color:${T.good}; }
  .ticker-status.late { color:${T.danger}; }
  .ticker-status.muted { color:${T.textMute}; }
  .ticker-pct { font-family:'Playfair Display',Georgia,serif; font-weight:500; font-size:14px; flex-shrink:0; }

  .page { padding:0 16px; }
  .stack > * + * { margin-top:12px; }
  .stack { padding-top:16px; }
  .page-head { padding:4px 2px; }
  .page-head h2 { font-family:'Playfair Display',Georgia,serif; font-weight:500; font-size:30px; letter-spacing:-0.02em; }
  .page-head p { color:${T.textMute}; margin-top:4px; font-size:13.5px; }
  .spacer { height:32px; }

  /* ── Hero ─────────────────────────────────────────── */
  .hero { background:${T.surface}; border:1px solid ${T.border}; border-radius:24px; padding:22px 18px 16px; text-align:center; position:relative; overflow:visible; }
  .hero::before { content:''; position:absolute; top:-40%; left:-20%; right:-20%; bottom:50%; background:radial-gradient(ellipse,${T.accentSoft}30 0%,transparent 60%); pointer-events:none; border-radius:24px; }
  .hero-label { display:inline-flex; align-items:center; gap:7px; font-size:10.5px; text-transform:uppercase; letter-spacing:0.16em; color:${T.textMute}; font-weight:500; }
  .dot { width:6px; height:6px; border-radius:50%; }
  .hero-balance { margin:12px 0 2px; display:flex; align-items:baseline; justify-content:center; gap:8px; }
  .hero-amt { font-family:'Playfair Display',Georgia,serif; font-size:48px; font-weight:500; letter-spacing:-0.04em; line-height:1; }
  .hero-of { font-size:13px; color:${T.textMute}; font-weight:500; }
  .paused-badge { position:absolute; top:12px; right:14px; background:${T.border}; color:${T.textMute}; font-size:10px; font-weight:600; text-transform:uppercase; letter-spacing:0.1em; padding:3px 10px; border-radius:100px; display:inline-flex; align-items:center; gap:4px; }
  .on-track-row { display:flex; justify-content:center; margin-top:8px; }
  .on-track-badge { display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:600; padding:5px 14px; border-radius:100px; }
  .on-track-badge.good { background:${T.good}18; color:${T.good}; }
  .on-track-badge.late { background:${T.danger}12; color:${T.danger}; }
  .on-track-badge.muted { background:${T.border}50; color:${T.textMute}; }

  /* ── Dates strip ──────────────────────────────────── */
  .dates-strip { background:${T.surface}; border:1px solid ${T.border}; border-radius:14px; padding:12px 16px; display:flex; align-items:center; justify-content:center; gap:14px; flex-wrap:wrap; }
  .dates-col { display:flex; flex-direction:column; align-items:center; min-width:0; }
  .dates-label { font-size:9px; text-transform:uppercase; letter-spacing:0.1em; color:${T.textMute}; font-weight:500; }
  .dates-val { font-family:'Playfair Display',Georgia,serif; font-size:15px; font-weight:500; margin-top:3px; }
  .dates-val.muted { color:${T.textMute}; text-decoration:line-through; opacity:0.7; }
  .dates-sep { color:${T.border}; font-size:18px; }

  /* ── Monthly meter ────────────────────────────────── */
  .monthly-meter { background:${T.surface}; border:1px solid ${T.border}; border-radius:16px; padding:14px 16px; }
  .mm-top { display:flex; justify-content:space-between; align-items:center; }
  .mm-label { font-size:10px; text-transform:uppercase; letter-spacing:0.1em; color:${T.textMute}; font-weight:500; }
  .mm-edit-btn { width:26px; height:26px; border-radius:7px; background:${T.bg}; display:flex; align-items:center; justify-content:center; color:${T.textMute}; }
  .mm-amounts { display:flex; align-items:baseline; gap:6px; margin-top:6px; flex-wrap:wrap; }
  .mm-current { font-family:'Playfair Display',Georgia,serif; font-size:24px; font-weight:500; line-height:1; }
  .mm-of { font-size:11px; color:${T.textMute}; }
  .mm-bonus { font-size:10px; color:${T.good}; font-weight:600; margin-left:4px; background:${T.good}15; padding:2px 6px; border-radius:6px; }
  .mm-track { height:8px; background:${T.border}; border-radius:100px; margin-top:10px; overflow:visible; position:relative; }
  .mm-fill-planned { height:100%; border-radius:100px; transition:width 0.8s cubic-bezier(.22,1,.36,1); }
  .mm-fill-bonus { height:100%; border-radius:0 100px 100px 0; position:absolute; top:0; opacity:0.55; transition:width 0.8s cubic-bezier(.22,1,.36,1); }
  .mm-sub { font-size:10.5px; color:${T.textMute}; margin-top:6px; }

  /* ── Countdown ────────────────────────────────────── */
  .countdown-bar { display:flex; align-items:center; justify-content:center; gap:6px; background:${T.surface}; border:1px solid ${T.border}; border-radius:14px; padding:12px 16px; }
  .cd-item { display:flex; flex-direction:column; align-items:center; min-width:36px; }
  .cd-num { font-family:'Playfair Display',Georgia,serif; font-size:22px; font-weight:500; line-height:1; }
  .cd-label { font-size:9px; text-transform:uppercase; letter-spacing:0.1em; color:${T.textMute}; margin-top:2px; }
  .cd-sep { font-size:18px; color:${T.border}; font-weight:300; margin-top:-6px; }
  .cd-to { font-size:10.5px; color:${T.textMute}; margin-left:8px; padding-left:10px; border-left:1px solid ${T.border}; }

  /* ── Stats ────────────────────────────────────────── */
  .stat-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
  .stat { background:${T.surface}; border:1px solid ${T.border}; border-radius:16px; padding:12px 14px; transition:all 0.15s; }
  .stat.tappable { cursor:pointer; }
  .stat.tappable:active { transform:scale(0.97); border-color:${T.accent}80; }
  .stat-label { font-size:10px; text-transform:uppercase; letter-spacing:0.1em; color:${T.textMute}; font-weight:500; display:flex; align-items:center; gap:4px; }
  .stat-val { font-family:'Playfair Display',Georgia,serif; font-size:24px; font-weight:500; margin-top:3px; letter-spacing:-0.02em; line-height:1.1; }
  .stat-sub { font-size:11px; color:${T.textMute}; margin-top:1px; }

  /* ── Activity feed ────────────────────────────────── */
  .activity-feed { display:flex; flex-direction:column; }
  .feed-row { display:flex; align-items:flex-start; gap:10px; padding:10px 0; border-bottom:1px solid ${T.border}40; cursor:pointer; }
  .feed-row:last-child { border-bottom:none; }
  .feed-mid { flex:1; min-width:0; }
  .feed-line { font-size:13px; line-height:1.35; }
  .feed-who { font-weight:600; }
  .feed-action { color:${T.textMute}; }
  .feed-amt { font-family:'Playfair Display',Georgia,serif; font-weight:600; }
  .feed-tag { color:${T.textMute}; font-size:11px; }
  .feed-note { font-family:'Playfair Display',Georgia,serif; font-style:italic; font-size:11.5px; color:${T.textMute}; margin-top:2px; }
  .feed-date { font-size:10.5px; color:${T.textMute}; margin-top:2px; }

  /* ── Cards & misc ─────────────────────────────────── */
  .card { background:${T.surface}; border:1px solid ${T.border}; border-radius:18px; padding:16px; }
  .card-head { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
  .card-head h3 { font-family:'Playfair Display',Georgia,serif; font-weight:500; font-size:18px; }
  .card-sub { font-size:11px; color:${T.textMute}; }
  .link-btn { display:inline-flex; align-items:center; gap:4px; font-size:12px; color:${T.accent}; font-weight:500; }
  .note-display { min-height:36px; display:flex; align-items:center; font-size:13.5px; cursor:pointer; }

  .ms-badges { display:flex; flex-wrap:wrap; gap:8px; }
  .ms-badge { display:flex; align-items:center; gap:8px; padding:8px 12px; border-radius:12px; border:1px solid ${T.border}; background:${T.bg}; flex:1 1 calc(50% - 4px); }
  .ms-badge-circle { width:26px; height:26px; border-radius:50%; border:1.5px dashed ${T.border}; display:flex; align-items:center; justify-content:center; font-size:9px; font-weight:600; color:${T.textMute}; flex-shrink:0; }
  .ms-badge.reached .ms-badge-circle { border-style:solid; color:white; }
  .ms-badge-label { font-size:11.5px; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .ms-badge-reward { font-size:10px; color:${T.accent}; font-style:italic; margin-top:1px; }

  .bar-chart { display:flex; align-items:flex-end; gap:6px; height:100px; padding-top:20px; }
  .bar-col { display:flex; flex-direction:column; align-items:center; flex:1; gap:4px; height:100%; }
  .bar-val { font-size:8px; color:${T.textMute}; font-weight:500; min-height:12px; text-align:center; line-height:1; }
  .bar-track { flex:1; width:100%; background:${T.border}; border-radius:4px; display:flex; align-items:flex-end; overflow:hidden; }
  .bar-fill { width:100%; border-radius:4px; transition:height 0.6s cubic-bezier(.22,1,.36,1); min-height:2px; }
  .bar-label { font-size:9px; color:${T.textMute}; font-weight:500; }

  .wi-toggle { cursor:pointer; margin-bottom:0; }
  .wi-body { display:flex; flex-direction:column; gap:14px; padding-top:8px; }
  .wi-comparison { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 0 4px; }
  .wi-col { flex:1; }
  .wi-col-label { font-size:10px; text-transform:uppercase; letter-spacing:0.1em; color:${T.textMute}; font-weight:500; }
  .wi-col-date { font-family:'Playfair Display',Georgia,serif; font-size:15px; font-weight:500; margin-top:3px; }
  .wi-col-sub { font-size:11px; color:${T.textMute}; margin-top:1px; }
  .wi-col-saved { font-size:11px; font-weight:600; margin-top:2px; }
  .wi-arrow { color:${T.textMute}; font-size:16px; flex-shrink:0; padding:0 4px; }
  .wi-slider-section { padding-top:4px; }

  .cont-row { display:flex; align-items:center; gap:10px; padding:10px 0; border-bottom:1px solid ${T.border}40; cursor:pointer; }
  .cont-row:last-child { border-bottom:none; }
  .avatar { width:34px; height:34px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:600; font-size:12px; color:white; flex-shrink:0; }
  .avatar.sm { width:24px; height:24px; font-size:10px; }
  .avatar.p1 { background:${T.accent}; }
  .avatar.p2 { background:${T.accentDeep}; }
  .avatar.adj { background:${T.surfaceAlt}; color:${T.accent}; border:1.5px solid ${T.accent}; }
  .cont-mid { flex:1; min-width:0; }
  .cont-who { font-weight:500; font-size:13px; }
  .cont-tag { color:${T.textMute}; font-weight:400; font-size:11px; }
  .cont-note { font-family:'Playfair Display',Georgia,serif; font-style:italic; font-size:12px; color:${T.textMute}; margin-top:1px; }
  .cont-date { font-size:10.5px; color:${T.textMute}; margin-top:1px; }
  .cont-amt { font-family:'Playfair Display',Georgia,serif; font-weight:500; font-size:15px; }

  /* ── Goals list ───────────────────────────────────── */
  .goal-card { cursor:pointer; transition:transform 0.15s; }
  .goal-card:active { transform:scale(0.98); }
  .goal-top { display:flex; align-items:center; gap:12px; }
  .goal-icon { width:40px; height:40px; border-radius:12px; display:flex; align-items:center; justify-content:center; font-size:18px; flex-shrink:0; }
  .goal-info { flex:1; min-width:0; }
  .goal-name { font-weight:500; font-size:15px; display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
  .paused-sm { display:inline-flex; align-items:center; gap:3px; font-size:9px; text-transform:uppercase; letter-spacing:0.08em; color:${T.textMute}; background:${T.border}; padding:1px 6px; border-radius:4px; font-weight:600; }
  .goal-nums { font-size:12px; color:${T.textMute}; margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .goal-actions { display:flex; gap:4px; align-items:center; flex-shrink:0; }
  .reorder-stack { display:flex; flex-direction:column; gap:1px; }
  .reorder-btn { width:18px; height:14px; border-radius:4px; background:${T.bg}; color:${T.textMute}; display:flex; align-items:center; justify-content:center; }
  .reorder-btn:disabled { opacity:0.3; cursor:default; }
  .sm-btn { width:32px; height:32px; border-radius:8px; background:${T.bg}; display:flex; align-items:center; justify-content:center; color:${T.textMute}; }
  .sm-btn.danger { color:${T.danger}; }
  .goal-bar-track { height:6px; background:${T.border}; border-radius:100px; margin:12px 0 8px; overflow:hidden; }
  .goal-bar-fill { height:100%; border-radius:100px; transition:width 0.8s cubic-bezier(.22,1,.36,1); }
  .goal-bottom { display:flex; justify-content:space-between; font-size:11px; color:${T.textMute}; }
  .goal-ms-row { display:flex; gap:5px; margin-top:10px; padding-top:10px; border-top:1px solid ${T.border}40; }
  .goal-ms-dot { width:22px; height:22px; border-radius:50%; border:1.5px dashed ${T.border}; display:flex; align-items:center; justify-content:center; font-size:7px; font-weight:600; color:${T.textMute}; }
  .goal-ms-dot.reached { border-style:solid; color:white; }
  .add-goal-btn { width:100%; padding:14px; background:${T.surface}; border:1.5px dashed ${T.border}; border-radius:16px; display:flex; align-items:center; justify-content:center; gap:6px; font-weight:500; font-size:14px; color:${T.textMute}; transition:all 0.2s; }
  .add-goal-btn:hover { border-color:${T.accent}; color:${T.accent}; }

  /* ── Filters ──────────────────────────────────────── */
  .pill-filters { display:flex; flex-direction:column; gap:6px; }
  .pill-group { display:flex; gap:6px; flex-wrap:wrap; }
  .pill { padding:6px 12px; border-radius:100px; border:1px solid ${T.border}; background:${T.surface}; color:${T.textMute}; font-size:12px; font-weight:500; transition:all 0.2s; white-space:nowrap; }
  .pill.active { background:${T.accent}18; border-color:${T.accent}60; color:${T.text}; }

  .nav-btn { display:flex; flex-direction:column; align-items:center; gap:2px; padding:5px 4px; color:${T.textMute}; font-size:9.5px; letter-spacing:0.02em; transition:color 0.2s; }
  .nav-btn.active { color:${T.accent}; }
  .nav-btn span { font-weight:500; }

  /* ── Sliders ──────────────────────────────────────── */
  .slider-row { display:flex; align-items:center; gap:12px; }
  .slider-val { font-family:'Playfair Display',Georgia,serif; font-size:20px; font-weight:500; min-width:60px; }
  .slider { flex:1; -webkit-appearance:none; appearance:none; height:4px; background:${T.border}; border-radius:100px; outline:none; }
  .slider::-webkit-slider-thumb { -webkit-appearance:none; width:20px; height:20px; border-radius:50%; background:${T.accent}; cursor:pointer; }
  .slider::-moz-range-thumb { width:20px; height:20px; border-radius:50%; background:${T.accent}; border:none; cursor:pointer; }
  .slider-marks { display:flex; justify-content:space-between; margin-top:6px; font-size:10px; color:${T.textMute}; }
  .apply-btn { margin-top:12px; width:100%; padding:12px; border-radius:12px; color:white; font-weight:500; font-size:13px; }

  /* ── Toggles ──────────────────────────────────────── */
  .toggle-row { display:flex; justify-content:space-between; align-items:center; padding:8px 0; gap:12px; }
  .toggle-title { font-weight:500; font-size:14px; }
  .toggle-sub { color:${T.textMute}; font-size:11.5px; margin-top:1px; }
  .toggle { width:46px; height:28px; background:${T.border}; border-radius:100px; position:relative; transition:background 0.3s; flex-shrink:0; }
  .toggle.on { background:${T.accent}; }
  .toggle-knob { position:absolute; top:3px; left:3px; width:22px; height:22px; border-radius:50%; background:white; box-shadow:0 2px 6px rgba(0,0,0,0.12); transition:transform 0.3s cubic-bezier(.22,1,.36,1); }
  .toggle.on .toggle-knob { transform:translateX(18px); }

  /* ── Sheets ───────────────────────────────────────── */
  .sheet-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:50; display:flex; align-items:flex-end; justify-content:center; animation:fadeIn 0.2s; }
  .sheet { width:100%; max-width:440px; background:${T.bg}; border-radius:24px 24px 0 0; border-top:1px solid ${T.border}; animation:slideUp 0.35s cubic-bezier(.22,1,.36,1); max-height:90dvh; display:flex; flex-direction:column; }
  @keyframes slideUp { from { transform:translateY(100%); } to { transform:translateY(0); } }
  .sheet-handle { width:36px; height:4px; background:${T.border}; border-radius:100px; margin:8px auto 0; flex-shrink:0; }
  .sheet-header { display:flex; justify-content:space-between; align-items:center; padding:12px 18px 8px; flex-shrink:0; }
  .sheet-title { font-family:'Playfair Display',Georgia,serif; font-weight:500; font-size:20px; }
  .sheet-body { padding:4px 18px calc(28px + env(safe-area-inset-bottom)); overflow-y:auto; display:flex; flex-direction:column; gap:10px; }

  .field-label { font-size:10px; text-transform:uppercase; letter-spacing:0.1em; color:${T.textMute}; font-weight:500; }
  .note-input { width:100%; padding:11px 13px; background:${T.surface}; border:1.5px solid ${T.border}; border-radius:12px; font-size:14px; outline:none; color:${T.text}; transition:border-color 0.2s; }
  .note-input:focus { border-color:${T.accent}; }
  .amt-wrap { display:flex; align-items:baseline; justify-content:center; gap:2px; }
  .amt-wrap.sm { justify-content:flex-start; }
  .amt-cur { font-family:'Playfair Display',Georgia,serif; font-size:28px; color:${T.textMute}; }
  .amt-wrap.sm .amt-cur { font-size:18px; }
  .amt-input { font-family:'Playfair Display',Georgia,serif; font-size:48px; font-weight:500; text-align:center; background:transparent; border:none; outline:none; color:${T.text}; width:60%; max-width:200px; -moz-appearance:textfield; }
  .amt-input.sm { font-size:22px; text-align:left; width:120px; }
  .amt-input::-webkit-outer-spin-button, .amt-input::-webkit-inner-spin-button { -webkit-appearance:none; }

  .hint-row { font-size:11px; color:${T.textMute}; padding:6px 12px; background:${T.surface}; border-radius:8px; line-height:1.4; }

  .who-row { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
  .who-btn { display:flex; align-items:center; justify-content:center; gap:7px; padding:10px; background:${T.surface}; border:1.5px solid ${T.border}; border-radius:12px; font-weight:500; font-size:13px; transition:all 0.2s; }
  .who-btn.active { border-color:${T.accent}; background:${T.accentSoft}30; }
  .type-row3 { display:grid; grid-template-columns:1fr 1fr 1fr; gap:6px; }
  .type-btn { padding:9px 6px; background:${T.surface}; border:1.5px solid ${T.border}; border-radius:12px; font-weight:500; font-size:12px; color:${T.textMute}; transition:all 0.2s; }
  .type-btn.active { border-color:${T.accent}; background:${T.accentSoft}30; color:${T.text}; }
  .quick-adds { display:grid; grid-template-columns:1fr 1fr; gap:6px; }
  .quick-add { padding:8px; background:${T.surface}; border:1px solid ${T.border}; border-radius:10px; font-size:11.5px; transition:all 0.2s; }

  /* ── Frequency picker ─────────────────────────────── */
  .freq-row { display:grid; grid-template-columns:repeat(3,1fr); gap:6px; }
  .freq-btn { padding:9px 6px; background:${T.surface}; border:1.5px solid ${T.border}; border-radius:10px; font-weight:500; font-size:11.5px; color:${T.textMute}; transition:all 0.2s; }
  .freq-btn.active { border-color:${T.accent}; background:${T.accentSoft}30; color:${T.text}; }

  /* ── Preview card in goal sheet ───────────────────── */
  .preview-card { background:${T.surface}; border:1px solid ${T.border}; border-radius:14px; padding:12px 14px; margin-top:4px; }
  .preview-label { font-size:10px; text-transform:uppercase; letter-spacing:0.1em; color:${T.textMute}; font-weight:500; }
  .preview-row { display:flex; flex-wrap:wrap; align-items:baseline; gap:8px; margin-top:4px; }
  .preview-date { font-family:'Playfair Display',Georgia,serif; font-size:17px; font-weight:500; }
  .preview-date.muted { color:${T.textMute}; font-size:12px; font-family:'DM Sans',sans-serif; font-weight:400; }
  .preview-late { font-size:11px; font-weight:600; color:${T.danger}; background:${T.danger}15; padding:2px 8px; border-radius:6px; }
  .preview-ahead { font-size:11px; font-weight:600; color:${T.good}; background:${T.good}15; padding:2px 8px; border-radius:6px; }

  .primary-btn { width:100%; padding:14px; background:linear-gradient(135deg,${T.accent},${T.accentDeep}); color:white; border-radius:14px; font-weight:600; font-size:14px; box-shadow:0 4px 14px ${T.accentDeep}30; transition:transform 0.15s; }
  .primary-btn:active { transform:scale(0.98); }
  .secondary-btn { padding:11px 16px; background:${T.surface}; border:1.5px solid ${T.border}; border-radius:12px; font-weight:500; font-size:13px; display:flex; align-items:center; justify-content:center; gap:6px; transition:all 0.2s; }
  .secondary-btn.compact { padding:9px 14px; font-size:12px; flex:1; }
  .secondary-btn.danger-outline { color:${T.danger}; border-color:${T.danger}40; }
  .danger-btn { width:100%; padding:12px; background:transparent; border:1px solid ${T.danger}40; color:${T.danger}; border-radius:12px; font-size:13px; display:flex; align-items:center; justify-content:center; gap:6px; }
  .color-row { display:flex; gap:8px; flex-wrap:wrap; }
  .color-btn { width:32px; height:32px; border-radius:50%; border:2px solid transparent; transition:all 0.2s; }
  .color-btn.active { border-color:${T.text}; transform:scale(1.15); }
  .icon-row { display:flex; gap:6px; flex-wrap:wrap; }
  .icon-pick { width:36px; height:36px; border-radius:10px; background:${T.surface}; border:1.5px solid ${T.border}; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.2s; }
  .icon-pick.active { border-color:${T.accent}; background:${T.accentSoft}30; }
  .ms-edit { background:${T.bg}; border:1px solid ${T.border}; border-radius:14px; padding:12px; display:flex; flex-direction:column; gap:6px; margin-top:6px; }
  .ms-edit-top { display:flex; justify-content:space-between; align-items:center; }
  .ms-pct-wrap { display:flex; align-items:baseline; gap:2px; font-weight:500; font-size:14px; color:${T.textMute}; }
  .ms-pct { width:48px; font-size:18px; font-weight:600; background:transparent; border:none; outline:none; color:${T.text}; text-align:center; font-family:'Playfair Display',Georgia,serif; }
  .add-ms-btn { width:100%; padding:10px; border:1.5px dashed ${T.border}; border-radius:12px; display:flex; align-items:center; justify-content:center; gap:5px; font-size:13px; color:${T.textMute}; font-weight:500; margin-top:6px; }

  /* ── Sync section ─────────────────────────────────── */
  .sync-section { background:${T.bg}; border:1px solid ${T.border}; border-radius:14px; padding:14px; }
  .sync-info { margin-top:8px; }
  .sync-status-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
  .sync-code { font-size:12px; font-weight:500; background:${T.surface}; padding:4px 10px; border-radius:8px; border:1px solid ${T.border}; word-break:break-all; }
  .sync-badge { font-size:10px; text-transform:uppercase; letter-spacing:0.08em; font-weight:600; padding:2px 8px; border-radius:100px; flex-shrink:0; }
  .sync-badge.live { background:${T.good}20; color:${T.good}; }
  .sync-badge.connecting { background:${T.accent}20; color:${T.accent}; }
  .sync-badge.offline { background:${T.border}40; color:${T.textMute}; }
  .sync-hint { font-size:11px; color:${T.textMute}; margin-top:8px; line-height:1.4; }
  .sync-actions { display:flex; gap:6px; margin-top:10px; }
  .sync-setup { margin-top:8px; display:flex; flex-direction:column; gap:8px; }
  .sync-or { font-size:11px; color:${T.textMute}; text-align:center; }
  .sync-join-row { display:flex; gap:6px; }
  .sync-join-row .note-input { flex:1; }

  .changelog { background:${T.bg}; border:1px solid ${T.border}; border-radius:12px; padding:12px; max-height:260px; overflow-y:auto; }
  .cl-entry + .cl-entry { margin-top:10px; padding-top:10px; border-top:1px solid ${T.border}40; }
  .cl-ver { font-weight:600; font-size:13px; }
  .cl-date { font-weight:400; color:${T.textMute}; font-size:11px; }
  .cl-notes { font-size:12px; color:${T.textMute}; margin-top:3px; line-height:1.5; }
  .version-tag { text-align:center; font-size:10px; color:${T.border}; margin-top:8px; letter-spacing:0.1em; }

  .empty { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:60px 20px; text-align:center; color:${T.textMute}; }
  .empty-msg { font-family:'Playfair Display',Georgia,serif; font-size:20px; margin-top:16px; color:${T.text}; }
  .empty-sub { font-size:13px; margin-top:6px; }

  /* ── Spin wheel ───────────────────────────────────────── */
  .wheel-wrap { display:flex; flex-direction:column; align-items:center; gap:20px; padding:4px 0 8px; }
  .wheel-container { position:relative; display:flex; align-items:center; justify-content:center; }
  .wheel-pointer { position:absolute; top:-14px; left:50%; transform:translateX(-50%); z-index:10; width:0; height:0; border-left:11px solid transparent; border-right:11px solid transparent; border-top:22px solid ${T.accentDeep}; filter:drop-shadow(0 2px 4px rgba(0,0,0,0.2)); }
  .wheel-pulse { position:absolute; inset:-10px; border-radius:50%; animation:pulse 2s ease-in-out infinite; pointer-events:none; }
  .spin-streak { display:inline-flex; align-items:center; gap:6px; background:${T.accent}18; border:1px solid ${T.accent}40; border-radius:100px; padding:6px 16px; font-size:12px; font-weight:700; color:${T.accentDeep}; align-self:center; }
  .spin-play-again { text-align:center; font-family:'Playfair Display',Georgia,serif; font-size:26px; font-weight:500; color:${T.accent}; padding:8px 0; animation:popIn 0.4s cubic-bezier(.22,1.4,.36,1); }
  .spin-result { animation:popIn 0.5s cubic-bezier(.22,1.4,.36,1); }
  .spin-type-badge { display:inline-block; font-size:10px; text-transform:uppercase; letter-spacing:0.14em; font-weight:700; padding:3px 12px; border-radius:100px; margin-bottom:10px; }
  .spin-history-emoji { width:34px; height:34px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:16px; flex-shrink:0; }
  .spin-partner-card { background:${T.surface}; border:1px solid ${T.border}; border-radius:16px; padding:14px 16px; display:flex; align-items:center; gap:12px; }
  .auth-wrap { background:${T.bg}; min-height:100dvh; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:32px 24px; font-family:'DM Sans',-apple-system,sans-serif; }
  .auth-card { width:100%; max-width:380px; display:flex; flex-direction:column; gap:16px; }
  .auth-inner { background:${T.surface}; border:1px solid ${T.border}; border-radius:20px; padding:24px 20px; display:flex; flex-direction:column; gap:14px; }
  .auth-brand { text-align:center; margin-bottom:8px; }
  .auth-title { font-family:'Playfair Display',Georgia,serif; font-size:28px; font-weight:500; letter-spacing:-0.02em; margin-top:12px; }
  .auth-sub { font-size:13px; color:${T.textMute}; margin-top:4px; }

  .celeb-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.5); backdrop-filter:blur(8px); z-index:100; display:flex; align-items:center; justify-content:center; overflow:hidden; animation:fadeIn 0.3s; }
  .celeb-card { background:${T.bg}; border-radius:24px; padding:36px 32px; text-align:center; border:1px solid ${T.border}; max-width:280px; animation:popIn 0.5s cubic-bezier(.22,1.4,.36,1); box-shadow:0 20px 60px rgba(0,0,0,0.3); }
  @keyframes popIn { from { transform:scale(0.7); opacity:0; } to { transform:scale(1); opacity:1; } }
  .celeb-pct { font-family:'Playfair Display',Georgia,serif; font-size:56px; font-weight:500; line-height:1; }
  .celeb-title { font-family:'Playfair Display',Georgia,serif; font-size:22px; font-weight:500; margin-top:10px; }
  .celeb-reward { font-size:14px; color:${T.accent}; margin-top:8px; font-style:italic; }
  .confetti { position:absolute; top:-10px; animation:fall linear forwards; }
  @keyframes fall { to { transform:translateY(105dvh) rotate(720deg); opacity:0; } }
`;
