// Reusable UI components: charts, cards, transaction rows

// ---- Donut chart (pure SVG) ----
function Donut({ data, size = 180, thickness = 22, gap = 1.5 }) {
  const total = data.reduce((s, d) => s + d.value, 0);
  if (total === 0) return null;
  const r = (size - thickness) / 2;
  const cx = size / 2, cy = size / 2;
  const circumference = 2 * Math.PI * r;
  let offset = 0;
  return (
    <svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
      {data.map((d, i) => {
        const frac = d.value / total;
        const len = frac * circumference - gap;
        const dash = `${Math.max(len, 0)} ${circumference}`;
        const dashoff = -offset;
        offset += frac * circumference;
        return (
          <circle key={i} cx={cx} cy={cy} r={r} fill="none"
            stroke={d.color} strokeWidth={thickness}
            strokeDasharray={dash} strokeDashoffset={dashoff}
            strokeLinecap="butt" />
        );
      })}
    </svg>
  );
}

// ---- Horizontal bar (for spending by card) ----
function HBar({ value, max, color, height = 8, bg = "var(--bg-muted)" }) {
  const pct = max > 0 ? Math.min(100, (value / max) * 100) : 0;
  return (
    <div style={{ height, width: "100%", background: bg, borderRadius: height/2, overflow: "hidden" }}>
      <div style={{ height: "100%", width: pct + "%", background: color, borderRadius: height/2, transition: "width 0.4s ease" }}/>
    </div>
  );
}

// ---- Sparkline / line chart for monthly trend ----
function MonthlyChart({ data, height = 180, accent = "var(--accent)" }) {
  const pad = { t: 16, r: 12, b: 28, l: 44 };
  const w = 600;
  const innerW = w - pad.l - pad.r;
  const innerH = height - pad.t - pad.b;
  const maxVal = Math.max(...data.flatMap(d => [d.expenses, d.income])) * 1.1;
  const x = (i) => pad.l + (i / (data.length - 1)) * innerW;
  const y = (v) => pad.t + innerH - (v / maxVal) * innerH;

  const expensesPath = data.map((d, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(d.expenses)}`).join(" ");
  const incomePath = data.map((d, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(d.income)}`).join(" ");

  const ticks = 4;
  const tickVals = Array.from({ length: ticks + 1 }, (_, i) => (maxVal / ticks) * i);

  return (
    <svg viewBox={`0 0 ${w} ${height}`} style={{ width: "100%", height: "auto", overflow: "visible" }}>
      {/* Y grid */}
      {tickVals.map((v, i) => (
        <g key={i}>
          <line x1={pad.l} x2={w - pad.r} y1={y(v)} y2={y(v)} stroke="var(--border-subtle)" strokeDasharray="2 4"/>
          <text x={pad.l - 8} y={y(v) + 4} fill="var(--text-muted)" fontSize="10" textAnchor="end">
            {v >= 1000 ? `${(v/1000).toFixed(0)}k` : v.toFixed(0)}
          </text>
        </g>
      ))}
      {/* Income area */}
      <path d={`${incomePath} L${x(data.length-1)},${pad.t + innerH} L${x(0)},${pad.t + innerH} Z`}
        fill="var(--accent-soft)" opacity="0.4"/>
      {/* Lines */}
      <path d={incomePath} fill="none" stroke="var(--text-2)" strokeWidth="1.5" strokeDasharray="4 3"/>
      <path d={expensesPath} fill="none" stroke={accent} strokeWidth="2.25"/>
      {/* Points */}
      {data.map((d, i) => (
        <g key={i}>
          <circle cx={x(i)} cy={y(d.expenses)} r="3" fill={accent}/>
          {i === data.length - 1 && (
            <circle cx={x(i)} cy={y(d.expenses)} r="6" fill="none" stroke={accent} strokeWidth="1.5" opacity="0.4"/>
          )}
          <text x={x(i)} y={height - 8} fill="var(--text-muted)" fontSize="10" textAnchor="middle">{d.month}</text>
        </g>
      ))}
    </svg>
  );
}

