/* Rule of 72 · Compound Growth Calculator — app logic + UI.
   React 18 via Babel standalone (no build step), matching the sister sites. */

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

/* ---------- Icons (inline SVG, stroke style — matches about-me/tfa) -------- */

const Icon = {
  Vault: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <rect x="3" y="4" width="18" height="16" rx="2" /><circle cx="12" cy="12" r="3.4" /><path d="M12 8.6v.01M12 15.4v.01M8.6 12h.01M15.4 12h.01" />
    </svg>
  ),
  Trend: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M3 17l6-6 4 4 8-8" /><path d="M15 7h6v6" />
    </svg>
  ),
  Clock: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="12" cy="12" r="9" /><path d="M12 7v5l3.2 2" />
    </svg>
  ),
  Trophy: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M8 4h8v5a4 4 0 0 1-8 0V4Z" /><path d="M8 5H5a3 3 0 0 0 3 3M16 5h3a3 3 0 0 1-3 3" /><path d="M12 13v3M9 20h6M10 20v-2.2M14 20v-2.2" />
    </svg>
  ),
  Play: (p) => (
    <svg viewBox="0 0 24 24" fill="currentColor" {...p}><path d="M8 5.5v13l11-6.5-11-6.5Z" /></svg>
  ),
  Pause: (p) => (
    <svg viewBox="0 0 24 24" fill="currentColor" {...p}><rect x="7" y="5" width="4" height="14" rx="1" /><rect x="13" y="5" width="4" height="14" rx="1" /></svg>
  ),
  Reset: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v5h5" />
    </svg>
  ),
  Expand: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" />
    </svg>
  ),
  Shrink: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" />
    </svg>
  ),
  Sun: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <circle cx="12" cy="12" r="4.2" /><path d="M12 2.5v2.2M12 19.3v2.2M4.5 12H2.3M21.7 12h-2.2M5.6 5.6l1.6 1.6M16.8 16.8l1.6 1.6M18.4 5.6l-1.6 1.6M7.2 16.8l-1.6 1.6" />
    </svg>
  ),
  Moon: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5Z" />
    </svg>
  ),
  Alert: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M12 3 2 20h20L12 3Z" /><path d="M12 10v4M12 17v.01" />
    </svg>
  ),
  Cross: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M6 6l12 12M18 6 6 18" />
    </svg>
  ),
  Cursor: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M6 3l6 15 2.3-6.2L20.5 9.5 6 3Z" />
    </svg>
  ),
  Chart: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M4 20V10M11 20V4M18 20v-7" />
    </svg>
  ),
  Bulb: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <path d="M9 18h6M10 21h4" /><path d="M12 3a6 6 0 0 0-3.5 10.9c.6.45.9 1.15.9 1.9V17h5.2v-1.2c0-.75.3-1.45.9-1.9A6 6 0 0 0 12 3Z" />
    </svg>
  ),
  Table: (p) => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" {...p}>
      <rect x="3" y="4" width="18" height="16" rx="2" /><path d="M3 10h18M9 4v16" />
    </svg>
  ),
};

/* ---------- Formatting helpers --------------------------------------------- */

function formatCurrency(val) {
  if (Math.abs(val) >= 1000000) return "$" + (val / 1000000).toFixed(2) + "M";
  return "$" + Math.round(val).toLocaleString();
}
function formatCurrencyShort(val) {
  if (Math.abs(val) >= 1000000) return "$" + (val / 1000000).toFixed(2) + "M";
  // Truncate (not round) the "k" case — so Exact Math's real $149,519
  // reads as "$149k", not a rounded-up "$150k" that would look identical
  // to Rule of 72's clean $150,000 estimate.
  if (Math.abs(val) >= 1000) return "$" + Math.floor(val / 1000) + "k";
  return "$" + Math.round(val).toLocaleString();
}

const DECADES = [1, 2, 3, 4];
const DECADE_AGE_RANGE = { 1: "20–30", 2: "30–40", 3: "40–50", 4: "50–60" };

