/**
 * <folk-mortgage-simulator> — Distributed mortgage simulator web component.
 *
 * Ports the rfunds-online mortgage simulator (React) to a single rSpace web component.
 * Models community-funded mortgages split into 80+ tranches with variable terms,
 * reinvestment loops, secondary markets, and 5 visualization modes.
 *
 * Attributes:
 *   space — space slug (used for URL sharing)
 */

import type {
  MortgageSimulatorConfig,
  MortgageTranche,
  MortgageState,
  LendingTier,
} from '../lib/mortgage-types';
import { DEFAULT_CONFIG } from '../lib/mortgage-types';
import {
  runFullSimulation,
  computeSummary,
  amortizationSchedule,
  calculateBuyerYield,
  calculateLenderReturns,
  calculateAffordability,
} from '../lib/mortgage-engine';
import type { MortgageSummary } from '../lib/mortgage-engine';
import { ViewHistory } from "../../../shared/view-history.js";

type ViewMode = 'mycelial' | 'flow' | 'grid' | 'lender' | 'borrower';

// ─── Helper functions ────────────────────────────────────

function fmt(n: number): string {
  if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`;
  if (n >= 1_000) return `$${(n / 1_000).toFixed(0)}k`;
  return `$${n.toFixed(0)}`;
}

function fmtCurrency(n: number): string {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n);
}

function fmtDetail(n: number): string {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2 }).format(n);
}

function fmtPct(n: number): string {
  return `${n.toFixed(1)}%`;
}

function seededRandom(seed: number) {
  let s = seed;
  return () => {
    s = (s * 16807 + 0) % 2147483647;
    return (s - 1) / 2147483646;
  };
}

// ─── Constants ───────────────────────────────────────────

const TIER_COLORS: Record<string, string> = {
  '2yr': '#06b6d4',
  '5yr': '#10b981',
  '10yr': '#3b82f6',
  '15yr': '#f59e0b',
  '30yr': '#ef4444',
};

const COLORS = {
  borrower: '#0ea5e9',
  principal: '#10b981',
  interest: '#f59e0b',
  community: '#8b5cf6',
  reinvest: '#a855f7',
  text: '#e2e8f0',
  textMuted: '#94a3b8',
  bg: '#0f172a',
  pipe: '#334155',
};

// ─── Component ───────────────────────────────────────────

class FolkMortgageSimulator extends HTMLElement {
  private shadow: ShadowRoot;
  private space = '';

  // State
  private config: MortgageSimulatorConfig = { ...DEFAULT_CONFIG };
  private states: MortgageState[] = [];
  private summary!: MortgageSummary;
  private currentMonth = DEFAULT_CONFIG.startMonth;
  private viewMode: ViewMode = 'mycelial';
  private _history = new ViewHistory<ViewMode>('mycelial', 'rflows');
  private selectedTrancheId: string | null = null;
  private controlsOpen = true;
  private playing = false;
  private speed = 1;
  private playInterval: ReturnType<typeof setInterval> | null = null;
  private advancedMode = false;
  private _listenersAttached = false;
  private _rafId: number | null = null;

  // Calculator state
  private lenderInvestment = DEFAULT_CONFIG.trancheSize;
  private lenderStrategy: 'compare' | 'liquid' | 'reinvest' = 'compare';
  private borrowerBudget = 2000;
  private borrowerDownPayment = DEFAULT_CONFIG.downPaymentPercent;
  private borrowerExpandedIdx: number | null = 0;

  // Grid state
  private gridSortBy: 'index' | 'repaid' | 'rate' | 'remaining' = 'index';
  private gridFilter: 'all' | 'active' | 'repaid' | 'open' | 'for-sale' = 'all';

  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.space = this.getAttribute('space') || 'demo';
    this._loadFromHash();
    this._recompute();
    this._attachListeners();
    this.render();
    window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
  }

  disconnectedCallback() {
    this._history.destroy();
    window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
    this._stopPlayback();
  }

  private _onViewRestored = (e: CustomEvent) => {
    if (e.detail?.moduleId !== 'rflows') return;
    this.viewMode = e.detail.view;
    const tabs = this.shadow.querySelectorAll('.view-tab');
    tabs.forEach((tab: Element) => {
      const t = tab as HTMLElement;
      t.className = `view-tab ${t.dataset.view === this.viewMode ? 'active' : ''}`;
    });
    this._updateView();
  };

  // ─── Core simulation ──────────────────────────────────

  private _recompute() {
    this.states = runFullSimulation(this.config);
    this.summary = computeSummary(this.states);
    if (this.currentMonth > this.maxMonth) {
      this.currentMonth = this.maxMonth;
    }
  }

  private get maxMonth(): number {
    return this.states.length - 1;
  }

  private get currentState(): MortgageState {
    return this.states[Math.min(this.currentMonth, this.maxMonth)];
  }

  // ─── URL hash sharing ─────────────────────────────────

  private _loadFromHash() {
    try {
      const hash = location.hash.slice(1);
      if (!hash) return;
      const LZString = (window as any).LZString;
      if (!LZString) return;
      const json = LZString.decompressFromEncodedURIComponent(hash);
      if (!json) return;
      const parsed = JSON.parse(json);
      this.config = { ...DEFAULT_CONFIG, ...parsed };
      if (parsed.startMonth != null) this.currentMonth = parsed.startMonth;
    } catch { /* ignore invalid hash */ }
  }

  private _saveToHash() {
    try {
      const LZString = (window as any).LZString;
      if (!LZString) return;
      const json = JSON.stringify(this.config);
      location.hash = LZString.compressToEncodedURIComponent(json);
    } catch { /* ignore */ }
  }

  // ─── Playback ─────────────────────────────────────────

  private _startPlayback() {
    this.playing = true;
    this._stopPlayback();
    this.playInterval = setInterval(() => {
      if (this.currentMonth >= this.maxMonth) {
        this._stopPlayback();
        this.playing = false;
        this._updatePlayBtn();
        return;
      }
      this.currentMonth = Math.min(this.currentMonth + this.speed, this.maxMonth);
      this._updateView();
    }, 100);
  }

  private _updatePlayBtn() {
    const btn = this.shadow.querySelector('[data-action="play-toggle"]') as HTMLElement | null;
    if (btn) btn.textContent = this.playing ? '\u23F8' : '\u25B6';
  }

  private _stopPlayback() {
    if (this.playInterval) {
      clearInterval(this.playInterval);
      this.playInterval = null;
    }
  }

  // ─── Config update ────────────────────────────────────

  private _updateConfig(partial: Partial<MortgageSimulatorConfig>) {
    this.config = { ...this.config, ...partial };
    this._recompute();
    this.currentMonth = Math.min(this.config.startMonth, this.maxMonth);
    this.playing = false;
    this._stopPlayback();
    this.selectedTrancheId = null;
    this._saveToHash();
    this.render();
  }

  // ─── Render ───────────────────────────────────────────

  render() {
    const state = this.currentState;
    const numTranches = Math.ceil(
      this.config.propertyValue * (1 - this.config.downPaymentPercent / 100) / this.config.trancheSize
    );

    this.shadow.innerHTML = `
      <style>${this._styles()}</style>
      <div class="root">
        ${this._renderHeader(state, numTranches)}
        <div class="layout">
          ${this._renderControls(numTranches)}
          <main class="main" id="viz">
            ${this._renderActiveView(state)}
            ${this._renderLenderDetail(state)}
          </main>
          <aside class="comparison" id="cmp">
            ${this._renderComparison(state)}
          </aside>
        </div>
      </div>
    `;
  }

  /** Partial update: just viz + comparison + timeline (no controls rebuild) */
  private _updateView() {
    const state = this.currentState;
    const viz = this.shadow.getElementById('viz');
    const cmp = this.shadow.getElementById('cmp');
    if (!viz || !cmp) { this.render(); return; }
    viz.innerHTML = this._renderActiveView(state) + this._renderLenderDetail(state);
    cmp.innerHTML = this._renderComparison(state);
    this._syncTimeline();
  }

  /** Sync timeline slider + label without rebuilding controls */
  private _syncTimeline() {
    const scrub = this.shadow.querySelector('[data-action="month-scrub"]') as HTMLInputElement | null;
    if (scrub && scrub !== this.shadow.activeElement) {
      scrub.value = String(this.currentMonth);
      scrub.max = String(this.maxMonth);
    }
    const label = this.shadow.getElementById('tl-label');
    if (label) {
      const y = Math.floor(this.currentMonth / 12);
      const m = this.currentMonth % 12;
      label.textContent = `Month ${this.currentMonth} (${y}y ${m}m)`;
    }
  }

  // ─── Styles ───────────────────────────────────────────

  private _styles(): string {
    return `
      :host { display: block; font-family: system-ui, -apple-system, sans-serif; }
      * { box-sizing: border-box; margin: 0; padding: 0; }
      .root { min-height: 100vh; background: #0f172a; color: #cbd5e1; }

      /* Header */
      .header { border-bottom: 1px solid #1e293b; padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; max-width: 1600px; margin: 0 auto; }
      .header h1 { font-size: 18px; font-weight: 700; color: #fff; margin: 0; }
      .header .subtitle { font-size: 12px; color: #64748b; margin-top: 2px; }
      .view-tabs { display: flex; gap: 6px; }
      .view-tab { padding: 6px 12px; border-radius: 6px; font-size: 13px; cursor: pointer; border: none; transition: all 0.15s; }
      .view-tab.active { background: #0284c7; color: #fff; }
      .view-tab:not(.active) { background: #1e293b; color: #64748b; }
      .view-tab:not(.active):hover { background: #334155; color: #94a3b8; }
      .mode-toggle { padding: 6px 12px; border-radius: 6px; font-size: 12px; cursor: pointer; border: 1px solid #334155; transition: all 0.15s; margin-left: 8px; background: #1e293b; color: #64748b; font-weight: 600; }
      .mode-toggle:hover { border-color: #475569; color: #94a3b8; }
      .mode-toggle.advanced { background: #7c3aed; color: #fff; border-color: #7c3aed; }

      /* Layout */
      .layout { max-width: 1600px; margin: 0 auto; display: flex; gap: 0; min-height: calc(100vh - 57px); }

      /* Controls sidebar */
      .sidebar { flex-shrink: 0; border-right: 1px solid #1e293b; transition: width 0.3s; overflow: hidden; position: relative; }
      .sidebar.open { width: 256px; }
      .sidebar.closed { width: 40px; }
      .sidebar-toggle { position: absolute; top: 8px; right: 8px; z-index: 10; width: 24px; height: 24px; border-radius: 4px; background: #334155; border: none; color: #94a3b8; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; }
      .sidebar-toggle:hover { background: #475569; }
      .sidebar-content { padding: 16px; overflow-y: auto; height: 100%; }
      .sidebar.closed .sidebar-content { opacity: 0; pointer-events: none; }
      .sidebar-label { writing-mode: vertical-lr; font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; color: #64748b; position: absolute; inset: 0; display: flex; align-items: flex-start; justify-content: center; padding-top: 48px; cursor: pointer; }
      .sidebar-label:hover { color: #94a3b8; }

      /* Section */
      .section { margin-bottom: 20px; }
      .section-title { font-size: 11px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }

      /* Slider */
      .slider-row { margin-bottom: 8px; }
      .slider-header { display: flex; justify-content: space-between; margin-bottom: 4px; }
      .slider-label { font-size: 12px; color: #94a3b8; }
      .slider-value { font-size: 12px; color: #fff; font-family: monospace; }
      input[type="range"] { width: 100%; accent-color: #0ea5e9; height: 4px; }
      .info-text { font-size: 11px; color: #64748b; margin-top: 4px; }

      /* Buttons */
      .btn { padding: 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; border: none; transition: all 0.15s; }
      .btn-active { background: #059669; color: #fff; }
      .btn-inactive { background: #334155; color: #94a3b8; }
      .btn-inactive:hover { background: #475569; }
      .btn-sky { background: #0284c7; color: #fff; }
      .btn-purple { background: #7c3aed; color: #fff; }
      .btn-group { display: flex; gap: 4px; margin-top: 8px; }

      /* Playback */
      .playback { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
      .play-btn { width: 32px; height: 32px; border-radius: 6px; background: #334155; border: none; color: #e2e8f0; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
      .play-btn:hover { background: #475569; }
      .speed-group { display: flex; gap: 4px; margin-left: auto; }
      .timeline-label { font-size: 11px; color: #64748b; text-align: center; margin-top: 4px; }

      /* Main */
      .main { flex: 1; padding: 16px; overflow-y: auto; }

      /* Comparison sidebar */
      .comparison { width: 288px; flex-shrink: 0; border-left: 1px solid #1e293b; padding: 16px; overflow-y: auto; }

      /* SVG */
      svg { width: 100%; }

      /* Tier sliders */
      .tier-row { display: flex; align-items: center; gap: 8px; font-size: 11px; margin-bottom: 6px; }
      .tier-label { width: 40px; font-family: monospace; color: #94a3b8; }
      .tier-rate { color: #64748b; width: 36px; }
      .tier-slider { flex: 1; }
      .tier-alloc { width: 32px; text-align: right; color: #64748b; }

      /* Grid */
      .lender-grid { display: grid; gap: 6px; }
      .grid-cell { position: relative; border-radius: 6px; padding: 6px; text-align: center; cursor: pointer; transition: all 0.15s; border: none; color: inherit; font-family: inherit; }
      .grid-cell:hover { transform: scale(1.02); }
      .grid-cell.selected { outline: 2px solid #38bdf8; transform: scale(1.05); z-index: 10; }
      .grid-cell.active { background: #1e293b; }
      .grid-cell.repaid { background: rgba(6, 78, 59, 0.4); }
      .grid-cell.open { background: rgba(30, 41, 59, 0.4); border: 1px dashed #475569; }
      .grid-cell .fill-bar { position: absolute; inset: 0; border-radius: 6px; opacity: 0.2; transition: all 0.3s; }
      .grid-cell .name { font-size: 11px; font-weight: 500; }
      .grid-cell .amount { font-size: 10px; font-family: monospace; color: #64748b; }
      .grid-cell .pct { font-size: 10px; font-family: monospace; }

      /* Detail panel */
      .detail-panel { background: #1e293b; border-radius: 8px; padding: 16px; border: 1px solid #334155; max-width: 560px; margin-top: 16px; }
      .detail-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
      .detail-close { background: none; border: none; color: #64748b; cursor: pointer; font-size: 18px; }
      .detail-close:hover { color: #fff; }
      .stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; }
      .stat-item .stat-label { font-size: 10px; color: #64748b; text-transform: uppercase; }
      .stat-item .stat-value { font-size: 13px; font-family: monospace; color: #fff; }
      .detail-section { background: #0f172a; border-radius: 6px; padding: 12px; margin-bottom: 12px; }
      .detail-section-title { font-size: 11px; font-weight: 600; color: #64748b; text-transform: uppercase; margin-bottom: 8px; }
      .progress-bar { height: 12px; background: #334155; border-radius: 999px; overflow: hidden; }
      .progress-fill { height: 100%; border-radius: 999px; transition: width 0.3s; }

      /* Comparison panel */
      .compare-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; align-items: center; margin-bottom: 8px; }
      .compare-label { font-size: 11px; color: #64748b; }
      .compare-val { font-size: 11px; font-family: monospace; text-align: center; padding: 4px 8px; border-radius: 4px; }
      .compare-myco { background: #1e293b; color: #fff; }
      .compare-myco.highlight { background: rgba(6, 78, 59, 0.4); color: #34d399; }
      .compare-trad { background: #1e293b; color: #64748b; }
      .stat-card { background: #1e293b; border-radius: 6px; padding: 8px; }
      .stat-card .sc-label { font-size: 10px; color: #64748b; text-transform: uppercase; }
      .stat-card .sc-value { font-size: 13px; font-family: monospace; color: #fff; }
      .community-insight { background: linear-gradient(135deg, rgba(14, 116, 144, 0.2), rgba(16, 185, 129, 0.2)); border-radius: 8px; padding: 16px; border: 1px solid rgba(14, 116, 144, 0.3); }

      /* Calculator views */
      .calc-header { text-align: center; margin-bottom: 24px; }
      .calc-header h2 { font-size: 18px; font-weight: 700; color: #fff; }
      .calc-header p { font-size: 13px; color: #64748b; margin-top: 4px; }
      .calc-input { background: rgba(30, 41, 59, 0.6); border-radius: 8px; padding: 16px; border: 1px solid #334155; }
      .calc-input-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
      .calc-input-label { font-size: 13px; color: #94a3b8; }
      .calc-input-value { font-size: 20px; font-family: monospace; font-weight: 700; color: #fff; }
      .calc-range-labels { display: flex; justify-content: space-between; font-size: 11px; color: #64748b; margin-top: 4px; }
      .inputs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
      .strategy-toggle { display: flex; gap: 8px; justify-content: center; margin-bottom: 24px; }
      .strategy-btn { padding: 8px 16px; border-radius: 8px; font-size: 13px; cursor: pointer; border: none; transition: all 0.15s; }
      .strategy-btn.active { background: #0284c7; color: #fff; }
      .strategy-btn:not(.active) { background: #1e293b; color: #64748b; }
      .strategy-btn:not(.active):hover { background: #334155; }

      /* Scenario cards */
      .scenario-card { background: rgba(30, 41, 59, 0.6); border-radius: 8px; border: 1px solid #334155; overflow: hidden; margin-bottom: 12px; }
      .scenario-card.best { border-color: rgba(16, 185, 129, 0.5); }
      .scenario-card.cheapest { border-color: rgba(245, 158, 11, 0.4); }
      .scenario-summary { display: flex; align-items: center; gap: 16px; padding: 12px 16px; cursor: pointer; width: 100%; border: none; background: none; color: inherit; font-family: inherit; text-align: left; }
      .scenario-summary:hover { background: rgba(51, 65, 85, 0.3); }
      .scenario-detail { border-top: 1px solid rgba(51, 65, 85, 0.5); padding: 12px 16px; }
      .scenario-badges { width: 96px; flex-shrink: 0; }
      .scenario-badges .name { font-size: 13px; font-weight: 700; color: #fff; }
      .badge { font-size: 9px; padding: 2px 4px; border-radius: 2px; text-transform: uppercase; letter-spacing: 0.05em; color: #fff; display: inline-block; margin-top: 2px; }
      .badge-green { background: #059669; }
      .badge-amber { background: #d97706; }
      .badge-best { background: #059669; }
      .scenario-stats { flex: 1; display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
      .expand-icon { color: #64748b; transition: transform 0.2s; }
      .expand-icon.open { transform: rotate(180deg); }

      /* Tier comparison cards */
      .tier-card { background: rgba(30, 41, 59, 0.6); border-radius: 8px; border: 1px solid #334155; overflow: hidden; margin-bottom: 16px; }
      .tier-header { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid rgba(51, 65, 85, 0.5); }
      .tier-columns { display: grid; grid-template-columns: 1fr 1fr; }
      .tier-col { padding: 16px; }
      .tier-col + .tier-col { border-left: 1px solid rgba(51, 65, 85, 0.5); }
      .tier-col-title { font-size: 11px; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 12px; display: flex; align-items: center; gap: 4px; }
      .tier-col-title .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
      .yield-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
      .yield-label { font-size: 11px; color: #64748b; }
      .yield-value { font-family: monospace; font-size: 13px; color: #fff; }
      .yield-value.large { font-size: 16px; font-weight: 700; }
      .yield-value.highlight { color: #34d399; }

      /* Mini stat */
      .mini-stat { background: #1e293b; border-radius: 6px; padding: 8px; text-align: center; }
      .mini-stat .ms-label { font-size: 10px; color: #64748b; text-transform: uppercase; }
      .mini-stat .ms-value { font-size: 13px; font-family: monospace; color: #fff; }

      /* Stacked bar */
      .stacked-bar { height: 8px; background: #334155; border-radius: 999px; overflow: hidden; display: flex; }
      .stacked-bar > div { height: 100%; }

      /* Table */
      .data-table { width: 100%; font-size: 11px; border-collapse: collapse; }
      .data-table th { text-align: left; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; padding: 4px 12px 4px 0; }
      .data-table th:not(:first-child) { text-align: right; }
      .data-table td { padding: 6px 12px 6px 0; border-top: 1px solid rgba(51, 65, 85, 0.3); }
      .data-table td:not(:first-child) { text-align: right; font-family: monospace; }
      .data-table tr.total td { border-top: 1px solid #475569; font-weight: 700; }

      /* Insight box */
      .insight-box { background: linear-gradient(135deg, rgba(14, 116, 144, 0.15), rgba(16, 185, 129, 0.15)); border-radius: 8px; padding: 16px; border: 1px solid rgba(14, 116, 144, 0.3); text-align: center; font-size: 13px; color: #94a3b8; margin-top: 24px; }
      .insight-box.purple { background: linear-gradient(135deg, rgba(124, 58, 237, 0.15), rgba(14, 116, 144, 0.15)); border-color: rgba(124, 58, 237, 0.3); }

      /* Filter/sort bars */
      .filter-bar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
      .filter-bar h3 { font-size: 11px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
      .sort-bar { display: flex; gap: 4px; margin-bottom: 12px; }
      .sort-bar span { font-size: 11px; color: #475569; margin-right: 4px; }

      /* Responsive */
      @media (max-width: 900px) {
        .layout { flex-direction: column; }
        .sidebar { width: 100% !important; border-right: none; border-bottom: 1px solid #1e293b; }
        .comparison { width: 100%; border-left: none; border-top: 1px solid #1e293b; }
        .inputs-grid { grid-template-columns: 1fr; }
      }
    `;
  }

  // ─── Header ───────────────────────────────────────────

  private _renderHeader(state: MortgageState, numTranches: number): string {
    const labels: Record<ViewMode, string> = {
      mycelial: 'Network', flow: 'Flow', grid: 'Grid', lender: 'Lender', borrower: 'Borrower'
    };
    return `
      <header class="header">
        <div>
          <h1>(you)rMortgage</h1>
          <div class="subtitle">${state.tranches.length} lenders × ${fmtCurrency(this.config.trancheSize)} tranches</div>
        </div>
        <div class="view-tabs">
          ${(Object.keys(labels) as ViewMode[]).map(mode => `
            <button class="view-tab ${this.viewMode === mode ? 'active' : ''}" data-action="view" data-view="${mode}">
              ${labels[mode]}
            </button>
          `).join('')}
          <button class="mode-toggle ${this.advancedMode ? 'advanced' : ''}" data-action="toggle-advanced">
            ${this.advancedMode ? 'Advanced' : 'Basic'}
          </button>
        </div>
      </header>
    `;
  }

  // ─── Controls sidebar ─────────────────────────────────

  private _renderControls(numTranches: number): string {
    const c = this.config;
    const totalPrincipal = c.propertyValue * (1 - c.downPaymentPercent / 100);
    const years = Math.floor(this.currentMonth / 12);
    const months = this.currentMonth % 12;

    return `
      <aside class="sidebar ${this.controlsOpen ? 'open' : 'closed'}">
        <button class="sidebar-toggle" data-action="toggle-controls">${this.controlsOpen ? '\u2039' : '\u203A'}</button>
        ${!this.controlsOpen ? `<div class="sidebar-label" data-action="toggle-controls">Loan Settings</div>` : ''}
        <div class="sidebar-content" style="${this.controlsOpen ? '' : 'opacity:0;pointer-events:none'}">

          <div class="section">
            <div class="section-title">Property</div>
            ${this._slider('propertyValue', 'Value', c.propertyValue, 100000, 2000000, 25000, fmtCurrency)}
            ${this._slider('downPaymentPercent', 'Down Payment', c.downPaymentPercent, 0, 50, 5, v => `${v}% (${fmtCurrency(c.propertyValue * v / 100)})`)}
            <div class="info-text">Loan: ${fmtCurrency(totalPrincipal)}</div>
          </div>

          ${this.advancedMode ? `
          <div class="section">
            <div class="section-title">Tranches</div>
            ${this._slider('trancheSize', 'Size', c.trancheSize, 1000, 25000, 1000, fmtCurrency)}
            <div class="info-text">= ${numTranches} lenders</div>
            ${this._slider('fundingPercent', 'Funded', c.fundingPercent, 50, 100, 5, v => `${v}% (${Math.round(numTranches * v / 100)} / ${numTranches})`)}
          </div>

          <div class="section">
            <div class="section-title">Interest</div>
            ${this._slider('interestRate', 'Base Rate', c.interestRate * 100, 1, 12, 0.25, v => `${v.toFixed(2)}%`, true)}
            ${this._slider('rateVariation', 'Rate Spread', c.rateVariation * 100, 0, 3, 0.25, v => v === 0 ? 'None' : `+/- ${v.toFixed(2)}%`, true)}
          </div>

          <div class="section">
            <div class="section-title">Lending Terms</div>
            ${this._slider('termYears', 'Max Term', c.termYears, 5, 30, 5, v => `${v} years`)}
            <div style="margin-top:8px">
              <button class="btn ${c.useVariableTerms ? 'btn-active' : 'btn-inactive'}" data-action="toggle-variable">
                ${c.useVariableTerms ? 'Variable Tiers' : 'Uniform Term'}
              </button>
            </div>
            ${c.useVariableTerms ? `
              <div style="margin-top:8px">
                ${c.lendingTiers.map((tier, i) => `
                  <div class="tier-row">
                    <span class="tier-label">${tier.label}</span>
                    <span class="tier-rate">${(tier.rate * 100).toFixed(1)}%</span>
                    <input type="range" class="tier-slider" min="0" max="50" step="5"
                      value="${tier.allocation * 100}" data-action="tier-alloc" data-tier="${i}"
                      style="accent-color:#10b981">
                    <span class="tier-alloc">${(tier.allocation * 100).toFixed(0)}%</span>
                  </div>
                `).join('')}
              </div>
            ` : ''}
          </div>

          <div class="section">
            <div class="section-title">Overpayment</div>
            ${this._slider('overpayment', 'Extra / mo', c.overpayment, 0, 2000, 50, fmtCurrency)}
            ${c.overpayment > 0 ? `
              <div class="btn-group">
                ${(['extra_principal', 'community_fund', 'split'] as const).map(t => `
                  <button class="btn ${c.overpaymentTarget === t ? 'btn-active' : 'btn-inactive'}" data-action="overpayment-target" data-target="${t}">
                    ${t === 'extra_principal' ? 'Principal' : t === 'community_fund' ? 'Community' : 'Split'}
                  </button>
                `).join('')}
              </div>
            ` : ''}
          </div>

          <div class="section">
            <div class="section-title">Reinvestment</div>
            ${this._slider('reinvestorPercent', 'Reinvestors', c.reinvestorPercent, 0, 80, 10, v => `${v}%`)}
            ${c.reinvestorPercent > 0 ? `
              <div class="info-text">Relend rates:</div>
              <div class="btn-group">
                ${[[0.03, 0.05], [0.02, 0.04], [0.03], [0.05]].map((rates, i) => {
                  const label = rates.map(r => `${(r * 100).toFixed(0)}%`).join(' / ');
                  const isActive = JSON.stringify(c.reinvestmentRates) === JSON.stringify(rates);
                  return `<button class="btn ${isActive ? 'btn-purple' : 'btn-inactive'}" data-action="reinvest-rates" data-rates='${JSON.stringify(rates)}'>${label}</button>`;
                }).join('')}
              </div>
            ` : ''}
          </div>
          ` : ''}

          <div class="section">
            <div class="section-title">Timeline</div>
            <div class="playback">
              <button class="play-btn" data-action="play-toggle">${this.playing ? '\u23F8' : '\u25B6'}</button>
              <button class="btn btn-inactive" data-action="reset-month">Reset</button>
              <div class="speed-group">
                ${[1, 2, 4].map(s => `
                  <button class="btn ${this.speed === s ? 'btn-sky' : 'btn-inactive'}" data-action="speed" data-speed="${s}">${s}x</button>
                `).join('')}
              </div>
            </div>
            <input type="range" min="0" max="${this.maxMonth}" value="${this.currentMonth}" data-action="month-scrub">
            <div class="timeline-label" id="tl-label">Month ${this.currentMonth} (${years}y ${months}m)</div>
          </div>
        </div>
      </aside>
    `;
  }

  private _slider(key: string, label: string, value: number, min: number, max: number, step: number, format: (v: number) => string, isPercent = false): string {
    return `
      <div class="slider-row">
        <div class="slider-header">
          <span class="slider-label">${label}</span>
          <span class="slider-value">${format(value)}</span>
        </div>
        <input type="range" min="${min}" max="${max}" step="${step}" value="${value}" data-action="config-slider" data-key="${key}" ${isPercent ? 'data-percent="true"' : ''}>
      </div>
    `;
  }

  // ─── Active view dispatcher ───────────────────────────

  private _renderActiveView(state: MortgageState): string {
    switch (this.viewMode) {
      case 'mycelial': return this._renderNetworkViz(state);
      case 'flow': return this._renderFlowViz(state);
      case 'grid': return this._renderGrid(state);
      case 'lender': return this._renderLenderCalc();
      case 'borrower': return this._renderBorrowerCalc();
    }
  }

  // ─── Network Viz (Mycelial) ───────────────────────────

  private _renderNetworkViz(state: MortgageState): string {
    const size = 700;
    const layout = this._computeNetworkLayout(state, size);
    const repaidPct = state.totalPrincipal > 0 ? state.totalPrincipalPaid / state.totalPrincipal : 0;

    let svg = `<svg viewBox="0 0 ${size} ${size}" style="min-height:500px;max-height:75vh">`;

    // Defs
    svg += `<defs>
      <radialGradient id="centerGlow" cx="50%" cy="50%" r="50%">
        <stop offset="0%" stop-color="#0ea5e9" stop-opacity="0.3"/>
        <stop offset="70%" stop-color="#0ea5e9" stop-opacity="0.05"/>
        <stop offset="100%" stop-color="#0ea5e9" stop-opacity="0"/>
      </radialGradient>
      <linearGradient id="hyphaActive" x1="0%" y1="0%" x2="100%" y2="0%">
        <stop offset="0%" stop-color="#0ea5e9" stop-opacity="0.6"/>
        <stop offset="100%" stop-color="#10b981" stop-opacity="0.3"/>
      </linearGradient>
      <linearGradient id="hyphaRepaid" x1="0%" y1="0%" x2="100%" y2="0%">
        <stop offset="0%" stop-color="#10b981" stop-opacity="0.3"/>
        <stop offset="100%" stop-color="#10b981" stop-opacity="0.1"/>
      </linearGradient>
      <linearGradient id="hyphaRelend" x1="0%" y1="0%" x2="100%" y2="0%">
        <stop offset="0%" stop-color="#8b5cf6" stop-opacity="0.4"/>
        <stop offset="100%" stop-color="#f59e0b" stop-opacity="0.2"/>
      </linearGradient>
    </defs>`;

    // Background ring guides
    [0.2, 0.35, 0.5].forEach(r => {
      svg += `<circle cx="${layout.cx}" cy="${layout.cy}" r="${size * r}" fill="none" stroke="#1e293b" stroke-width="0.5" stroke-dasharray="4,8"/>`;
    });

    // Relend/staking connections
    layout.relendLinks.forEach((link, i) => {
      const from = layout.nodes[link.from];
      const to = layout.nodes[link.to];
      if (!from || !to) return;
      const isHighlighted = this.selectedTrancheId === from.tranche.id || this.selectedTrancheId === to.tranche.id;
      const path = this._mycelialPath(from.x, from.y, to.x, to.y, i * 7 + 31);
      svg += `<path d="${path}" fill="none" stroke="url(#hyphaRelend)" stroke-width="${isHighlighted ? 2 : 0.8}" opacity="${isHighlighted ? 0.8 : 0.25 + link.strength * 0.3}" stroke-dasharray="${isHighlighted ? 'none' : '3,6'}"/>`;
      if (isHighlighted) {
        svg += `<circle r="2.5" fill="#8b5cf6" opacity="0.7"><animateMotion path="${path}" dur="${2 + link.strength}s" repeatCount="indefinite"/></circle>`;
      }
    });

    // Hyphae: Center → Lenders
    layout.nodes.forEach((node, i) => {
      const t = node.tranche;
      const isSelected = t.id === this.selectedTrancheId;
      const isActive = t.status === 'active';
      const isOpen = !t.funded;
      const flowStrength = isActive && !isOpen ? Math.max(0.15, t.monthlyPayment / (state.monthlyPayment || 1)) : 0;
      const curvature = 0.1 + (i % 5) * 0.06 * (i % 2 === 0 ? 1 : -1);
      const path = this._hyphaPath(layout.cx, layout.cy, node.x, node.y, curvature);

      const stroke = isSelected ? '#38bdf8' : isOpen ? '#334155' : isActive ? 'url(#hyphaActive)' : 'url(#hyphaRepaid)';
      const sw = isSelected ? 2.5 : isOpen ? 0.5 : Math.max(0.5, flowStrength * 3);
      const op = isSelected ? 0.9 : isOpen ? 0.15 : isActive ? 0.4 : 0.15;

      svg += `<path d="${path}" fill="none" stroke="${stroke}" stroke-width="${sw}" opacity="${op}" stroke-dasharray="${isOpen ? '4,6' : 'none'}"/>`;
      if (isActive && isSelected) {
        svg += `<circle r="2" fill="#0ea5e9" opacity="0.9"><animateMotion path="${path}" dur="${1.5 + i * 0.05}s" repeatCount="indefinite"/></circle>`;
      }
    });

    // Lender Nodes
    layout.nodes.forEach(node => {
      const t = node.tranche;
      const isSelected = t.id === this.selectedTrancheId;
      const isActive = t.status === 'active';
      const isOpen = !t.funded;
      const repaidFrac = t.principal > 0 ? t.totalPrincipalPaid / t.principal : 0;
      const baseR = Math.max(6, Math.min(16, 4 + (t.principal / state.trancheSize) * 8));
      const r = isSelected ? baseR + 3 : isOpen ? baseR - 1 : baseR;

      svg += `<g data-action="select-tranche" data-tranche-id="${t.id}" style="cursor:pointer">`;

      // Outer ring
      svg += `<circle cx="${node.x}" cy="${node.y}" r="${r}" fill="${isOpen ? 'none' : isActive ? '#1e293b' : '#064e3b'}" fill-opacity="${isOpen ? 0 : 0.8}" stroke="${isSelected ? '#38bdf8' : isOpen ? '#475569' : isActive ? '#334155' : '#065f46'}" stroke-width="${isSelected ? 2 : 1}" stroke-dasharray="${isOpen ? '3,3' : 'none'}" opacity="${isOpen ? 0.5 : 1}"/>`;

      // Repaid fill
      if (repaidFrac > 0 && repaidFrac < 1) {
        svg += `<clipPath id="clip-${t.id}"><rect x="${node.x - r}" y="${node.y + r - r * 2 * repaidFrac}" width="${r * 2}" height="${r * 2 * repaidFrac}"/></clipPath>`;
      }
      if (repaidFrac > 0) {
        svg += `<circle cx="${node.x}" cy="${node.y}" r="${r - 1}" fill="${isActive ? '#0ea5e9' : '#10b981'}" fill-opacity="0.5" ${repaidFrac < 1 ? `clip-path="url(#clip-${t.id})"` : ''}/>`;
      }

      // Reinvestment glow
      if (t.reinvestmentRate != null && isActive) {
        svg += `<circle cx="${node.x}" cy="${node.y}" r="${r + 3}" fill="none" stroke="#a78bfa" stroke-width="1" opacity="0.6" stroke-dasharray="${t.isReinvested ? 'none' : '3,3'}"/>`;
      }
      if (t.isReinvested) {
        svg += `<circle cx="${node.x}" cy="${node.y}" r="2.5" fill="#8b5cf6" opacity="0.9"/>`;
      }

      // Interest glow
      if (t.totalInterestPaid > t.principal * 0.1 && isActive && !t.reinvestmentRate) {
        svg += `<circle cx="${node.x}" cy="${node.y}" r="${r + 3}" fill="none" stroke="#f59e0b" stroke-width="0.5" opacity="${0.3 + Math.min(0.5, t.totalInterestPaid / t.principal)}" stroke-dasharray="2,3"/>`;
      }

      // For-sale marker
      if (t.listedForSale) {
        svg += `<circle cx="${node.x + r * 0.7}" cy="${node.y - r * 0.7}" r="3" fill="#f59e0b"/>`;
      }

      // Transfer history marker
      if (t.transferHistory.length > 0) {
        svg += `<circle cx="${node.x - r * 0.7}" cy="${node.y - r * 0.7}" r="3" fill="#8b5cf6"/>`;
      }

      // Label on select
      if (isSelected) {
        const hasReinvest = t.reinvestmentRate != null;
        const boxH = hasReinvest ? 44 : 32;
        svg += `<rect x="${node.x - 42}" y="${node.y + r + 4}" width="84" height="${boxH}" rx="4" fill="#0f172a" fill-opacity="0.95" stroke="${t.isReinvested ? '#8b5cf6' : '#334155'}" stroke-width="0.5"/>`;
        svg += `<text x="${node.x}" y="${node.y + r + 17}" text-anchor="middle" fill="${isOpen ? '#64748b' : '#e2e8f0'}" font-size="9" font-weight="600">${isOpen ? 'Open Slot' : t.lender.name}${t.isReinvested ? ' (R)' : ''}</text>`;
        svg += `<text x="${node.x}" y="${node.y + r + 29}" text-anchor="middle" fill="#94a3b8" font-size="8">${fmt(t.principal)} @ ${(t.interestRate * 100).toFixed(1)}%</text>`;
        if (hasReinvest) {
          svg += `<text x="${node.x}" y="${node.y + r + 41}" text-anchor="middle" fill="#a78bfa" font-size="7">reinvests @ ${(t.reinvestmentRate! * 100).toFixed(0)}%${t.reinvestmentPool > 0 ? ` (${fmt(t.reinvestmentPool)} pooled)` : ''}</text>`;
        }
      }

      svg += `</g>`;
    });

    // Center Node
    svg += `<g>`;
    svg += `<circle cx="${layout.cx}" cy="${layout.cy}" r="${size * 0.12}" fill="url(#centerGlow)"/>`;
    svg += `<circle cx="${layout.cx}" cy="${layout.cy}" r="36" fill="none" stroke="#1e293b" stroke-width="4"/>`;
    svg += `<circle cx="${layout.cx}" cy="${layout.cy}" r="36" fill="none" stroke="#10b981" stroke-width="4" stroke-dasharray="${repaidPct * 226.2} ${226.2}" stroke-dashoffset="${226.2 * 0.25}" stroke-linecap="round" opacity="0.8"/>`;
    svg += `<circle cx="${layout.cx}" cy="${layout.cy}" r="30" fill="#0f172a" stroke="#0ea5e9" stroke-width="2"/>`;
    svg += `<path d="M${layout.cx},${layout.cy - 14} l-12,10 h4 v8 h5 v-5 h6 v5 h5 v-8 h4 z" fill="none" stroke="#0ea5e9" stroke-width="1.5" stroke-linejoin="round"/>`;
    svg += `<text x="${layout.cx}" y="${layout.cy + 20}" text-anchor="middle" fill="#e2e8f0" font-size="9" font-weight="600">${fmt(state.totalPrincipal)}</text>`;
    svg += `</g>`;

    // Legend
    svg += `<g transform="translate(16, ${size - 120})">
      <rect width="140" height="112" rx="6" fill="#0f172a" fill-opacity="0.9" stroke="#1e293b" stroke-width="0.5"/>
      <g transform="translate(10, 14)"><circle cx="5" cy="0" r="4" fill="#0ea5e9" fill-opacity="0.5" stroke="#334155"/><text x="14" y="3" fill="#94a3b8" font-size="8">Active lender</text></g>
      <g transform="translate(10, 28)"><circle cx="5" cy="0" r="4" fill="#10b981" fill-opacity="0.5" stroke="#065f46"/><text x="14" y="3" fill="#94a3b8" font-size="8">Repaid lender</text></g>
      <g transform="translate(10, 42)"><circle cx="5" cy="0" r="4" fill="none" stroke="#475569" stroke-width="1" stroke-dasharray="3,3" opacity="0.5"/><text x="14" y="3" fill="#94a3b8" font-size="8">Open slot</text></g>
      <g transform="translate(10, 56)"><circle cx="5" cy="0" r="4" fill="#1e293b" stroke="#a78bfa" stroke-width="1"/><circle cx="5" cy="0" r="2" fill="#8b5cf6"/><text x="14" y="3" fill="#94a3b8" font-size="8">Reinvested tranche</text></g>
      <g transform="translate(10, 70)"><line x1="0" y1="0" x2="10" y2="0" stroke="#8b5cf6" stroke-width="1.5" opacity="0.6"/><text x="14" y="3" fill="#94a3b8" font-size="8">Reinvestment link</text></g>
      <g transform="translate(10, 84)"><line x1="0" y1="0" x2="10" y2="0" stroke="#0ea5e9" stroke-width="1.5" opacity="0.6"/><text x="14" y="3" fill="#94a3b8" font-size="8">Payment flow</text></g>
      <g transform="translate(10, 98)"><circle cx="5" cy="0" r="3" fill="#f59e0b"/><text x="14" y="3" fill="#94a3b8" font-size="8">For sale / transferred</text></g>
    </g>`;

    // Stats overlay
    svg += `<g transform="translate(${size - 146}, ${size - 60})">
      <rect width="130" height="52" rx="6" fill="#0f172a" fill-opacity="0.9" stroke="#1e293b" stroke-width="0.5"/>
      <text x="10" y="16" fill="#94a3b8" font-size="8">Month ${state.currentMonth} — ${state.tranches.length} lenders</text>
      <text x="10" y="30" fill="#10b981" font-size="9" font-weight="600">${(repaidPct * 100).toFixed(1)}% repaid</text>
      <text x="10" y="43" fill="#f59e0b" font-size="8">${fmt(state.totalInterestPaid)} interest earned</text>
    </g>`;

    // Community Fund
    if (state.communityFundBalance > 0) {
      svg += `<g transform="translate(${size - 100}, 16)">
        <rect width="84" height="36" rx="6" fill="#8b5cf6" fill-opacity="0.15" stroke="#8b5cf6" stroke-width="1"/>
        <text x="42" y="15" text-anchor="middle" fill="#8b5cf6" font-size="8" font-weight="600">Community Fund</text>
        <text x="42" y="28" text-anchor="middle" fill="#c4b5fd" font-size="10" font-weight="500">${fmt(state.communityFundBalance)}</text>
      </g>`;
    }

    svg += `</svg>`;
    return svg;
  }

  private _computeNetworkLayout(state: MortgageState, size: number) {
    const cx = size / 2, cy = size / 2;
    const n = state.tranches.length;
    const maxRings = n <= 20 ? 1 : n <= 60 ? 2 : 3;
    const baseRadius = size * 0.2;
    const ringGap = (size * 0.32) / maxRings;
    const rng = seededRandom(42);

    const sorted = [...state.tranches].sort((a, b) => {
      if (a.status !== b.status) return a.status === 'active' ? -1 : 1;
      return b.principalRemaining - a.principalRemaining;
    });

    const perRing = Math.ceil(sorted.length / maxRings);
    const nodes: { x: number; y: number; tranche: MortgageTranche }[] = [];

    sorted.forEach((tranche, i) => {
      const ring = Math.min(Math.floor(i / perRing), maxRings - 1);
      const indexInRing = i - ring * perRing;
      const countInRing = Math.min(perRing, sorted.length - ring * perRing);
      const baseAngle = (indexInRing / countInRing) * Math.PI * 2 - Math.PI / 2;
      const angleJitter = (rng() - 0.5) * 0.15;
      const radiusJitter = (rng() - 0.5) * ringGap * 0.25;
      const angle = baseAngle + angleJitter;
      const radius = baseRadius + ring * ringGap + ringGap * 0.5 + radiusJitter;

      nodes.push({
        x: cx + Math.cos(angle) * radius,
        y: cy + Math.sin(angle) * radius,
        tranche,
      });
    });

    // Relend links
    const relendLinks: { from: number; to: number; strength: number }[] = [];
    for (let i = 0; i < nodes.length; i++) {
      const t = nodes[i].tranche;
      if (!t.isReinvested || !t.parentTrancheId) continue;
      const parentIdx = nodes.findIndex(n => n.tranche.id === t.parentTrancheId);
      if (parentIdx < 0) continue;
      relendLinks.push({ from: parentIdx, to: i, strength: Math.min(1, t.principal / state.trancheSize) });
    }

    // Same-lender links
    const lenderNodes = new Map<string, number[]>();
    nodes.forEach((n, i) => {
      const lid = n.tranche.lender.id;
      if (!lenderNodes.has(lid)) lenderNodes.set(lid, []);
      lenderNodes.get(lid)!.push(i);
    });
    for (const [, indices] of Array.from(lenderNodes)) {
      if (indices.length < 2) continue;
      for (let j = 1; j < indices.length; j++) {
        if (!relendLinks.some(l => (l.from === indices[0] && l.to === indices[j]) || (l.from === indices[j] && l.to === indices[0]))) {
          relendLinks.push({ from: indices[0], to: indices[j], strength: 0.5 });
        }
      }
    }

    return { cx, cy, nodes, relendLinks };
  }

  private _hyphaPath(x1: number, y1: number, x2: number, y2: number, curvature: number): string {
    const dx = x2 - x1, dy = y2 - y1;
    const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
    return `M${x1},${y1} Q${mx - dy * curvature},${my + dx * curvature} ${x2},${y2}`;
  }

  private _mycelialPath(x1: number, y1: number, x2: number, y2: number, seed: number): string {
    const rng = seededRandom(seed);
    const dx = x2 - x1, dy = y2 - y1;
    const dist = Math.sqrt(dx * dx + dy * dy);
    const nx = -dy / dist, ny = dx / dist;
    const wobble = dist * 0.15;
    return `M${x1},${y1} C${x1 + dx * 0.33 + nx * wobble * (rng() - 0.5)},${y1 + dy * 0.33 + ny * wobble * (rng() - 0.5)} ${x1 + dx * 0.66 + nx * wobble * (rng() - 0.5)},${y1 + dy * 0.66 + ny * wobble * (rng() - 0.5)} ${x2},${y2}`;
  }

  // ─── Flow (Sankey) Viz ────────────────────────────────

  private _renderFlowViz(state: MortgageState): string {
    // Group tranches by tier
    const groups = new Map<string, MortgageTranche[]>();
    for (const t of state.tranches) {
      const label = t.tierLabel || 'default';
      if (!groups.has(label)) groups.set(label, []);
      groups.get(label)!.push(t);
    }

    interface TierGroup { label: string; color: string; tranches: MortgageTranche[]; totalPrincipal: number; totalPrincipalRemaining: number; totalMonthlyPayment: number; totalMonthlyPrincipal: number; totalMonthlyInterest: number; activeCount: number; repaidCount: number; reinvestCount: number; reinvestPool: number; }

    const tiers: TierGroup[] = [];
    for (const [label, tranches] of Array.from(groups)) {
      const active = tranches.filter(t => t.status === 'active');
      const repaid = tranches.filter(t => t.status === 'repaid');
      const reinvesting = tranches.filter(t => t.reinvestmentRate != null);
      tiers.push({
        label, color: TIER_COLORS[label] || `hsl(${(tiers.length * 67) % 360}, 60%, 55%)`,
        tranches, totalPrincipal: tranches.reduce((s, t) => s + t.principal, 0),
        totalPrincipalRemaining: tranches.reduce((s, t) => s + t.principalRemaining, 0),
        totalMonthlyPayment: active.reduce((s, t) => s + t.monthlyPayment, 0),
        totalMonthlyPrincipal: active.reduce((s, t) => s + t.monthlyPrincipal, 0),
        totalMonthlyInterest: active.reduce((s, t) => s + t.monthlyInterest, 0),
        activeCount: active.length, repaidCount: repaid.length,
        reinvestCount: reinvesting.length, reinvestPool: reinvesting.reduce((s, t) => s + t.reinvestmentPool, 0),
      });
    }

    tiers.sort((a, b) => {
      const aR = a.label.startsWith('reinvest'), bR = b.label.startsWith('reinvest');
      if (aR !== bR) return aR ? 1 : -1;
      return a.label.localeCompare(b.label, undefined, { numeric: true });
    });

    const totalMonthly = tiers.reduce((s, t) => s + t.totalMonthlyPayment, 0);
    const totalInterest = tiers.reduce((s, t) => s + t.totalMonthlyInterest, 0);
    const totalPrincipalFlow = tiers.reduce((s, t) => s + t.totalMonthlyPrincipal, 0);
    const reinvestTotal = tiers.reduce((s, t) => s + t.reinvestPool, 0);

    const W = 800, H = 520;
    const colBorrower = 60, colSplit = 280, colTiers = 520, colEnd = 740, nodeW = 20, tierGap = 8;
    const borrowerH = Math.max(60, totalMonthly > 0 ? 120 : 60);
    const borrowerY = (H - borrowerH) / 2;
    const maxFlow = Math.max(1, totalMonthly);
    const availH = H - 60;

    const tierNodes = tiers.map((tier, i) => ({
      ...tier, h: Math.max(20, (tier.totalMonthlyPayment / maxFlow) * availH * 0.7), y: 0, idx: i
    }));

    const totalTierH = tierNodes.reduce((s, n) => s + n.h, 0) + (tierNodes.length - 1) * tierGap;
    let tierStartY = (H - totalTierH) / 2;
    for (const node of tierNodes) { node.y = tierStartY; tierStartY += node.h + tierGap; }

    const splitH = borrowerH, splitY = borrowerY;

    const sankeyLink = (x1: number, y1: number, h1: number, x2: number, y2: number, h2: number, color: string) => {
      const mx = (x1 + x2) / 2;
      return `<path d="M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2} L${x2},${y2 + h2} C${mx},${y2 + h2} ${mx},${y1 + h1} ${x1},${y1 + h1} Z" fill="${color}"/>`;
    };

    let svg = `<svg viewBox="0 0 ${W} ${H}" style="min-height:400px">`;

    // Defs
    svg += `<defs>
      <linearGradient id="sankeyBorrower" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="${COLORS.borrower}" stop-opacity="0.8"/><stop offset="100%" stop-color="${COLORS.borrower}" stop-opacity="0.4"/></linearGradient>
      ${tierNodes.map(t => `<linearGradient id="tierGrad-${t.label}" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="${t.color}" stop-opacity="0.6"/><stop offset="100%" stop-color="${t.color}" stop-opacity="0.3"/></linearGradient>`).join('')}
      <linearGradient id="principalGrad" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="${COLORS.principal}" stop-opacity="0.5"/><stop offset="100%" stop-color="${COLORS.principal}" stop-opacity="0.2"/></linearGradient>
      <linearGradient id="interestGrad" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stop-color="${COLORS.interest}" stop-opacity="0.5"/><stop offset="100%" stop-color="${COLORS.interest}" stop-opacity="0.2"/></linearGradient>
    </defs>`;

    // Borrower node
    svg += `<rect x="${colBorrower - nodeW / 2}" y="${borrowerY}" width="${nodeW}" height="${borrowerH}" rx="4" fill="${COLORS.borrower}"/>`;
    svg += `<text x="${colBorrower}" y="${borrowerY - 8}" text-anchor="middle" fill="${COLORS.text}" font-size="12" font-weight="600">Borrower</text>`;
    svg += `<text x="${colBorrower}" y="${borrowerY + borrowerH + 16}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="10">${fmt(totalMonthly + state.overpayment)}/mo</text>`;

    // Split node
    const principalH = totalMonthly > 0 ? splitH * (totalPrincipalFlow / totalMonthly) : splitH / 2;
    svg += `<rect x="${colSplit - nodeW / 2}" y="${splitY}" width="${nodeW}" height="${principalH}" rx="2" fill="${COLORS.principal}"/>`;
    svg += `<rect x="${colSplit - nodeW / 2}" y="${splitY + principalH}" width="${nodeW}" height="${splitH - principalH}" rx="2" fill="${COLORS.interest}"/>`;
    svg += `<text x="${colSplit}" y="${splitY - 8}" text-anchor="middle" fill="${COLORS.text}" font-size="11" font-weight="600">Payment Split</text>`;
    svg += `<text x="${colSplit + nodeW / 2 + 6}" y="${splitY + 14}" text-anchor="start" fill="${COLORS.principal}" font-size="9">Principal ${fmt(totalPrincipalFlow)}</text>`;
    svg += `<text x="${colSplit + nodeW / 2 + 6}" y="${splitY + splitH - 4}" text-anchor="start" fill="${COLORS.interest}" font-size="9">Interest ${fmt(totalInterest)}</text>`;

    // Borrower → Split
    svg += sankeyLink(colBorrower + nodeW / 2, borrowerY, borrowerH, colSplit - nodeW / 2, splitY, splitH, 'url(#sankeyBorrower)');

    // Split → Tiers
    let principalOffset = 0, interestOffset = 0;
    const interestStart = splitY + principalH;
    tierNodes.forEach(tier => {
      if (tier.totalMonthlyPayment === 0) return;
      const pFrac = totalPrincipalFlow > 0 ? tier.totalMonthlyPrincipal / totalPrincipalFlow : 0;
      const iFrac = totalInterest > 0 ? tier.totalMonthlyInterest / totalInterest : 0;
      const pH = principalH * pFrac;
      const iH = (splitH - principalH) * iFrac;
      const pDstFrac = tier.totalMonthlyPayment > 0 ? tier.totalMonthlyPrincipal / tier.totalMonthlyPayment : 0.5;
      const pDstH = tier.h * pDstFrac;

      if (pH > 0.5) svg += sankeyLink(colSplit + nodeW / 2, splitY + principalOffset, pH, colTiers - nodeW / 2, tier.y, pDstH, 'url(#principalGrad)');
      if (iH > 0.5) svg += sankeyLink(colSplit + nodeW / 2, interestStart + interestOffset, iH, colTiers - nodeW / 2, tier.y + pDstH, tier.h - pDstH, 'url(#interestGrad)');
      principalOffset += pH;
      interestOffset += iH;
    });

    // Tier nodes
    tierNodes.forEach(tier => {
      const pFrac = tier.totalMonthlyPayment > 0 ? tier.totalMonthlyPrincipal / tier.totalMonthlyPayment : 0.5;
      const isReinvest = tier.label.startsWith('reinvest');
      const selected = tier.tranches.some(t => t.id === this.selectedTrancheId);

      svg += `<g data-action="select-tier" data-tier-label="${tier.label}" style="cursor:pointer">`;
      svg += `<rect x="${colTiers - nodeW / 2}" y="${tier.y}" width="${nodeW}" height="${tier.h * pFrac}" rx="2" fill="${tier.color}" ${selected ? 'stroke="#fff" stroke-width="2"' : ''}/>`;
      svg += `<rect x="${colTiers - nodeW / 2}" y="${tier.y + tier.h * pFrac}" width="${nodeW}" height="${tier.h * (1 - pFrac)}" rx="2" fill="${tier.color}" opacity="0.5" ${selected ? 'stroke="#fff" stroke-width="2"' : ''}/>`;
      svg += `<text x="${colTiers + nodeW / 2 + 8}" y="${tier.y + tier.h / 2 - 6}" fill="${tier.color}" font-size="11" font-weight="600" dominant-baseline="middle">${isReinvest ? `Reinvest (${tier.label.replace('reinvest@', '')})` : tier.label}</text>`;
      svg += `<text x="${colTiers + nodeW / 2 + 8}" y="${tier.y + tier.h / 2 + 8}" fill="${COLORS.textMuted}" font-size="9" dominant-baseline="middle">${tier.activeCount} active / ${tier.repaidCount} repaid · ${fmt(tier.totalMonthlyPayment)}/mo</text>`;
      if (tier.reinvestPool > 100) {
        svg += `<text x="${colTiers + nodeW / 2 + 8}" y="${tier.y + tier.h / 2 + 22}" fill="${COLORS.reinvest}" font-size="8" dominant-baseline="middle">Pool: ${fmt(tier.reinvestPool)}</text>`;
      }
      svg += `</g>`;

      // Tier → Lender outcome
      if (tier.totalMonthlyPayment > 0) {
        svg += sankeyLink(colTiers + nodeW / 2, tier.y, tier.h, colEnd - 6, tier.y, tier.h, `url(#tierGrad-${tier.label})`);
      }
    });

    // Right-side labels
    svg += `<text x="${colEnd}" y="30" fill="${COLORS.text}" font-size="11" font-weight="600">Lenders</text>`;
    tierNodes.forEach(tier => {
      svg += `<circle cx="${colEnd}" cy="${tier.y + tier.h / 2}" r="3" fill="${tier.color}"/>`;
      svg += `<text x="${colEnd + 8}" y="${tier.y + tier.h / 2}" fill="${COLORS.textMuted}" font-size="9" dominant-baseline="middle">${tier.tranches.length} × ${fmt(tier.totalPrincipal / tier.tranches.length)}</text>`;
    });

    // Reinvestment loop
    if (reinvestTotal > 100) {
      svg += `<g opacity="0.6">
        <defs><marker id="arrowReinvest" viewBox="0 0 6 6" refX="6" refY="3" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="${COLORS.reinvest}"/></marker></defs>
        <path d="M${colEnd - 10},${H / 2 + 40} Q${colEnd + 30},${H / 2 + 80} ${colEnd + 30},${H - 30} Q${colEnd + 30},${H - 10} ${colTiers},${H - 10} Q${colSplit},${H - 10} ${colSplit},${H / 2 + 60}" fill="none" stroke="${COLORS.reinvest}" stroke-width="${Math.max(1.5, Math.min(4, reinvestTotal / 1000))}" stroke-dasharray="6,4" marker-end="url(#arrowReinvest)"/>
        <text x="${(colTiers + colSplit) / 2}" y="${H - 16}" text-anchor="middle" fill="${COLORS.reinvest}" font-size="9">Reinvestment: ${fmt(reinvestTotal)} pooled</text>
      </g>`;
    }

    // Community fund
    if (state.communityFundBalance > 0) {
      svg += `<rect x="10" y="${H - 50}" width="100" height="36" rx="6" fill="${COLORS.community}" fill-opacity="0.15" stroke="${COLORS.community}" stroke-width="1"/>`;
      svg += `<text x="60" y="${H - 36}" text-anchor="middle" fill="${COLORS.community}" font-size="9" font-weight="600">Community Fund</text>`;
      svg += `<text x="60" y="${H - 22}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="9">${fmt(state.communityFundBalance)}</text>`;
    }

    // Summary stats
    svg += `<text x="${W / 2}" y="${H - 8}" text-anchor="middle" fill="${COLORS.textMuted}" font-size="9">Month ${state.currentMonth} · ${state.tranches.length} tranches · ${fmt(state.totalPrincipalRemaining)} remaining · ${((state.totalPrincipalPaid / state.totalPrincipal) * 100).toFixed(1)}% repaid</text>`;

    svg += `</svg>`;
    return svg;
  }

  // ─── Grid View ────────────────────────────────────────

  private _renderGrid(state: MortgageState): string {
    const tranches = state.tranches;
    let filtered = [...tranches];

    switch (this.gridFilter) {
      case 'active': filtered = filtered.filter(t => t.status === 'active' && t.funded); break;
      case 'repaid': filtered = filtered.filter(t => t.status === 'repaid'); break;
      case 'open': filtered = filtered.filter(t => !t.funded); break;
      case 'for-sale': filtered = filtered.filter(t => t.listedForSale); break;
    }
    switch (this.gridSortBy) {
      case 'repaid': filtered.sort((a, b) => (b.totalPrincipalPaid / b.principal) - (a.totalPrincipalPaid / a.principal)); break;
      case 'rate': filtered.sort((a, b) => b.interestRate - a.interestRate); break;
      case 'remaining': filtered.sort((a, b) => a.principalRemaining - b.principalRemaining); break;
    }

    const repaidCount = tranches.filter(t => t.status === 'repaid').length;
    const openCount = tranches.filter(t => !t.funded).length;
    const forSaleCount = tranches.filter(t => t.listedForSale).length;

    const colSize = tranches.length > 50 ? '56px' : '72px';

    let html = `<div>`;

    // Header
    html += `<div class="filter-bar">
      <h3>Lenders (${tranches.length})${repaidCount > 0 ? ` <span style="color:#34d399">/ ${repaidCount} repaid</span>` : ''}</h3>
      <div style="display:flex;gap:4px">
        ${(['all', 'active', 'repaid', 'open', 'for-sale'] as const).map(f => `
          <button class="btn ${this.gridFilter === f ? 'btn-sky' : 'btn-inactive'}" data-action="grid-filter" data-filter="${f}">
            ${f === 'for-sale' ? `sale (${forSaleCount})` : f === 'open' ? `open (${openCount})` : f}
          </button>
        `).join('')}
      </div>
    </div>`;

    // Sort
    html += `<div class="sort-bar">
      <span>Sort:</span>
      ${(['index', 'repaid', 'rate', 'remaining'] as const).map(s => `
        <button class="btn ${this.gridSortBy === s ? 'btn-sky' : 'btn-inactive'}" data-action="grid-sort" data-sort="${s}">
          ${s === 'index' ? '#' : s}
        </button>
      `).join('')}
    </div>`;

    // Grid
    html += `<div class="lender-grid" style="grid-template-columns:repeat(auto-fill, minmax(${colSize}, 1fr))">`;
    filtered.forEach(t => {
      const repaidPct = t.principal > 0 ? (t.totalPrincipalPaid / t.principal) * 100 : 0;
      const isSelected = t.id === this.selectedTrancheId;
      const isRepaid = t.status === 'repaid';
      const isOpen = !t.funded;
      const cls = isSelected ? 'selected' : '';
      const bgCls = isOpen ? 'open' : isRepaid ? 'repaid' : 'active';

      html += `<button class="grid-cell ${cls} ${bgCls}" data-action="select-tranche" data-tranche-id="${t.id}" title="${isOpen ? `Open: ${fmtCurrency(t.principal)} @ ${(t.interestRate * 100).toFixed(1)}% — ${t.tierLabel}` : `${t.lender.name}: ${fmtCurrency(t.principal)} @ ${(t.interestRate * 100).toFixed(1)}%`}">`;

      if (!isOpen) {
        html += `<div class="fill-bar" style="background:${isRepaid ? '#34d399' : '#38bdf8'};clip-path:inset(${100 - repaidPct}% 0 0 0)"></div>`;
      }

      html += `<div style="position:relative">
        <div class="name" style="color:${isOpen ? '#64748b' : isRepaid ? '#6ee7b7' : '#e2e8f0'};${isOpen ? 'font-style:italic' : ''}">${isOpen ? 'Open' : t.lender.name}</div>
        <div class="amount">${fmtCurrency(t.principal)}</div>
        <div class="pct" style="color:${isOpen ? '#0284c7' : isRepaid ? '#34d399' : '#38bdf8'}">${isOpen ? t.tierLabel : isRepaid ? '100%' : `${repaidPct.toFixed(0)}%`}</div>
      </div>`;

      if (t.listedForSale) html += `<div style="position:absolute;top:-2px;right:-2px;width:12px;height:12px;background:#f59e0b;border-radius:50%;font-size:8px;display:flex;align-items:center;justify-content:center">$</div>`;
      if (t.transferHistory.length > 0) html += `<div style="position:absolute;top:-2px;left:-2px;width:12px;height:12px;background:#8b5cf6;border-radius:50%;font-size:8px;display:flex;align-items:center;justify-content:center">${t.transferHistory.length}</div>`;

      html += `</button>`;
    });
    html += `</div></div>`;
    return html;
  }

  // ─── Lender Detail ────────────────────────────────────

  private _renderLenderDetail(state: MortgageState): string {
    if (!this.selectedTrancheId) return '';
    const tranche = state.tranches.find(t => t.id === this.selectedTrancheId);
    if (!tranche) return '';

    const repaidPct = tranche.principal > 0 ? (tranche.totalPrincipalPaid / tranche.principal) * 100 : 0;
    const schedule = amortizationSchedule(tranche.principal, tranche.interestRate, tranche.termMonths);
    const buyerYield = calculateBuyerYield(tranche, tranche.askingPrice ?? tranche.principalRemaining);

    let html = `<div class="detail-panel">`;

    // Header
    html += `<div class="detail-header">
      <div>
        <div style="font-size:18px;font-weight:600;color:#fff">${tranche.lender.name}</div>
        <div style="font-size:11px;color:#64748b;font-family:monospace">${tranche.lender.walletAddress.slice(0, 10)}...</div>
      </div>
      <button class="detail-close" data-action="close-detail">×</button>
    </div>`;

    // Stats
    html += `<div class="stats-grid">
      <div class="stat-item"><div class="stat-label">Tranche</div><div class="stat-value">${fmtCurrency(tranche.principal)}</div></div>
      <div class="stat-item"><div class="stat-label">Rate</div><div class="stat-value">${(tranche.interestRate * 100).toFixed(2)}%</div></div>
      <div class="stat-item"><div class="stat-label">Status</div><div class="stat-value" style="color:${tranche.status === 'repaid' ? '#34d399' : '#38bdf8'}">${tranche.status}</div></div>
      <div class="stat-item"><div class="stat-label">Remaining</div><div class="stat-value">${fmtCurrency(tranche.principalRemaining)}</div></div>
      <div class="stat-item"><div class="stat-label">Interest Earned</div><div class="stat-value">${fmtCurrency(tranche.totalInterestPaid)}</div></div>
      <div class="stat-item"><div class="stat-label">Repaid</div><div class="stat-value">${repaidPct.toFixed(2)}%</div></div>
    </div>`;

    // Progress bar
    html += `<div style="margin-bottom:16px">
      <div style="display:flex;justify-content:space-between;font-size:11px;color:#64748b;margin-bottom:4px">
        <span>Principal Repayment</span><span>${repaidPct.toFixed(2)}%</span>
      </div>
      <div class="progress-bar"><div class="progress-fill" style="width:${Math.min(100, repaidPct)}%;background:${tranche.status === 'repaid' ? '#10b981' : '#0ea5e9'}"></div></div>
    </div>`;

    // This month
    if (tranche.status === 'active') {
      const piRatio = tranche.monthlyPayment > 0 ? (tranche.monthlyPrincipal / tranche.monthlyPayment) * 100 : 50;
      html += `<div class="detail-section">
        <div class="detail-section-title">This Month</div>
        <div style="display:flex;gap:16px">
          <div style="flex:1"><div style="font-size:11px;color:#475569">Payment</div><div style="font-size:13px;font-family:monospace;color:#fff">${fmtCurrency(tranche.monthlyPayment)}</div></div>
          <div style="flex:1"><div style="font-size:11px;color:#475569">Principal</div><div style="font-size:13px;font-family:monospace;color:#34d399">${fmtCurrency(tranche.monthlyPrincipal)}</div></div>
          <div style="flex:1"><div style="font-size:11px;color:#475569">Interest</div><div style="font-size:13px;font-family:monospace;color:#fbbf24">${fmtCurrency(tranche.monthlyInterest)}</div></div>
        </div>
        <div style="display:flex;height:8px;border-radius:999px;overflow:hidden;margin-top:8px">
          <div style="width:${piRatio}%;background:#10b981"></div>
          <div style="flex:1;background:#f59e0b"></div>
        </div>
        <div style="display:flex;justify-content:space-between;font-size:10px;color:#475569;margin-top:2px"><span>Principal</span><span>Interest</span></div>
      </div>`;
    }

    // Secondary market
    html += `<div class="detail-section">
      <div class="detail-section-title">Secondary Market</div>
      ${tranche.status === 'repaid'
        ? `<div style="font-size:11px;color:#475569">Tranche fully repaid — not tradeable</div>`
        : `<button class="btn ${tranche.listedForSale ? 'btn-active' : 'btn-inactive'}" data-action="toggle-sale" data-tranche-id="${tranche.id}">${tranche.listedForSale ? 'Remove Listing' : 'List for Sale'}</button>
          ${tranche.listedForSale ? `<div style="margin-top:8px;font-size:11px;color:#64748b">
            <div>Asking: ${fmtCurrency(tranche.askingPrice ?? tranche.principalRemaining)}</div>
            <div>Buyer yield: ${buyerYield.annualYield.toFixed(2)}%/yr</div>
            <div>Months remaining: ${buyerYield.monthsRemaining}</div>
          </div>` : ''}`
      }
    </div>`;

    // Transfer history
    if (tranche.transferHistory.length > 0) {
      html += `<div class="detail-section">
        <div class="detail-section-title">Transfer History (${tranche.transferHistory.length})</div>
        ${tranche.transferHistory.map(t => `
          <div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:4px">
            <span style="color:#64748b">${t.fromLenderId.replace('lender-', 'L')} → ${t.toLenderId.replace('lender-', 'L')}</span>
            <span style="font-family:monospace;color:${t.premiumPercent >= 0 ? '#34d399' : '#ef4444'}">${fmtCurrency(t.price)} (${t.premiumPercent >= 0 ? '+' : ''}${t.premiumPercent.toFixed(1)}%)</span>
          </div>
        `).join('')}
      </div>`;
    }

    // Mini amortization chart
    if (schedule.length > 0) {
      const maxBalance = schedule[0].balance;
      const maxInterest = schedule[schedule.length - 1].cumulativeInterest;
      const maxVal = Math.max(maxBalance, maxInterest);
      const h = 80, w = 300;
      const step = Math.max(1, Math.floor(schedule.length / 60));
      const points = schedule.filter((_, i) => i % step === 0 || i === schedule.length - 1);

      const balancePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${(p.month / schedule.length) * w},${h - (p.balance / maxVal) * h}`).join(' ');
      const interestPath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${(p.month / schedule.length) * w},${h - (p.cumulativeInterest / maxVal) * h}`).join(' ');
      const currentX = (tranche.monthsElapsed / schedule.length) * w;

      html += `<div class="detail-section">
        <div class="detail-section-title">Amortization</div>
        <svg viewBox="0 0 ${w} ${h + 20}" style="max-height:100px">
          <path d="${balancePath}" fill="none" stroke="#0ea5e9" stroke-width="1.5" opacity="0.8"/>
          <path d="${interestPath}" fill="none" stroke="#f59e0b" stroke-width="1.5" opacity="0.8"/>
          ${tranche.monthsElapsed > 0 ? `<line x1="${currentX}" y1="0" x2="${currentX}" y2="${h}" stroke="#94a3b8" stroke-width="1" stroke-dasharray="3,3"/>` : ''}
          <circle cx="10" cy="${h + 10}" r="3" fill="#0ea5e9"/><text x="18" y="${h + 13}" fill="#94a3b8" font-size="8">Balance</text>
          <circle cx="80" cy="${h + 10}" r="3" fill="#f59e0b"/><text x="88" y="${h + 13}" fill="#94a3b8" font-size="8">Cum. Interest</text>
        </svg>
      </div>`;
    }

    html += `</div>`;
    return html;
  }

  // ─── Lender Calculator ────────────────────────────────

  private _renderLenderCalc(): string {
    const c = this.config;
    const tiers = c.useVariableTerms
      ? c.lendingTiers.map(t => ({ label: t.label, termYears: t.termYears, rate: t.rate }))
      : [{ label: `${c.termYears}yr`, termYears: c.termYears, rate: c.interestRate }];

    const scenarios = calculateLenderReturns(this.lenderInvestment, tiers);
    const bestLiquid = Math.max(...scenarios.map(s => s.liquid.effectiveYield));
    const bestReinvest = Math.max(...scenarios.map(s => s.reinvested.effectiveYield));

    let html = `<div style="max-width:900px;margin:0 auto">`;
    html += `<div class="calc-header"><h2>Lender Return Calculator</h2><p>See what you could earn lending into the rMortgage</p></div>`;

    // Investment amount
    html += `<div class="calc-input" style="margin-bottom:24px">
      <div class="calc-input-header"><span class="calc-input-label">Your Investment</span><span class="calc-input-value">${fmtCurrency(this.lenderInvestment)}</span></div>
      <input type="range" min="1000" max="25000" step="1000" value="${this.lenderInvestment}" data-action="lender-investment">
      <div class="calc-range-labels"><span>$1,000</span><span>$25,000</span></div>
    </div>`;

    // Strategy toggle
    html += `<div class="strategy-toggle">
      ${(['compare', 'liquid', 'reinvest'] as const).map(k => `
        <button class="strategy-btn ${this.lenderStrategy === k ? 'active' : ''}" data-action="lender-strategy" data-strategy="${k}">
          ${k === 'compare' ? 'Compare Both' : k === 'liquid' ? 'Monthly Liquidity' : 'Reinvest to Term'}
        </button>
      `).join('')}
    </div>`;

    if (this.lenderStrategy === 'compare') {
      scenarios.forEach(s => {
        const color = TIER_COLORS[s.tierLabel] || '#64748b';
        const reinvestGain = s.reinvested.totalInterest - s.liquid.totalInterest;
        html += `<div class="tier-card">
          <div class="tier-header" style="border-left:3px solid ${color}">
            <span style="font-size:18px;font-weight:700;color:${color}">${s.tierLabel}</span>
            <span style="font-size:13px;color:#64748b">@ ${(s.rate * 100).toFixed(1)}% APR</span>
            <span style="font-size:11px;color:#475569;margin-left:auto">${s.termYears} year commitment</span>
          </div>
          <div class="tier-columns">
            <div class="tier-col">
              <div class="tier-col-title"><span class="dot" style="background:#22d3ee"></span>Monthly Liquidity</div>
              <div class="yield-row"><span class="yield-label">Monthly income</span><span class="yield-value">${fmtDetail(s.liquid.monthlyPayment)}</span></div>
              <div class="yield-row"><span class="yield-label">Total interest</span><span class="yield-value${s.liquid.effectiveYield === bestLiquid ? ' highlight' : ''}">${fmtCurrency(s.liquid.totalInterest)}</span></div>
              <div class="yield-row"><span class="yield-label">Total received</span><span class="yield-value">${fmtCurrency(s.liquid.totalReturn)}</span></div>
              <div style="border-top:1px solid rgba(51,65,85,0.5);padding-top:8px;margin-top:8px">
                <div class="yield-row"><span class="yield-label">Effective yield</span><span class="yield-value large${s.liquid.effectiveYield === bestLiquid ? ' highlight' : ''}">${s.liquid.effectiveYield.toFixed(2)}%/yr</span></div>
              </div>
            </div>
            <div class="tier-col">
              <div class="tier-col-title"><span class="dot" style="background:#a78bfa"></span>Reinvest to Term</div>
              <div class="yield-row"><span class="yield-label">Final value</span><span class="yield-value">${fmtCurrency(s.reinvested.finalValue)}</span></div>
              <div class="yield-row"><span class="yield-label">Total interest</span><span class="yield-value${s.reinvested.effectiveYield === bestReinvest ? ' highlight' : ''}">${fmtCurrency(s.reinvested.totalInterest)}</span></div>
              <div class="yield-row"><span class="yield-label">Extra vs liquid</span><span class="yield-value" style="color:${reinvestGain > 0 ? '#34d399' : '#64748b'}">${reinvestGain > 0 ? '+' : ''}${fmtCurrency(reinvestGain)}</span></div>
              <div style="border-top:1px solid rgba(51,65,85,0.5);padding-top:8px;margin-top:8px">
                <div class="yield-row"><span class="yield-label">Effective yield</span><span class="yield-value large${s.reinvested.effectiveYield === bestReinvest ? ' highlight' : ''}">${s.reinvested.effectiveYield.toFixed(2)}%/yr</span></div>
              </div>
            </div>
          </div>
        </div>`;
      });
    } else {
      // Single strategy card view
      scenarios.forEach(s => {
        const color = TIER_COLORS[s.tierLabel] || '#64748b';
        const data = this.lenderStrategy === 'liquid' ? s.liquid : s.reinvested;
        const isBest = this.lenderStrategy === 'liquid' ? s.liquid.effectiveYield === bestLiquid : s.reinvested.effectiveYield === bestReinvest;

        html += `<div class="scenario-card${isBest ? ' best' : ''}" style="border-left:3px solid ${color};padding:16px;margin-bottom:12px">
          <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
            <div style="display:flex;align-items:center;gap:8px">
              <span style="font-size:18px;font-weight:700;color:${color}">${s.tierLabel}</span>
              <span style="font-size:13px;color:#64748b">@ ${(s.rate * 100).toFixed(1)}%</span>
              ${isBest ? '<span class="badge badge-best">Best</span>' : ''}
            </div>
            <div style="text-align:right">
              <div style="font-size:20px;font-family:monospace;font-weight:700;color:#fff">${data.effectiveYield.toFixed(2)}%</div>
              <div style="font-size:11px;color:#475569">effective yield/yr</div>
            </div>
          </div>
          <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px">
            ${this.lenderStrategy === 'liquid' ? `
              <div class="mini-stat"><div class="ms-label">Monthly</div><div class="ms-value">${fmtDetail(s.liquid.monthlyPayment)}</div></div>
              <div class="mini-stat"><div class="ms-label">Total Interest</div><div class="ms-value" style="color:#fbbf24">${fmtCurrency(s.liquid.totalInterest)}</div></div>
              <div class="mini-stat"><div class="ms-label">Total Received</div><div class="ms-value" style="color:#34d399">${fmtCurrency(s.liquid.totalReturn)}</div></div>
            ` : `
              <div class="mini-stat"><div class="ms-label">Final Value</div><div class="ms-value" style="color:#34d399">${fmtCurrency(s.reinvested.finalValue)}</div></div>
              <div class="mini-stat"><div class="ms-label">Total Interest</div><div class="ms-value" style="color:#fbbf24">${fmtCurrency(s.reinvested.totalInterest)}</div></div>
              <div class="mini-stat"><div class="ms-label">Growth</div><div class="ms-value" style="color:#a78bfa">${((s.reinvested.totalReturn / this.lenderInvestment - 1) * 100).toFixed(1)}%</div></div>
            `}
          </div>
          <div class="stacked-bar" style="margin-top:12px">
            <div style="width:${(this.lenderInvestment / data.totalReturn) * 100}%;background:#0ea5e9;border-radius:999px 0 0 999px"></div>
            <div style="width:${(data.totalInterest / data.totalReturn) * 100}%;background:#f59e0b"></div>
          </div>
          <div style="display:flex;justify-content:space-between;font-size:10px;color:#475569;margin-top:4px"><span>Principal</span><span>Interest</span></div>
        </div>`;
      });
    }

    // Insight
    const best = scenarios.reduce((a, b) => b.reinvested.effectiveYield > a.reinvested.effectiveYield ? b : a);
    const liquidBest = scenarios.reduce((a, b) => b.liquid.effectiveYield > a.liquid.effectiveYield ? b : a);
    html += `<div class="insight-box purple">
      Reinvesting returns in a <strong style="color:#c4b5fd">${best.tierLabel}</strong> tranche yields
      <strong style="color:#34d399">${best.reinvested.effectiveYield.toFixed(2)}%/yr</strong> vs
      <strong style="color:#22d3ee">${liquidBest.liquid.effectiveYield.toFixed(2)}%/yr</strong> with monthly liquidity.
      ${best.reinvested.totalInterest > liquidBest.liquid.totalInterest
        ? ` That's <strong style="color:#fff">${fmtCurrency(best.reinvested.totalInterest - liquidBest.liquid.totalInterest)}</strong> more over the term.`
        : ''}
    </div>`;

    html += `</div>`;
    return html;
  }

  // ─── Borrower Calculator ──────────────────────────────

  private _renderBorrowerCalc(): string {
    const c = this.config;
    const tiers = c.useVariableTerms
      ? c.lendingTiers
      : [{ label: `${c.termYears}yr`, termYears: c.termYears, rate: c.interestRate, allocation: 1 }];

    const scenarios = calculateAffordability(this.borrowerBudget, this.borrowerDownPayment, tiers);
    const bestLoan = Math.max(...scenarios.map(s => s.maxLoan));
    const leastInterest = Math.min(...scenarios.map(s => s.totalInterest));

    let html = `<div style="max-width:900px;margin:0 auto">`;
    html += `<div class="calc-header"><h2>What Can I Afford?</h2><p>Enter what you can pay monthly — see how tier mix affects your borrowing power</p></div>`;

    // Inputs
    html += `<div class="inputs-grid">
      <div class="calc-input">
        <div class="calc-input-header"><span class="calc-input-label">Monthly Budget</span><span class="calc-input-value">${fmtCurrency(this.borrowerBudget)}</span></div>
        <input type="range" min="500" max="10000" step="100" value="${this.borrowerBudget}" data-action="borrower-budget">
        <div class="calc-range-labels"><span>$500</span><span>$10,000</span></div>
      </div>
      <div class="calc-input">
        <div class="calc-input-header"><span class="calc-input-label">Down Payment</span><span class="calc-input-value">${this.borrowerDownPayment}%</span></div>
        <input type="range" min="0" max="50" step="5" value="${this.borrowerDownPayment}" data-action="borrower-down" style="accent-color:#10b981">
        <div class="calc-range-labels"><span>0%</span><span>50%</span></div>
      </div>
    </div>`;

    // Scenarios
    scenarios.forEach((s, idx) => {
      const isBest = s.maxLoan === bestLoan;
      const isCheapest = s.totalInterest === leastInterest;
      const isExpanded = this.borrowerExpandedIdx === idx;

      html += `<div class="scenario-card${isBest ? ' best' : isCheapest ? ' cheapest' : ''}">
        <button class="scenario-summary" data-action="borrower-expand" data-idx="${idx}">
          <div class="scenario-badges">
            <div class="name">${s.label}</div>
            <div>${isBest ? '<span class="badge badge-green">Max $</span> ' : ''}${isCheapest ? '<span class="badge badge-amber">Low Int</span>' : ''}</div>
          </div>
          <div class="scenario-stats">
            <div><div style="font-size:10px;color:#475569;text-transform:uppercase">You Can Borrow</div><div style="font-size:13px;font-family:monospace;color:#34d399;font-weight:700">${fmtCurrency(s.maxLoan)}</div></div>
            <div><div style="font-size:10px;color:#475569;text-transform:uppercase">Property Value</div><div style="font-size:13px;font-family:monospace;color:#fff">${fmtCurrency(s.propertyValue)}</div></div>
            <div><div style="font-size:10px;color:#475569;text-transform:uppercase">Total Interest</div><div style="font-size:13px;font-family:monospace;color:#fbbf24">${fmtCurrency(s.totalInterest)}</div></div>
            <div><div style="font-size:10px;color:#475569;text-transform:uppercase">Payoff</div><div style="font-size:13px;font-family:monospace;color:#38bdf8">${s.payoffYears}yr</div></div>
          </div>
          <span class="expand-icon${isExpanded ? ' open' : ''}">▾</span>
        </button>`;

      if (isExpanded) {
        html += `<div class="scenario-detail">
          <div style="font-size:11px;color:#64748b;margin-bottom:12px">${s.description}</div>

          <div style="margin-bottom:16px">
            <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
              <span style="font-size:11px;color:#475569">Borrowing power</span>
              <span style="font-size:11px;color:#64748b;margin-left:auto">${fmtCurrency(s.maxLoan)} / ${fmtCurrency(bestLoan)} max</span>
            </div>
            <div class="progress-bar" style="height:12px"><div class="progress-fill" style="width:${(s.maxLoan / bestLoan) * 100}%;background:linear-gradient(90deg,#0ea5e9,#10b981)"></div></div>
          </div>

          <div style="margin-bottom:16px">
            <div style="font-size:11px;color:#475569;margin-bottom:4px">Tier allocation of your ${fmtCurrency(this.borrowerBudget)}/mo</div>
            <div style="height:24px;background:#334155;border-radius:999px;overflow:hidden;display:flex">
              ${s.breakdown.map((b, i) => {
                const frac = b.monthlyPayment / this.borrowerBudget;
                const colors = ['#06b6d4', '#10b981', '#3b82f6', '#f59e0b', '#ef4444'];
                return `<div style="width:${frac * 100}%;height:100%;background:${colors[i % colors.length]};display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;color:rgba(255,255,255,0.8)" title="${b.tierLabel}: ${fmtCurrency(b.monthlyPayment)}/mo → ${fmtCurrency(b.principal)}">${frac > 0.08 ? b.tierLabel : ''}</div>`;
              }).join('')}
            </div>
          </div>

          <table class="data-table">
            <thead><tr><th>Tier</th><th>Rate</th><th>Monthly</th><th>Principal</th><th>Interest</th><th>Term</th></tr></thead>
            <tbody>
              ${s.breakdown.map(b => `<tr>
                <td style="font-weight:500;color:#fff">${b.tierLabel}</td>
                <td style="color:#94a3b8">${(b.rate * 100).toFixed(1)}%</td>
                <td style="color:#38bdf8">${fmtCurrency(b.monthlyPayment)}</td>
                <td style="color:#34d399">${fmtCurrency(b.principal)}</td>
                <td style="color:#fbbf24">${fmtCurrency(b.totalInterest)}</td>
                <td style="color:#64748b">${b.termYears}yr</td>
              </tr>`).join('')}
              <tr class="total">
                <td style="color:#fff">Total</td><td></td>
                <td style="color:#fff">${fmtCurrency(this.borrowerBudget)}</td>
                <td style="color:#34d399">${fmtCurrency(s.maxLoan)}</td>
                <td style="color:#fbbf24">${fmtCurrency(s.totalInterest)}</td>
                <td style="color:#64748b">${s.payoffYears}yr</td>
              </tr>
            </tbody>
          </table>

          <div style="margin-top:12px">
            <div style="font-size:10px;color:#475569;margin-bottom:4px">Total cost breakdown</div>
            <div class="stacked-bar">
              <div style="width:${(s.maxLoan / s.totalPaid) * 100}%;background:#10b981"></div>
              <div style="width:${(s.totalInterest / s.totalPaid) * 100}%;background:#f59e0b"></div>
            </div>
            <div style="display:flex;justify-content:space-between;font-size:10px;color:#475569;margin-top:4px">
              <span>Principal ${fmtCurrency(s.maxLoan)}</span>
              <span>Interest ${fmtCurrency(s.totalInterest)}</span>
              ${this.borrowerDownPayment > 0 ? `<span>Down ${fmtCurrency(s.propertyValue - s.maxLoan)}</span>` : ''}
            </div>
          </div>
        </div>`;
      }

      html += `</div>`;
    });

    // Insight
    const best = scenarios.find(s => s.maxLoan === bestLoan);
    const cheapest = scenarios.find(s => s.totalInterest === leastInterest);
    if (best && cheapest) {
      const diff = best.maxLoan - cheapest.maxLoan;
      const savedInterest = best.totalInterest - cheapest.totalInterest;
      html += `<div class="insight-box">
        <strong style="color:#34d399">${best.label}</strong> lets you borrow the most (${fmtCurrency(best.maxLoan)})${
        diff > 0 && cheapest.label !== best.label
          ? `, but <strong style="color:#fbbf24">${cheapest.label}</strong> saves you <strong style="color:#fff">${fmtCurrency(savedInterest)}</strong> in interest${cheapest.payoffYears < best.payoffYears ? ` and pays off ${best.payoffYears - cheapest.payoffYears} years sooner` : ''}`
          : ''
        }. With rMortgage, your interest stays in the community either way.
      </div>`;
    }

    html += `</div>`;
    return html;
  }

  // ─── Comparison Panel ─────────────────────────────────

  private _renderComparison(state: MortgageState): string {
    const s = this.summary;
    const repaidPct = state.totalPrincipal > 0 ? (state.totalPrincipalPaid / state.totalPrincipal) * 100 : 0;

    let html = `<div style="display:flex;flex-direction:column;gap:16px;font-size:13px">`;

    // Progress
    html += `<div>
      <div class="section-title">Progress</div>
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
        <div class="stat-card"><div class="sc-label">Principal Repaid</div><div class="sc-value">${fmtCurrency(state.totalPrincipalPaid)} <span style="font-size:11px;color:#64748b">${fmtPct(repaidPct)}</span></div></div>
        <div class="stat-card"><div class="sc-label">Remaining</div><div class="sc-value">${fmtCurrency(state.totalPrincipalRemaining)}</div></div>
        <div class="stat-card"><div class="sc-label">Interest Paid</div><div class="sc-value" style="color:#fbbf24">${fmtCurrency(state.totalInterestPaid)}</div></div>
        <div class="stat-card"><div class="sc-label">Tranches Done</div><div class="sc-value" style="color:#34d399">${state.tranchesRepaid} / ${state.tranches.length}</div></div>
      </div>
      <div style="margin-top:8px"><div class="progress-bar" style="height:8px"><div class="progress-fill" style="width:${Math.min(100, repaidPct)}%;background:linear-gradient(90deg,#0ea5e9,#10b981)"></div></div></div>
    </div>`;

    // Community Fund
    if (state.communityFundBalance > 0) {
      html += `<div>
        <div class="section-title">Community Fund</div>
        <div style="background:rgba(16,185,129,0.15);border-radius:6px;padding:12px;border:1px solid rgba(16,185,129,0.3)">
          <div style="font-size:18px;font-family:monospace;color:#34d399">${fmtCurrency(state.communityFundBalance)}</div>
          <div style="font-size:11px;color:#64748b;margin-top:4px">Overflow directed to community resilience</div>
        </div>
      </div>`;
    }

    // Comparison
    html += `<div>
      <div class="section-title">rMortgage vs Traditional</div>
      <div style="display:grid;grid-template-columns:auto 1fr 1fr;gap:4px 8px;margin-bottom:4px">
        <div></div><div style="font-size:10px;color:#475569;text-align:center;text-transform:uppercase">Myco</div><div style="font-size:10px;color:#475569;text-align:center;text-transform:uppercase">Trad</div>
      </div>
      ${this._compareRow('Monthly', fmtCurrency(s.mycoMonthlyPayment), fmtCurrency(s.tradMonthlyPayment), false)}
      ${this._compareRow('Total Interest', fmtCurrency(s.mycoTotalInterest), fmtCurrency(s.tradTotalInterest), s.interestSaved > 0)}
      ${this._compareRow('Payoff', `${Math.ceil(s.mycoPayoffMonths / 12)}y`, `${Math.ceil(s.tradPayoffMonths / 12)}y`, s.monthsSaved > 0)}
      ${this._compareRow('Avg Yield', fmtPct(s.avgLenderYield), 'N/A (bank)', false)}
    </div>`;

    // Key insight
    html += `<div class="community-insight">
      <div style="font-size:11px;font-weight:600;color:#7dd3fc;text-transform:uppercase;margin-bottom:8px">Community Wealth</div>
      <div style="font-size:24px;font-family:monospace;color:#34d399;margin-bottom:4px">${fmtCurrency(s.communityRetained)}</div>
      <div style="font-size:11px;color:#64748b">Interest that stays in the community instead of flowing to a distant institution.${s.interestSaved > 0 ? ` Plus ${fmtCurrency(s.interestSaved)} saved through distributed rates.` : ''}</div>
    </div>`;

    html += `</div>`;
    return html;
  }

  private _compareRow(label: string, myco: string, trad: string, highlight: boolean): string {
    return `<div class="compare-row">
      <div class="compare-label">${label}</div>
      <div class="compare-val compare-myco${highlight ? ' highlight' : ''}">${myco}</div>
      <div class="compare-val compare-trad">${trad}</div>
    </div>`;
  }

  // ─── Event handling ───────────────────────────────────

  private _attachListeners() {
    if (this._listenersAttached) return;
    this._listenersAttached = true;

    this.shadow.addEventListener('click', (e: Event) => {
      const target = (e.target as HTMLElement).closest('[data-action]') as HTMLElement | null;
      if (!target) return;
      const action = target.dataset.action;

      switch (action) {
        case 'view': {
          this._history.push(target.dataset.view as ViewMode);
          this.viewMode = target.dataset.view as ViewMode;
          // Update tab highlights in-place
          const tabs = this.shadow.querySelectorAll('.view-tab');
          tabs.forEach((tab: Element) => {
            const t = tab as HTMLElement;
            t.className = `view-tab ${t.dataset.view === this.viewMode ? 'active' : ''}`;
          });
          this._updateView();
          break;
        }

        case 'toggle-advanced':
          this.advancedMode = !this.advancedMode;
          this.render();
          break;

        case 'toggle-controls':
          this.controlsOpen = !this.controlsOpen;
          this.render();
          break;

        case 'play-toggle':
          if (this.playing) { this._stopPlayback(); this.playing = false; }
          else this._startPlayback();
          this._updatePlayBtn();
          break;

        case 'reset-month':
          this.currentMonth = 0;
          this._stopPlayback();
          this.playing = false;
          this._updatePlayBtn();
          this._updateView();
          break;

        case 'speed': {
          this.speed = Number(target.dataset.speed);
          // Update speed button highlights
          const btns = this.shadow.querySelectorAll('[data-action="speed"]');
          btns.forEach((b: Element) => {
            const s = Number((b as HTMLElement).dataset.speed);
            (b as HTMLElement).className = `btn ${this.speed === s ? 'btn-sky' : 'btn-inactive'}`;
          });
          break;
        }

        case 'toggle-variable':
          this._updateConfig({ useVariableTerms: !this.config.useVariableTerms });
          break;

        case 'overpayment-target':
          this._updateConfig({ overpaymentTarget: target.dataset.target as any });
          break;

        case 'reinvest-rates':
          this._updateConfig({ reinvestmentRates: JSON.parse(target.dataset.rates!) });
          break;

        case 'select-tranche': {
          const id = target.dataset.trancheId!;
          this.selectedTrancheId = this.selectedTrancheId === id ? null : id;
          this._updateView();
          break;
        }

        case 'select-tier': {
          const tierLabel = target.dataset.tierLabel!;
          const state = this.currentState;
          const tierTranches = state.tranches.filter(t => t.tierLabel === tierLabel);
          const first = tierTranches.find(t => t.status === 'active') ?? tierTranches[0];
          if (first) {
            this.selectedTrancheId = this.selectedTrancheId === first.id ? null : first.id;
            this._updateView();
          }
          break;
        }

        case 'close-detail':
          this.selectedTrancheId = null;
          this._updateView();
          break;

        case 'toggle-sale':
          this._updateView();
          break;

        case 'grid-filter':
          this.gridFilter = target.dataset.filter as any;
          this._updateView();
          break;

        case 'grid-sort':
          this.gridSortBy = target.dataset.sort as any;
          this._updateView();
          break;

        case 'lender-strategy':
          this.lenderStrategy = target.dataset.strategy as any;
          this._updateView();
          break;

        case 'borrower-expand': {
          const idx = Number(target.dataset.idx);
          this.borrowerExpandedIdx = this.borrowerExpandedIdx === idx ? null : idx;
          this._updateView();
          break;
        }
      }
    });

    // Range inputs — debounced to avoid full DOM rebuilds during drag
    this.shadow.addEventListener('input', (e: Event) => {
      const target = e.target as HTMLInputElement;
      if (!target.dataset.action) return;
      const action = target.dataset.action;
      const val = Number(target.value);

      switch (action) {
        case 'config-slider': {
          const key = target.dataset.key as keyof MortgageSimulatorConfig;
          const isPercent = target.dataset.percent === 'true';
          this.config = { ...this.config, [key]: isPercent ? val / 100 : val } as any;
          // Update the value label next to the slider immediately
          const valEl = target.closest('.slider-row')?.querySelector('.slider-value') as HTMLElement;
          if (valEl) {
            // Re-derive display from current config value
            const v = isPercent ? val : val;
            if (key === 'propertyValue') valEl.textContent = fmtCurrency(val);
            else if (key === 'downPaymentPercent') valEl.textContent = `${val}% (${fmtCurrency(this.config.propertyValue * val / 100)})`;
            else if (key === 'interestRate') valEl.textContent = `${val.toFixed(2)}%`;
            else if (key === 'rateVariation') valEl.textContent = val === 0 ? 'None' : `+/- ${val.toFixed(2)}%`;
            else if (key === 'termYears') valEl.textContent = `${val} years`;
            else if (key === 'overpayment') valEl.textContent = fmtCurrency(val);
            else if (key === 'reinvestorPercent') valEl.textContent = `${val}%`;
            else if (key === 'trancheSize') valEl.textContent = fmtCurrency(val);
            else if (key === 'fundingPercent') {
              const nt = Math.ceil(this.config.propertyValue * (1 - this.config.downPaymentPercent / 100) / this.config.trancheSize);
              valEl.textContent = `${val}% (${Math.round(nt * val / 100)} / ${nt})`;
            }
          }
          // Debounce the expensive recompute + view update
          if (this._rafId) cancelAnimationFrame(this._rafId);
          this._rafId = requestAnimationFrame(() => {
            this._recompute();
            this.currentMonth = Math.min(this.config.startMonth, this.maxMonth);
            this._saveToHash();
            this._updateView();
            this._rafId = null;
          });
          break;
        }
        case 'tier-alloc': {
          const tierIdx = Number(target.dataset.tier);
          const newTiers = [...this.config.lendingTiers];
          newTiers[tierIdx] = { ...newTiers[tierIdx], allocation: val / 100 };
          this.config = { ...this.config, lendingTiers: newTiers };
          // Update alloc label
          const allocEl = target.closest('.tier-row')?.querySelector('.tier-alloc') as HTMLElement;
          if (allocEl) allocEl.textContent = `${val.toFixed(0)}%`;
          if (this._rafId) cancelAnimationFrame(this._rafId);
          this._rafId = requestAnimationFrame(() => {
            this._recompute();
            this._saveToHash();
            this._updateView();
            this._rafId = null;
          });
          break;
        }
        case 'month-scrub':
          this.currentMonth = val;
          this._updateView();
          break;
        case 'lender-investment':
          this.lenderInvestment = val;
          this._updateView();
          break;
        case 'borrower-budget':
          this.borrowerBudget = val;
          this._updateView();
          break;
        case 'borrower-down':
          this.borrowerDownPayment = val;
          this._updateView();
          break;
      }
    });
  }
}

customElements.define('folk-mortgage-simulator', FolkMortgageSimulator);