// ---- Miles bonus progress bar (the key feature) ----
function MilesProgressBar({ card, expanded = false }) {
  if (!card?.bonus) return null;
  const b = card.bonus;
  const spent = card.spent || 0;
  const m = computeMiles(card);

  // Multi-segment cards (PPV split, PRVI regional, KrisFlyer SQ-gate)
  if (b.kind === "ppv") {
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        <SubBar label="Online" spent={card.onlineSpent || 0} cap={b.onlineCapSgd} rate={b.rate} color={card.color}/>
        <SubBar label="Contactless" spent={card.contactlessSpent || 0} cap={b.contactlessCapSgd} rate={b.rate} color={card.color}/>
        <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11.5, color: "var(--text-muted)", paddingTop: 2 }}>
          <span>Bonus tier total: S${b.capSgd}/mo</span>
          <span style={{ fontVariantNumeric: "tabular-nums", color: "var(--text-2)" }}>{fmt.miles(m.miles)} mi earned</span>
        </div>
      </div>
    );
  }

  if (b.kind === "krisflyer") {
    const sqSpent = card.sqSpent || 0;
    const otherSpent = Math.max(0, spent - sqSpent);
    const sqUnlocked = sqSpent >= b.sqGateSgd;
    const sqPct = Math.min(100, (sqSpent / b.sqGateSgd) * 100);
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        <div>
          <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, marginBottom: 5 }}>
            <span style={{ color: "var(--text-muted)" }}>SQ spend gate <span style={{ color: sqUnlocked ? "var(--success)" : "var(--text-muted)", fontWeight: 600 }}>{sqUnlocked ? "✓ Unlocked" : "Locked"}</span></span>
            <span style={{ color: "var(--text-1)", fontVariantNumeric: "tabular-nums", fontWeight: 600 }}>
              {fmt.sgd(sqSpent, { decimals: 0 })} <span style={{ color: "var(--text-muted)", fontWeight: 400 }}>/ {fmt.sgd(b.sqGateSgd, { decimals: 0 })}</span>
            </span>
          </div>
          <div style={{ position: "relative", height: 8, background: "var(--bg-muted)", borderRadius: 4, overflow: "hidden" }}>
            <div style={{ height: "100%", width: sqPct + "%", background: sqUnlocked ? "var(--success)" : card.color, borderRadius: 4 }}/>
          </div>
          <div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 4 }}>
            {sqUnlocked
              ? "Earning 2.4mpd on accelerated · 3mpd on SQ"
              : `Earning 1.2mpd until S$${(b.sqGateSgd - sqSpent).toFixed(0)} more on SQ`}
          </div>
        </div>
        <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11.5, color: "var(--text-muted)", paddingTop: 2, borderTop: "1px solid var(--border-subtle)" }}>
          <span>Other spend: {fmt.sgd(otherSpent, { decimals: 0 })}</span>
          <span style={{ fontVariantNumeric: "tabular-nums", color: "var(--text-2)" }}>{fmt.miles(m.miles)} mi earned</span>
        </div>
      </div>
    );
  }

  if (b.kind === "prvi") {
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
        <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12 }}>
          <span style={{ color: "var(--text-muted)" }}>Uncapped · tiered by region</span>
          <span style={{ color: "var(--text-1)", fontVariantNumeric: "tabular-nums", fontWeight: 600 }}>{fmt.miles(m.miles)} mi</span>
        </div>
        <div style={{ display: "flex", height: 10, borderRadius: 5, overflow: "hidden", background: "var(--bg-muted)" }}>
          {m.segments.filter(s => s.spent > 0).map((s, i) => {
            const pct = (s.spent / spent) * 100;
            const colors = [card.color + "88", card.color, card.accent, "#10b981"];
            return <div key={i} style={{ width: pct + "%", background: colors[i] || card.color }} title={`${s.label}: ${fmt.sgd(s.spent, { decimals: 0 })}`}/>;
          })}
        </div>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 8, fontSize: 10.5, color: "var(--text-muted)", marginTop: 2 }}>
          {m.segments.filter(s => s.spent > 0).map((s, i) => {
            const colors = [card.color + "88", card.color, card.accent, "#10b981"];
            return (
              <span key={i} style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
                <span style={{ width: 6, height: 6, borderRadius: 1, background: colors[i] || card.color }}/>
                {s.label.replace(/ @ .*/, "")}: {fmt.sgd(s.spent, { decimals: 0 })}
              </span>
            );
          })}
        </div>
      </div>
    );
  }

  // Single-cap cards (Lady's, Citi Rewards, Trust)
  if (!b.capSgd) {
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
        <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12 }}>
          <span style={{ color: "var(--text-muted)" }}>Uncapped · {b.rate}mpd</span>
          <span style={{ color: "var(--text-2)", fontVariantNumeric: "tabular-nums" }}>{fmt.miles(m.miles)} mi</span>
        </div>
        <div style={{ height: 6, background: "var(--bg-muted)", borderRadius: 3, overflow: "hidden" }}>
          <div style={{ height: "100%", width: "100%", background: `repeating-linear-gradient(45deg, ${card.color}, ${card.color} 4px, ${card.color}cc 4px, ${card.color}cc 8px)` }}/>
        </div>
      </div>
    );
  }

  const pct = Math.min(100, (spent / b.capSgd) * 100);
  const remaining = Math.max(0, b.capSgd - spent);
  const overflow = Math.max(0, spent - b.capSgd);
  const isOver = spent > b.capSgd;
  const isNear = pct > 80 && pct < 100;

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", fontSize: 12 }}>
        <span style={{ color: "var(--text-muted)" }}>{b.rate}mpd up to {fmt.sgdShort(b.capSgd)}/mo</span>
        <span style={{ color: "var(--text-1)", fontVariantNumeric: "tabular-nums", fontWeight: 600 }}>
          {fmt.sgd(spent, { decimals: 0 })} <span style={{ color: "var(--text-muted)", fontWeight: 400 }}>/ {fmt.sgd(b.capSgd, { decimals: 0 })}</span>
        </span>
      </div>
      <div style={{ position: "relative", height: 10, background: "var(--bg-muted)", borderRadius: 5, overflow: "hidden" }}>
        <div style={{ height: "100%", width: pct + "%", background: isOver ? "var(--danger)" : isNear ? "var(--warning)" : card.color, borderRadius: 5, transition: "width 0.5s cubic-bezier(.2,.8,.2,1)" }}/>
        {[25, 50, 75].map(t => (
          <div key={t} style={{ position: "absolute", left: t + "%", top: 0, bottom: 0, width: 1, background: "var(--bg)", opacity: 0.5 }}/>
        ))}
      </div>
      <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11.5, color: "var(--text-muted)" }}>
        {isOver ? (
          <span style={{ color: "var(--danger)", fontWeight: 500 }}>⚠ {fmt.sgd(overflow, { decimals: 0 })} over cap — earning {b.baseRate}mpd</span>
        ) : isNear ? (
          <span style={{ color: "var(--warning)", fontWeight: 500 }}>⚡ {fmt.sgd(remaining, { decimals: 0 })} left to max bonus</span>
        ) : (
          <span>{fmt.sgd(remaining, { decimals: 0 })} left in bonus tier</span>
        )}
        <span style={{ fontVariantNumeric: "tabular-nums", color: "var(--text-2)" }}>{fmt.miles(m.miles)} mi earned</span>
      </div>
    </div>
  );
}