/* ---------- Core math (ported 1:1 from the original model) ----------------- */

function calculateExactSchedule(deposit, rate, activeDecades) {
  const schedule = [];
  let currentBalance = 0;
  let cumPrincipal = 0;
  const r = rate / 100;

  for (let year = 1; year <= 40; year++) {
    const decadeNum = Math.ceil(year / 10);
    const currentDeposit = activeDecades[decadeNum] ? deposit : 0;
    const age = 20 + year;
    const prevBalance = currentBalance;

    currentBalance = (prevBalance + currentDeposit) * (1 + r);
    const interest = currentBalance - prevBalance - currentDeposit;
    cumPrincipal += currentDeposit;

    schedule.push({ year, age, deposit: currentDeposit, principal: cumPrincipal, balance: currentBalance, interest });
  }
  return schedule;
}

function computeStacks(mode, deposit, rate, activeDecades) {
  // The two modes use genuinely different math, not just a different growth
  // curve on the same starting number:
  //
  // "Rule of 72" is the mental-math shortcut end to end — the classic
  // "$10k/yr for 10 years is roughly 15x, i.e. $150k" rule of thumb for one
  // decade's own contributions (calibrated at ~7.2%, so it's a fixed
  // multiple of deposit, not a real compounding calculation), then each
  // later decade doubles per the Rule-of-72 doubling period for the chosen
  // rate. At 7.2% this reproduces the textbook $150k → $300k → $600k →
  // $1.2M progression exactly.
  //
  // "Exact Math" compounds the real contributions precisely: the actual
  // 10-year balance (e.g. $149,519, not a round $150k) grown forward by
  // (1+rate/100)^10 each subsequent decade.
  //
  // Both still respond to the rate slider — Rule of 72's growth multiple
  // changes with rate even though its base decade figure doesn't (that's
  // inherent to the shortcut: it approximates the doubling period, not the
  // exact value of a decade of contributions).
  const growth = mode === "rule72" ? Math.pow(2, rate / 7.2) : Math.pow(1 + rate / 100, 10);

  let base;
  if (mode === "rule72") {
    base = 15 * deposit;
  } else {
    base = 0;
    for (let i = 0; i < 10; i++) base = (base + deposit) * (1 + rate / 100);
  }

  // Unconditional (ignores skip toggles) — decade N's own money grown
  // forward through however many decades remain until Age 60.
  const full = {
    1: [base, base * growth, base * growth ** 2, base * growth ** 3],
    2: [base, base * growth, base * growth ** 2],
    3: [base, base * growth],
    4: [base],
  };

  return {
    d1_1: activeDecades[1] ? full[1][0] : 0,
    d1_2: activeDecades[1] ? full[1][1] : 0,
    d1_3: activeDecades[1] ? full[1][2] : 0,
    d1_4: activeDecades[1] ? full[1][3] : 0,
    d2_1: activeDecades[2] ? full[2][0] : 0,
    d2_2: activeDecades[2] ? full[2][1] : 0,
    d2_3: activeDecades[2] ? full[2][2] : 0,
    d3_1: activeDecades[3] ? full[3][0] : 0,
    d3_2: activeDecades[3] ? full[3][1] : 0,
    d4_1: activeDecades[4] ? full[4][0] : 0,
    fullD1AtEnd: full[1][3],
  };
}

/* ---------- Theme toggle ---------------------------------------------------- */

function useTheme() {
  const [theme, setTheme] = useState(() => document.documentElement.getAttribute("data-theme") || "dark");
  useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]);
  return [theme, () => setTheme((t) => (t === "dark" ? "light" : "dark"))];
}

/* ---------- Header ----------------------------------------------------------- */