// Sub-bar for PPV split caps
function SubBar({ label, spent, cap, rate, color }) {
  const pct = Math.min(100, (spent / cap) * 100);
  const isOver = spent > cap;
  return (
    <div>
      <div style={{ display: "flex", justifyContent: "space-between", fontSize: 11.5, marginBottom: 4 }}>
        <span style={{ color: "var(--text-muted)" }}>{label} · {rate}mpd</span>
        <span style={{ color: "var(--text-1)", fontVariantNumeric: "tabular-nums", fontWeight: 600 }}>
          {fmt.sgd(spent, { decimals: 0 })} <span style={{ color: "var(--text-muted)", fontWeight: 400 }}>/ {fmt.sgd(cap, { decimals: 0 })}</span>
        </span>
      </div>
      <div style={{ height: 7, background: "var(--bg-muted)", borderRadius: 4, overflow: "hidden" }}>
        <div style={{ height: "100%", width: pct + "%", background: isOver ? "var(--danger)" : color, borderRadius: 4 }}/>
      </div>
    </div>
  );
}

// ---- Visual credit card ----
function CreditCard({ card, size = "md", showMiles = false, onClick }) {
  const isLg = size === "lg";
  const w = isLg ? 320 : 240;
  const h = isLg ? 200 : 150;
  const m = card.bonus ? computeMiles(card) : { miles: 0, bonusMiles: 0, baseMiles: 0 };
  const isDebit = card.type === "debit";
  return (
    <div
      onClick={onClick}
      style={{
        width: w,
        height: h,
        borderRadius: 14,
        background: `linear-gradient(135deg, ${card.color} 0%, ${card.color}dd 60%, ${card.accent}33 100%)`,
        position: "relative",
        padding: isLg ? 20 : 16,
        color: "#fff",
        boxShadow: "0 4px 14px -4px rgba(0,0,0,0.3), 0 1px 0 rgba(255,255,255,0.06) inset",
        overflow: "hidden",
        cursor: onClick ? "pointer" : "default",
        flexShrink: 0,
        display: "flex",
        flexDirection: "column",
        justifyContent: "space-between",
        fontFamily: "var(--font-sans)",
      }}
    >
      {/* Decorative shapes */}
      <div style={{ position: "absolute", right: -40, top: -40, width: 120, height: 120, borderRadius: "50%", background: `${card.accent}22`, pointerEvents: "none" }}/>
      <div style={{ position: "absolute", right: 30, bottom: -30, width: 80, height: 80, borderRadius: "50%", background: `${card.accent}11`, pointerEvents: "none" }}/>

      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", position: "relative" }}>
        <div>
          <div style={{ fontSize: isLg ? 13 : 11, opacity: 0.7, fontWeight: 500, letterSpacing: "0.02em" }}>{card.network}</div>
          <div style={{ fontSize: isLg ? 16 : 13, fontWeight: 600, marginTop: 2 }}>{card.name}</div>
        </div>
        <CardBrand network={card.network} size={isLg ? 26 : 20}/>
      </div>

      <div style={{ position: "relative" }}>
        {showMiles && !isDebit ? (
          <>
            <div style={{ fontSize: 11, opacity: 0.7 }}>Miles this month</div>
            <div style={{ fontSize: isLg ? 28 : 22, fontWeight: 600, fontVariantNumeric: "tabular-nums", letterSpacing: "-0.01em" }}>
              {fmt.miles(m.miles)}
            </div>
          </>
        ) : (
          <>
            <div style={{ fontFamily: "var(--font-mono)", fontSize: isLg ? 18 : 15, letterSpacing: "0.12em", opacity: 0.95 }}>
              •••• {card.last4}
            </div>
            <div style={{ display: "flex", justifyContent: "space-between", marginTop: 8, fontSize: 11, opacity: 0.7 }}>
              <span>Statement</span>
              <span style={{ fontVariantNumeric: "tabular-nums", fontWeight: 500, opacity: 1 }}>{fmt.sgd(card.statementBalance)}</span>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ---- Transaction row ----
function TxRow({ tx, dense = false }) {
  const [editing, setEditing] = React.useState(false);
  const [showPopover, setShowPopover] = React.useState(false);
  const [draft, setDraft] = React.useState(tx.merchant);
  const [hovered, setHovered] = React.useState(false);
  const [, forceTick] = React.useReducer(x => x + 1, 0);
  const inputRef = React.useRef(null);
  const popoverRef = React.useRef(null);
  const pencilRef = React.useRef(null);

  React.useEffect(() => {
    if (editing && inputRef.current) {
      inputRef.current.focus();
      inputRef.current.select();
    }
  }, [editing]);

  // Close popover on outside click
  React.useEffect(() => {
    if (!showPopover) return;
    const handler = (e) => {
      if (popoverRef.current && !popoverRef.current.contains(e.target)
          && pencilRef.current && !pencilRef.current.contains(e.target)) {
        setShowPopover(false);
      }
    };
    const escHandler = (e) => { if (e.key === "Escape") setShowPopover(false); };
    document.addEventListener("mousedown", handler);
    document.addEventListener("keydown", escHandler);
    return () => {
      document.removeEventListener("mousedown", handler);
      document.removeEventListener("keydown", escHandler);
    };
  }, [showPopover]);

  const save = () => {
    const trimmed = draft.trim();
    if (trimmed && trimmed !== tx.merchant) {
      tx.merchant = trimmed;
      forceTick();
    } else {
      setDraft(tx.merchant);
    }
    setEditing(false);
  };
  const cancel = () => {
    setDraft(tx.merchant);
    setEditing(false);
  };

  // Helper: mutate a field on the tx and force re-render
  const updateField = (key, val) => {
    tx[key] = val;
    forceTick();
  };

  const cat = tx.category ? getCategory(tx.category) : null;
  const card = tx.cardId ? getCard(tx.cardId) : null;
  const fromAcc = tx.fromAccount ? getAccount(tx.fromAccount) : null;
  const toAcc = tx.toAccount ? getAccount(tx.toAccount) : null;
  const acc = tx.account ? getAccount(tx.account) : null;

  let icon, iconBg, iconColor, primary, secondary, amount, amountColor;

  if (tx.type === "expense") {
    icon = cat?.icon || "tag";
    iconBg = cat ? `${cat.color}1a` : "var(--bg-muted)";
    iconColor = cat?.color || "var(--text-2)";
    primary = tx.merchant;
    secondary = (
      <>
        {cat?.name}
        {card && <> · {card.name}</>}
        {!card && tx.paymentMethod && <> · {tx.paymentMethod}</>}
        {tx.recurring && <> · <Icon name="repeat" size={10} style={{ display: "inline-block", verticalAlign: "middle" }}/></>}
        {tx.reimbursable && <> · <span style={{ color: "var(--warning)" }}>Reimbursable</span></>}
      </>
    );
    amount = "−" + fmt.sgd(tx.amount);
    amountColor = "var(--text-1)";
  } else if (tx.type === "income") {
    icon = tx.incomeType === "salary" ? "arrow-down-left" : tx.incomeType === "investment" ? "trending-up" : "users";
    iconBg = "var(--success-soft)";
    iconColor = "var(--success)";
    primary = tx.merchant;
    const incomeTypeLabel = ({
      salary: "Salary",
      reservist: "Reservist",
      investment: "Investment",
      reimbursement: "Reimbursement",
      bonus: "Bonus",
      refund: "Refund",
      gift: "Gift",
      other: "Other income",
    })[tx.incomeType] || "Income";
    secondary = <>
      {incomeTypeLabel}
      {acc && <> · {acc.name}</>}
    </>;
    amount = "+" + fmt.sgd(tx.amount);
    amountColor = "var(--success)";
  } else if (tx.type === "transfer") {
    icon = "swap";
    iconBg = "var(--info-soft)";
    iconColor = "var(--info)";
    primary = `${fromAcc?.name} → ${toAcc?.name}`;
    secondary = <>Transfer{tx.note && <> · {tx.note}</>}{tx.recurring && <> · <Icon name="repeat" size={10} style={{ display: "inline-block", verticalAlign: "middle" }}/></>}</>;
    amount = fmt.sgd(tx.amount);
    amountColor = "var(--text-muted)";
  }

  return (
    <div className="tx-row"
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{
      position: "relative",
      display: "grid",
      gridTemplateColumns: "auto 1fr auto auto",
      gap: 12,
      padding: dense ? "10px 16px" : "14px 16px",
      alignItems: "center",
      borderBottom: "1px solid var(--border-subtle)",
    }}>
      <div style={{
        width: 36, height: 36, borderRadius: 10,
        background: iconBg, color: iconColor,
        display: "grid", placeItems: "center", flexShrink: 0,
      }}>
        <Icon name={icon} size={16}/>
      </div>
      <div style={{ minWidth: 0 }}>
        {editing ? (
          <input
            ref={inputRef}
            value={draft}
            onChange={e => setDraft(e.target.value)}
            onBlur={save}
            onKeyDown={e => {
              if (e.key === "Enter") save();
              if (e.key === "Escape") cancel();
            }}
            style={{
              fontSize: 14, fontWeight: 500, color: "var(--text-1)",
              width: "100%", padding: "2px 6px", margin: "-3px -7px",
              background: "var(--bg-muted)",
              border: "1px solid var(--accent, var(--text-1))",
              borderRadius: 4,
              outline: "none", fontFamily: "inherit",
            }}
          />
        ) : (
          <div
            onClick={(e) => { e.stopPropagation(); setEditing(true); }}
            style={{
              fontSize: 14, fontWeight: 500, color: "var(--text-1)",
              whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
              cursor: "text",
            }}
            title="Click to edit name">
            {tx.merchant}
          </div>
        )}
        <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 2, display: "flex", alignItems: "center", gap: 4 }}>{secondary}</div>
      </div>
      <button
        ref={pencilRef}
        onClick={(e) => { e.stopPropagation(); setShowPopover(s => !s); }}
        title="Edit transaction details"
        style={{
          width: 28, height: 28, borderRadius: 6,
          background: showPopover ? "var(--bg-muted)" : "transparent", border: "none",
          display: "grid", placeItems: "center",
          color: showPopover ? "var(--text-1)" : "var(--text-muted)",
          cursor: "pointer",
          opacity: hovered || showPopover ? 1 : 0,
          transition: "opacity 0.12s, background 0.12s",
        }}
        onMouseEnter={e => { if (!showPopover) e.currentTarget.style.background = "var(--bg-muted)"; }}
        onMouseLeave={e => { if (!showPopover) e.currentTarget.style.background = "transparent"; }}>
        <Icon name="edit" size={13}/>
      </button>
      <div style={{ textAlign: "right" }}>
        <div style={{ fontSize: 14, fontWeight: 600, fontVariantNumeric: "tabular-nums", color: amountColor }}>{amount}</div>
        <div style={{ fontSize: 11, color: "var(--text-muted)", marginTop: 2, fontVariantNumeric: "tabular-nums" }}>{tx.time}</div>
      </div>

      {showPopover && (
        <TxEditPopover
          ref={popoverRef}
          tx={tx}
          onChange={updateField}
          onClose={() => setShowPopover(false)}
        />
      )}
    </div>
  );
}

// ---- Inline edit popover for a transaction ----
const TxEditPopover = React.forwardRef(({ tx, onChange, onClose }, ref) => {
  const { ACCOUNTS, CARDS, CATEGORIES } = window.FIN_DATA;
  const incomeTypes = [
    { id: "salary", label: "Salary" },
    { id: "reservist", label: "Reservist" },
    { id: "bonus", label: "Bonus" },
    { id: "reimbursement", label: "Reimbursement" },
    { id: "investment", label: "Investment / Dividend" },
    { id: "refund", label: "Refund" },
    { id: "gift", label: "Gift" },
    { id: "other", label: "Other" },
  ];

  return (
    <div ref={ref}
      onMouseDown={e => e.stopPropagation()}
      style={{
        position: "absolute", top: "calc(100% - 6px)", right: 16, zIndex: 50,
        width: 280, padding: 12,
        background: "var(--bg)",
        border: "1px solid var(--border)",
        borderRadius: 10,
        boxShadow: "0 8px 28px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.06)",
        display: "flex", flexDirection: "column", gap: 10,
      }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 2 }}>
        <span style={{ fontSize: 11, fontWeight: 600, color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.04em" }}>Edit transaction</span>
        <button onClick={onClose} style={{ background: "none", border: "none", cursor: "pointer", color: "var(--text-muted)", padding: 2, display: "grid", placeItems: "center" }}>
          <Icon name="x" size={14}/>
        </button>
      </div>

      <PopoverField label="Name">
        <input
          defaultValue={tx.merchant}
          onBlur={e => onChange("merchant", e.target.value.trim() || tx.merchant)}
          onKeyDown={e => { if (e.key === "Enter") e.target.blur(); }}
          style={popoverInputStyle}/>
      </PopoverField>

      {tx.type === "income" && (
        <PopoverField label="Type">
          <select
            value={tx.incomeType || "other"}
            onChange={e => onChange("incomeType", e.target.value)}
            style={popoverInputStyle}>
            {incomeTypes.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
          </select>
        </PopoverField>
      )}

      {tx.type === "expense" && (
        <PopoverField label="Category">
          <select
            value={tx.category || ""}
            onChange={e => onChange("category", e.target.value)}
            style={popoverInputStyle}>
            {CATEGORIES.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
          </select>
        </PopoverField>
      )}

      {(tx.type === "income" || (tx.type === "expense" && !tx.cardId)) && (
        <PopoverField label="Account">
          <select
            value={tx.account || ""}
            onChange={e => onChange("account", e.target.value)}
            style={popoverInputStyle}>
            {ACCOUNTS.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
          </select>
        </PopoverField>
      )}

      {tx.type === "expense" && tx.cardId && (
        <PopoverField label="Card">
          <select
            value={tx.cardId}
            onChange={e => onChange("cardId", e.target.value)}
            style={popoverInputStyle}>
            {CARDS.filter(c => c.type === "credit" || c.type === "debit").map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
          </select>
        </PopoverField>
      )}

      <PopoverField label="Note">
        <input
          defaultValue={tx.note || ""}
          placeholder="Optional note…"
          onBlur={e => onChange("note", e.target.value.trim() || undefined)}
          onKeyDown={e => { if (e.key === "Enter") e.target.blur(); }}
          style={popoverInputStyle}/>
      </PopoverField>

      <div style={{ fontSize: 10.5, color: "var(--text-muted)", marginTop: 2 }}>
        Changes save when you tab away or press Enter.
      </div>
    </div>
  );
});

function PopoverField({ label, children }) {
  return (
    <label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
      <span style={{ fontSize: 11, fontWeight: 500, color: "var(--text-muted)" }}>{label}</span>
      {children}
    </label>
  );
}

const popoverInputStyle = {
  fontSize: 13, fontFamily: "inherit", color: "var(--text-1)",
  padding: "6px 8px",
  background: "var(--bg-muted)",
  border: "1px solid var(--border-subtle)",
  borderRadius: 6,
  outline: "none",
  width: "100%",
};

// ---- Section headers ----
function SectionHeader({ title, subtitle, action }) {
  return (
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 12 }}>
      <div>
        <h3 style={{ fontSize: 15, fontWeight: 600, color: "var(--text-1)", margin: 0, letterSpacing: "-0.01em" }}>{title}</h3>
        {subtitle && <div style={{ fontSize: 12, color: "var(--text-muted)", marginTop: 2 }}>{subtitle}</div>}
      </div>
      {action}
    </div>
  );
}

// ---- Stat card ----
function Stat({ label, value, delta, icon, accent }) {
  return (
    <div className="card" style={{ padding: 18, display: "flex", flexDirection: "column", gap: 8 }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
        <span style={{ fontSize: 12, color: "var(--text-muted)", fontWeight: 500 }}>{label}</span>
        {icon && (
          <div style={{ width: 28, height: 28, borderRadius: 8, background: accent ? `${accent}1a` : "var(--bg-muted)", color: accent || "var(--text-2)", display: "grid", placeItems: "center" }}>
            <Icon name={icon} size={14}/>
          </div>
        )}
      </div>
      <div style={{ fontSize: 24, fontWeight: 600, fontVariantNumeric: "tabular-nums", letterSpacing: "-0.02em", color: "var(--text-1)" }}>{value}</div>
      {delta && (
        <div style={{ fontSize: 11.5, color: delta.positive ? "var(--success)" : delta.negative ? "var(--danger)" : "var(--text-muted)", display: "flex", alignItems: "center", gap: 4, fontWeight: 500 }}>
          {delta.positive && <Icon name="arrow-up" size={11}/>}
          {delta.negative && <Icon name="arrow-down" size={11}/>}
          <span>{delta.label}</span>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { Donut, HBar, MonthlyChart, MilesProgressBar, CreditCard, TxRow, SectionHeader, Stat });