function SiteHeader({ theme, onToggleTheme, mode, onSetMode }) {
  return (
    <header className="site-header">
      <div className="brand">
        <div className="brand-mark">72</div>
        <div className="brand-text">
          <div className="brand-title">
            Compound Growth Calculator
            <span className="brand-pill">Rule of 72</span>
          </div>
          <div className="brand-sub">Mental doubling model vs. exact compounding math</div>
        </div>
      </div>
      <div className="header-actions">
        <div className="mode-toggle" role="tablist" aria-label="Calculation mode">
          <button type="button" className={mode === "rule72" ? "is-active" : ""} onClick={() => onSetMode("rule72")}>Rule of 72</button>
          <button type="button" className={mode === "exact" ? "is-active" : ""} onClick={() => onSetMode("exact")}>Exact Math</button>
        </div>
        <button type="button" className="header-btn icon-only" onClick={onToggleTheme} aria-label="Toggle theme">
          {theme === "dark" ? <Icon.Sun /> : <Icon.Moon />}
        </button>
      </div>
    </header>
  );
}

/* ---------- Controls panel ---------------------------------------------------- */

function ControlsPanel({ deposit, setDeposit, rate, setRate, doublingYears, nestEgg, activeCount, principalSaved }) {
  const depositFill = ((deposit - 1000) / (100000 - 1000)) * 100;
  const rateFill = (rate / 15) * 100;
  const doublingLabel = rate > 0 ? (72 / rate).toFixed(1) : "∞";

  return (
    <section className="glass-panel controls-panel">
      <div className="controls-grid">
        <div className="control-field">
          <div className="control-label-row">
            <span className="control-label"><Icon.Vault />Annual Deposit</span>
            <span className="control-value">${deposit.toLocaleString()}</span>
          </div>
          <input type="range" min="1000" max="100000" step="1000" value={deposit}
                 style={{ "--fill": depositFill + "%" }}
                 onChange={(e) => setDeposit(Number(e.target.value))} />
          <div className="range-scale"><span>$1k/yr</span><span>$50k/yr</span><span>$100k/yr</span></div>
        </div>

        <div className="control-field">
          <div className="control-label-row">
            <span className="control-label"><Icon.Trend />Annual Return Rate</span>
            <span className="control-value">{rate.toFixed(1)}%</span>
          </div>
          <input type="range" min="0" max="15" step="0.2" value={rate}
                 style={{ "--fill": rateFill + "%" }}
                 onChange={(e) => setRate(Math.round(Number(e.target.value) * 10) / 10)} />
          <div className="range-scale"><span>0%</span><span>7.5%</span><span>15%</span></div>
        </div>

        <div className="glass-card stat-card">
          <span className="stat-card-label"><Icon.Clock />Doubling Period</span>
          <span className="stat-card-value">{doublingLabel}<span>years / double</span></span>
          <span className="stat-card-foot">72 ÷ {rate.toFixed(1)} = {doublingLabel} yrs</span>
        </div>

        <div className="glass-card stat-card is-highlight">
          <span className="stat-card-label"><Icon.Trophy />Age 60 Nest Egg</span>
          <span className="stat-card-value">{formatCurrency(nestEgg)}</span>
          <span className="stat-card-foot">From {formatCurrencyShort(principalSaved)} principal · {activeCount}/4 decades active</span>
        </div>
      </div>
    </section>
  );
}

/* ---------- Decade stack visualizer ------------------------------------------- */

const STACK_LAYOUT = {
  1: [{ col: 1, key: "d1_1", sub: "Base decade" }, { col: 2, key: "d1_2", sub: "D1 doubled" }, { col: 3, key: "d1_3", sub: "D1 2× doubled" }, { col: 4, key: "d1_4", sub: "D1 3× doubled" }],
  2: [{ col: 2, key: "d2_1", sub: "Base decade" }, { col: 3, key: "d2_2", sub: "D2 doubled" }, { col: 4, key: "d2_3", sub: "D2 2× doubled" }],
  3: [{ col: 3, key: "d3_1", sub: "Base decade" }, { col: 4, key: "d3_2", sub: "D3 doubled" }],
  4: [{ col: 4, key: "d4_1", sub: "Base decade" }],
};

function StacksVisualizer({ deposit, rate, mode, activeDecades, toggleDecade, restoreAll, stacks, focusDecade, setFocusDecade, onAnimateFilter }) {
  const [animStep, setAnimStep] = useState(0); // 0 = idle/all visible, 1-4 = revealing, 5 = done
  const [isPlaying, setIsPlaying] = useState(false);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const intervalRef = useRef(null);
  const panelRef = useRef(null);

  useEffect(() => () => clearInterval(intervalRef.current), []);

  useEffect(() => {
    const handler = () => setIsFullscreen(document.fullscreenElement === panelRef.current);
    document.addEventListener("fullscreenchange", handler);
    return () => document.removeEventListener("fullscreenchange", handler);
  }, []);

  const toggleFullscreen = () => {
    if (document.fullscreenElement) {
      document.exitFullscreen();
    } else if (panelRef.current) {
      panelRef.current.requestFullscreen();
    }
  };

  const playAnimation = () => {
    if (isPlaying) {
      clearInterval(intervalRef.current);
      setIsPlaying(false);
      setAnimStep(0);
      return;
    }
    setIsPlaying(true);
    let step = 1;
    setAnimStep(1);
    intervalRef.current = setInterval(() => {
      step += 1;
      if (step > 4) {
        clearInterval(intervalRef.current);
        setIsPlaying(false);
        setAnimStep(0);
        return;
      }
      setAnimStep(step);
    }, 800);
  };

  const resetAnimation = () => {
    clearInterval(intervalRef.current);
    setIsPlaying(false);
    setAnimStep(0);
    onAnimateFilter("all");
  };

  const skippedDecades = DECADES.filter((d) => !activeDecades[d]);
  const totalPortfolio = stacks.d1_4 + stacks.d2_3 + stacks.d3_2 + stacks.d4_1;

  let alertMsg = "";
  if (skippedDecades.length > 0) {
    if (!activeDecades[1] && activeDecades[2] && activeDecades[3] && activeDecades[4]) {
      alertMsg = `Skipping Age 20–30 saved $100k in deposits, but cost you ${formatCurrency(stacks.fullD1AtEnd)} in compound growth by Age 60!`;
    } else {
      alertMsg = `Currently skipping ${skippedDecades.length} decade(s) (Decade ${skippedDecades.join(", ")}). Compound accumulation reduced!`;
    }
  }

  const decadeTotals = { 1: stacks.d1_4, 2: stacks.d2_3, 3: stacks.d3_2, 4: stacks.d4_1 };
  const pct = (v) => (totalPortfolio > 0 ? ((v / totalPortfolio) * 100).toFixed(1) : "0.0");
  const doublingsLabel = { 1: "3× Doublings", 2: "2× Doublings", 3: "1× Doubling", 4: "Base Decade" };

  return (
    <section className={"glass-panel stacks-panel" + (isFullscreen ? " is-fullscreen" : "")} ref={panelRef}>
      <div className="stacks-header">
        <div>
          <span className="eyebrow">Handwritten Model, Recreated</span>
          <h2 className="stacks-title">Decade Doubling Stacks</h2>
          <p className="stacks-desc">Each 10-year savings block compounds forward every remaining decade to Age 60. Click a block below to test skipping a decade.</p>
        </div>
        <div className="stacks-actions">
          <button type="button" className="btn-primary" onClick={playAnimation}>
            {isPlaying ? <Icon.Pause /> : <Icon.Play />}
            {isPlaying ? "Pause" : "Animate Stacks"}
          </button>
          <button type="button" className="btn-ghost" onClick={resetAnimation}><Icon.Reset />Reset</button>
          <button type="button" className="btn-ghost" onClick={toggleFullscreen} title="Toggle full screen for presenting">
            {isFullscreen ? <Icon.Shrink /> : <Icon.Expand />}
            {isFullscreen ? "Exit Full Screen" : "Full Screen"}
          </button>
        </div>
      </div>

      {alertMsg && (
        <div className="alert-banner">
          <div className="alert-banner-text">
            <span className="alert-icon"><Icon.Alert /></span>
            <div>
              <span className="alert-banner-title">Opportunity Cost Warning</span>
              <span className="alert-banner-body">{alertMsg}</span>
            </div>
          </div>
          <button type="button" className="alert-reset-btn" onClick={restoreAll}>Reset All</button>
        </div>
      )}

      <div className="stacks-board">
        <div className="stacks-board-inner">
          <div className="rate-stamp"><span className="rate-stamp-dot" />{rate.toFixed(1)}%</div>

          <div className="stack-columns">
            {[1, 2, 3, 4].map((col) => {
              const itemsWithOwner = DECADES.flatMap((d) => STACK_LAYOUT[d].filter((s) => s.col === col).map((s) => ({ ...s, owner: d })));
              return (
                <div className="stack-col" key={col}>
                  <div className={"stack-col-content" + (col === 4 ? " is-end" : "")}>
                    {col === 4 && (
                      <div
                        className="stack-total-badge"
                        onClick={() => { setFocusDecade(null); onAnimateFilter("all"); }}
                        title="Total portfolio at Age 60"
                      >
                        <span className="stack-total-label">Total Portfolio</span>
                        <span className="stack-total-value">{formatCurrency(totalPortfolio)}</span>
                      </div>
                    )}
                    {itemsWithOwner.map(({ key, sub, owner }) => {
                      const value = stacks[key];
                      const isActive = activeDecades[owner];
                      const isDimmedByAnim = animStep > 0 && owner > animStep;
                      return (
                        <div
                          key={key}
                          className={"decade-badge" + (!isActive ? " is-disabled" : "") + (focusDecade === owner ? " is-focused" : "")}
                          style={{ "--tone": `var(--d${owner})`, opacity: isDimmedByAnim ? 0.15 : 1 }}
                          onClick={() => { setFocusDecade(owner); onAnimateFilter(owner); }}
                          title={`Decade ${owner} (${DECADE_AGE_RANGE[owner]})`}
                        >
                          {!isActive && <div className="badge-skip-x"><Icon.Cross /></div>}
                          <span className="decade-badge-value">{formatCurrencyShort(value)}</span>
                          <span className="decade-badge-sub">{sub}</span>
                        </div>
                      );
                    })}
                  </div>
                </div>
              );
            })}
          </div>

          <div className="stack-timeline">
            {DECADES.map((d) => (
              <div
                className="stack-timeline-seg"
                key={d}
                style={{ "--tone": activeDecades[d] ? `var(--d${d})` : "150, 150, 160" }}
              />
            ))}
          </div>

          <div className="stack-age-row">
            {DECADES.map((d, idx) => (
              <div className="stack-tick-cell" key={d}>
                {idx === 0 && (
                  <div className="stack-tick stack-tick-start">
                    <span className="stack-tick-line" />
                    <span className="stack-tick-label">Age 20</span>
                  </div>
                )}
                <div className={"stack-tick" + (d === 4 ? " stack-tick-end" : "")}>
                  <span className="stack-tick-line" style={{ background: activeDecades[d] ? `rgb(var(--d${d}))` : "var(--ink-faint)" }} />
                  <span className="stack-tick-label">Age {20 + d * 10}</span>
                </div>
              </div>
            ))}
          </div>

          <div className="axis-row">
            <div className="axis-hint"><Icon.Cursor />Click any decade block below to test skipping savings for that 10-year period</div>
            <div className="toggle-row">
              {DECADES.map((d) => (
                <div
                  key={d}
                  className={"decade-toggle" + (!activeDecades[d] ? " is-off" : "")}
                  style={{ "--tone": `var(--d${d})` }}
                  onClick={() => toggleDecade(d)}
                >
                  {!activeDecades[d] && (
                    <div className="toggle-skip-x"><Icon.Cross />Skipped</div>
                  )}
                  <div className="decade-toggle-label">${(deposit / 1000).toFixed(0)}k/yr (${(10 * deposit / 1000).toFixed(0)}k)</div>
                  <div className="decade-toggle-meta">Principal ${(10 * deposit / 1000).toFixed(0)}k</div>
                  <div className="decade-toggle-hint">Click to skip / enable</div>
                </div>
              ))}
            </div>
          </div>
        </div>
      </div>

      <div className="breakdown-grid">
        {DECADES.map((d) => (
          <div className="glass-card breakdown-card" key={d} style={{ "--tone": `var(--d${d})` }}>
            <div className="breakdown-head">
              <span>Decade {d} (Ages {DECADE_AGE_RANGE[d]})</span>
              <span className="breakdown-tag">{doublingsLabel[d]}</span>
            </div>
            <div className="breakdown-value">{formatCurrency(decadeTotals[d])}</div>
            <div className="breakdown-note">Generates <strong>{pct(decadeTotals[d])}%</strong> of final wealth</div>
          </div>
        ))}
      </div>
    </section>
  );
}

/* ---------- Chart panel ------------------------------------------------------- */

function ChartPanel({ scheduleData, rate }) {
  const canvasRef = useRef(null);
  const chartRef = useRef(null);

  useEffect(() => {
    const styles = getComputedStyle(document.documentElement);
    const accent = styles.getPropertyValue("--accent-deep").trim() || "#E0C28A";
    const muted = styles.getPropertyValue("--ink-faint").trim() || "rgba(237,228,206,0.26)";
    const inkMuted = styles.getPropertyValue("--ink-muted").trim() || "rgba(237,228,206,0.46)";
    const gridColor = "rgba(148,148,148,0.08)";

    const ctx = canvasRef.current.getContext("2d");
    const gradient = ctx.createLinearGradient(0, 0, 0, 280);
    gradient.addColorStop(0, "rgba(224,194,138,0.28)");
    gradient.addColorStop(1, "rgba(224,194,138,0.0)");

    if (chartRef.current) chartRef.current.destroy();
    chartRef.current = new Chart(ctx, {
      type: "line",
      data: {
        labels: scheduleData.map((d) => "Age " + d.age),
        datasets: [
          {
            label: "Total Balance",
            data: scheduleData.map((d) => d.balance),
            borderColor: accent,
            backgroundColor: gradient,
            fill: true,
            tension: 0.35,
            borderWidth: 2.5,
            pointRadius: 0,
            pointHoverRadius: 4,
            pointHoverBackgroundColor: accent,
          },
          {
            label: "Cumulative Principal",
            data: scheduleData.map((d) => d.principal),
            borderColor: muted,
            backgroundColor: "transparent",
            borderDash: [4, 4],
            borderWidth: 1.5,
            pointRadius: 0,
          },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { display: false },
          tooltip: {
            mode: "index",
            intersect: false,
            backgroundColor: "#141922",
            titleFont: { family: "Outfit", size: 12, weight: "600" },
            bodyFont: { family: "JetBrains Mono", size: 11 },
            padding: 10,
            borderColor: "rgba(201,168,118,0.25)",
            borderWidth: 1,
            callbacks: { label: (c) => c.dataset.label + ": $" + Math.round(c.raw).toLocaleString() },
          },
        },
        scales: {
          x: { grid: { color: gridColor }, ticks: { color: inkMuted, font: { family: "Outfit", size: 10 }, maxTicksLimit: 9 } },
          y: {
            grid: { color: gridColor },
            ticks: {
              color: inkMuted,
              font: { family: "JetBrains Mono", size: 10 },
              callback: (v) => (v >= 1000000 ? "$" + (v / 1000000).toFixed(1) + "M" : v >= 1000 ? "$" + (v / 1000).toFixed(0) + "k" : "$" + v),
            },
          },
        },
      },
    });

    return () => { if (chartRef.current) chartRef.current.destroy(); };
  }, [scheduleData]);

  return (
    <div className="glass-panel chart-panel">
      <div className="chart-head">
        <div>
          <div className="chart-title"><Icon.Chart />Compound Portfolio Trajectory</div>
          <div className="chart-sub">Total balance vs. cumulative principal saved</div>
        </div>
        <div className="chart-legend">
          <span><span className="legend-dot" style={{ background: "var(--accent-deep)" }} />Balance</span>
          <span><span className="legend-dot" style={{ background: "var(--ink-faint)" }} />Principal</span>
        </div>
      </div>
      <div className="chart-canvas-wrap"><canvas ref={canvasRef}></canvas></div>
    </div>
  );
}

/* ---------- Insights panel ----------------------------------------------------- */

function InsightsPanel({ stacks, mode, totalPortfolio, totalInterest, deposit }) {
  return (
    <div className="glass-panel insights-panel">
      <div className="insights-title"><Icon.Bulb />Financial Insights</div>
      <div className="insight-list">
        <div className="insight-item">
          <div className="insight-head">1. Early Start Advantage</div>
          <div className="insight-body">Deposits made from <strong>Age 20–30</strong> alone grow to <strong>{formatCurrency(stacks.fullD1AtEnd)}</strong> by Age 60 — the single largest slice of the whole nest egg.</div>
        </div>
        <div className="insight-item">
          <div className="insight-head">2. Mental Model Precision</div>
          <div className="insight-body">At 7.2%, the Rule of 72 shortcut and exact compounding land within a percent or two of each other — a fast way to estimate any doubling timeline in your head.</div>
        </div>
        <div className="insight-item">
          <div className="insight-head">3. The Delay Penalty</div>
          <div className="insight-body">Skip the first decade and the Age-60 total falls from <strong>{formatCurrency(totalPortfolio)}</strong> — waiting costs far more than it saves.</div>
        </div>
      </div>
      <div className="insights-foot">Total Interest Earned: <strong>{formatCurrency(totalInterest)}</strong></div>
    </div>
  );
}

/* ---------- Schedule table ------------------------------------------------------ */

function ScheduleTable({ scheduleData, rate, deposit, filterDecade, setFilterDecade }) {
  const rows = scheduleData.filter((r) => filterDecade === "all" || Math.ceil(r.year / 10) === filterDecade);

  return (
    <section className="glass-panel table-panel">
      <div className="table-head">
        <div>
          <div className="table-title"><Icon.Table />Year-by-Year Compound Schedule</div>
          <div className="table-sub">Exact 40-year breakdown at <span>{rate.toFixed(2)}%</span> annual return, <span>${deposit.toLocaleString()}</span> annual deposits</div>
        </div>
        <div className="filter-badges">
          <button type="button" className={"filter-badge" + (filterDecade === "all" ? " is-active" : "")} onClick={() => setFilterDecade("all")}>All 40 Yrs</button>
          {DECADES.map((d) => (
            <button key={d} type="button" className={"filter-badge" + (filterDecade === d ? " is-active" : "")} onClick={() => setFilterDecade(d)}>
              Yr {(d - 1) * 10 + 1}–{d * 10}
            </button>
          ))}
        </div>
      </div>

      <div className="table-scroll">
        <table className="schedule-table">
          <thead>
            <tr>
              <th className="center">Yr</th>
              <th className="center">Age</th>
              <th className="center">Annual Deposit</th>
              <th className="num">Cumul. Principal</th>
              <th className="num">Balance ({rate.toFixed(2)}%)</th>
              <th className="num">Annual Growth</th>
            </tr>
          </thead>
          <tbody>
            {rows.map((row) => (
              <tr key={row.year} className={row.year % 10 === 0 ? "is-milestone" : ""}>
                <td className="center">{row.year}</td>
                <td className="center">{row.age}</td>
                <td className="center">${row.deposit.toLocaleString()}</td>
                <td className="num">${row.principal.toLocaleString()}</td>
                <td className="num">${Math.round(row.balance).toLocaleString()}</td>
                <td className="num">+${Math.round(row.interest).toLocaleString()}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </section>
  );
}

/* ---------- Footer -------------------------------------------------------------- */

function SiteFooter() {
  return (
    <footer className="site-footer">
      <div className="footer-inner">
        <div className="footer-brand">Rule of 72 · Zhijie Tan</div>
        <p className="footer-disclaimer">
          Educational illustration only — not a guarantee of investment performance. Actual returns fluctuate and may include periods of loss.
          Consult a financial professional before making investment decisions.
        </p>
      </div>
    </footer>
  );
}

/* ---------- App ------------------------------------------------------------------ */

function App() {
  const [theme, toggleTheme] = useTheme();
  const [mode, setMode] = useState("rule72");
  const [deposit, setDeposit] = useState(10000);
  const [rate, setRate] = useState(7.2);
  const [activeDecades, setActiveDecades] = useState({ 1: true, 2: true, 3: true, 4: true });
  const [focusDecade, setFocusDecade] = useState(null);
  const [filterDecade, setFilterDecade] = useState("all");

  const toggleDecade = useCallback((d) => {
    setActiveDecades((prev) => ({ ...prev, [d]: !prev[d] }));
  }, []);
  const restoreAll = useCallback(() => setActiveDecades({ 1: true, 2: true, 3: true, 4: true }), []);

  const scheduleData = useMemo(() => calculateExactSchedule(deposit, rate, activeDecades), [deposit, rate, activeDecades]);
  const stacks = useMemo(() => computeStacks(mode, deposit, rate, activeDecades), [mode, deposit, rate, activeDecades]);

  const totalPortfolio = stacks.d1_4 + stacks.d2_3 + stacks.d3_2 + stacks.d4_1;
  const principalSaved = scheduleData[39].principal;
  const totalInterest = Math.max(0, totalPortfolio - principalSaved);
  const activeCount = DECADES.filter((d) => activeDecades[d]).length;

  const handleAnimateFilter = (decadeOrAll) => setFilterDecade(decadeOrAll);

  return (
    <div className="app-shell">
      <div className="bg-halo" />
      <SiteHeader theme={theme} onToggleTheme={toggleTheme} mode={mode} onSetMode={setMode} />

      <main className="page">
        <div className="hero-block">
          <span className="eyebrow">Compound Interest · Mental Math</span>
          <h1 className="hero-title">See how early money <em>multiplies.</em></h1>
          <p className="hero-sub">
            The Rule of 72 estimates how long it takes an investment to double: divide 72 by the annual return.
            Adjust the deposit and rate below to see exactly where your future nest egg comes from — decade by decade.
          </p>
        </div>

        <ControlsPanel
          deposit={deposit} setDeposit={setDeposit}
          rate={rate} setRate={setRate}
          doublingYears={72 / rate}
          nestEgg={totalPortfolio}
          activeCount={activeCount}
          principalSaved={principalSaved}
        />

        <StacksVisualizer
          deposit={deposit} rate={rate} mode={mode}
          activeDecades={activeDecades} toggleDecade={toggleDecade} restoreAll={restoreAll}
          stacks={stacks}
          focusDecade={focusDecade} setFocusDecade={setFocusDecade}
          onAnimateFilter={handleAnimateFilter}
        />

        <div className="analytics-grid">
          <ChartPanel scheduleData={scheduleData} rate={rate} />
          <InsightsPanel stacks={stacks} mode={mode} totalPortfolio={totalPortfolio} totalInterest={totalInterest} deposit={deposit} />
        </div>

        <ScheduleTable scheduleData={scheduleData} rate={rate} deposit={deposit} filterDecade={filterDecade} setFilterDecade={setFilterDecade} />
      </main>

      <SiteFooter />
    </div>
  );
}

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