/* Folclore Latin Fund I — Return model
   Fund I base case: five houses entered over six years and sold between
   years 8 and 12, one of them failing. Fee formula from the five-investment
   research model; cash flows, preferred return and the European waterfall
   are timed year by year. */

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

const RM_LP = 147;
const RM_GP = 3;
const RM_FUND = 150;
const RM_PREF = 0.08;
const RM_CARRY = 0.2;
const RM_TARGET_NET = 2.8;
const RM_SPX = 0.1;
const RM_MAX_HOUSES = 6;
const RM_PLATFORM_LIMIT = 30;
const RM_EPS = 1e-6;
const RM_WORDS = ["no", "one", "two", "three", "four", "five", "six"];

/* The base case put forward to investors. Years count from the first year
   of the fund; cheques in $M. A house may sell in tranches, each with its
   own multiple on the share of cost it carries. */
const RM_BASE_HOUSES = [
  {
    name: "Swimwear house",
    entry: 1,
    cheque: 25,
    followOn: 4,
    followOnYear: 3,
    exits: [
      { year: 8, share: 0.4, moic: 3.78 },
      { year: 12, share: 0.6, moic: 8.23 },
    ],
  },
  {
    name: "Leather goods house (bags)",
    entry: 2,
    cheque: 18,
    followOn: 4,
    followOnYear: 4,
    exits: [{ year: 9, share: 1, moic: 4 }],
  },
  {
    name: "Jewelry house",
    entry: 3,
    cheque: 21,
    followOn: 4,
    followOnYear: 5,
    exits: [{ year: 9, share: 1, moic: 4 }],
  },
  {
    name: "Leather goods house (shoes)",
    entry: 4,
    cheque: 18,
    followOn: 3,
    followOnYear: 6,
    exits: [{ year: 11, share: 1, moic: 3 }],
  },
  {
    name: "Fashion house",
    entry: 3,
    cheque: 18,
    followOn: 0,
    followOnYear: 3,
    exits: [{ year: 8, share: 1, moic: 0.9 }],
  },
];
const RM_SPARE_NAMES = [
  "Beauty house",
  "Spirits house",
  "Home goods house",
  "Eyewear house",
  "Wine house",
  "Accessories house",
];
const RM_SPLIT_EARLY_SHARE = 0.4;
const RM_RECYCLE_DEFAULT = { on: true, moic: 3 };
const RM_CANON_EXITS = RM_BASE_HOUSES.map((h) =>
  h.exits.map((e) => ({ year: e.year, share: e.share, moic: e.moic }))
);

const RM_PRESETS = [
  {
    key: "down",
    name: "Downside",
    note: "The swimwear house sells once, in year 10, at 3×. Bags and jewelry return 2× in year 9, shoes 0.9× in year 11, and the fashion house recovers half its cost in year 8.",
    m: [3, 2, 2, 0.9, 0.5, 1],
    exits: [
      [{ year: 10, share: 1, moic: 3 }],
      [{ year: 9, share: 1, moic: 2 }],
      [{ year: 9, share: 1, moic: 2 }],
      [{ year: 11, share: 1, moic: 0.9 }],
      [{ year: 8, share: 1, moic: 0.5 }],
    ],
  },
  {
    key: "base",
    name: "Base case",
    note: "Four houses sold between 3× and 6.5× — the swimwear house in two tranches, to a strategic — and one failure at 0.9×.",
    m: [0.4 * 3.78 + 0.6 * 8.23, 4.0, 4.0, 3.0, 0.9, 2.0],
    exits: RM_CANON_EXITS,
  },
  {
    key: "up",
    name: "Upside",
    note: "The swimwear house reaches 10× and the three other survivors clear 4×; the failure still fails.",
    m: [10.0, 5.0, 5.0, 4.0, 0.9, 3.0],
    exits: RM_CANON_EXITS,
  },
];

const RM_NAV = [
  ["top", "↑", "Fund terms"],
  ["01", "01", "Where the capital goes"],
  ["02", "02", "The houses"],
  ["03", "03", "Key assumptions"],
  ["04", "04", "The waterfall"],
  ["05", "05", "Reading the picture"],
];
const RM_LAST_SEC = "05";

/* ---------- Formatting ---------- */
function rmFm(x, d = 1) {
  const n = Number(x);
  if (!Number.isFinite(n)) return "—";
  const sign = n < -RM_EPS ? "−" : "";
  return sign + "$" + Math.abs(n).toFixed(d) + "M";
}
function rmFx(x, d = 2) {
  return Number(x).toFixed(d) + "×";
}
function rmFxs(x) {
  const n = Number(x);
  const one = Math.round(n * 10) / 10;
  return (Math.abs(n - one) < 0.005 ? one.toFixed(1) : n.toFixed(2)) + "×";
}
function rmFp(x, d = 1) {
  return (x * 100).toFixed(d) + "%";
}
function rmCode(i) {
  return String(i + 1).padStart(2, "0");
}
function rmDash(v) {
  return v > RM_EPS ? rmFm(v) : "—";
}
function rmClamp(v, lo, hi) {
  return Math.max(lo, Math.min(hi, v));
}
function rmList(items) {
  if (items.length <= 1) return items.join("");
  return items.slice(0, -1).join(", ") + " and " + items[items.length - 1];
}
function rmLower(name) {
  return name.replace(/^./, (m) => m.toLowerCase());
}

/* ---------- Fund shape ---------- */
function rmShape(term) {
  const ext = term === 14 ? 2 : 0;
  const expenses = 1.5 + 0.4 * term + 1.5;
  const lpExp = (expenses * RM_LP) / RM_FUND;
  const denom = 1 + 0.015 * 7 + 0.01 * ext;
  const L = (RM_LP - 14.7 - lpExp) / denom;
  const G = RM_GP - (expenses * RM_GP) / RM_FUND;
  const invested = L + G;
  const fees = 14.7 + 0.105 * L + 0.01 * ext * L;
  return { fees, expenses, invested, L, G, lpShare: L / invested, ext };
}

function rmFeeInYear(y, s) {
  if (y <= 5) return 0.02 * RM_LP;
  if (y <= 12) return 0.015 * s.L;
  return 0.01 * s.L;
}
function rmExpInYear(y, term) {
  return 0.4 + (y === 1 ? 1.5 : 0) + (y === term ? 1.5 : 0);
}

/* Grow LP calls and distributions to the final year at the S&P 500's
   long-run total return, so the two can be compared on one clock. */
function rmSpxTerminal(rows, rate) {
  const T = rows.length ? rows[rows.length - 1].year : 0;
  let fvCalls = 0;
  let fvDists = 0;
  rows.forEach((r) => {
    const g = Math.pow(1 + rate, T - r.year);
    fvCalls += r.lpCall * g;
    fvDists += r.lpDist * g;
  });
  return {
    spxFvCalls: fvCalls,
    spxFvDists: fvDists,
    spxRatio: fvCalls > RM_EPS ? fvDists / fvCalls : 0,
  };
}

function rmSolveIrr(cfs) {
  let pos = false;
  let neg = false;
  cfs.forEach((v) => {
    if (v > RM_EPS) pos = true;
    if (v < -RM_EPS) neg = true;
  });
  if (!pos || !neg) return 0;
  const npv = (r) => cfs.reduce((s, v, t) => s + v / Math.pow(1 + r, t), 0);
  let lo = -0.99;
  let hi = 3;
  for (let i = 0; i < 100; i++) {
    const mid = (lo + hi) / 2;
    if (npv(mid) > 0) lo = mid;
    else hi = mid;
  }
  return (lo + hi) / 2;
}

/* ---------- Houses ---------- */
function rmCost(h) {
  return h.cheque + (h.followOn || 0);
}
function rmProceeds(h) {
  const c = rmCost(h);
  return h.exits.reduce((a, e) => a + c * e.share * e.moic, 0);
}
function rmMoic(h) {
  const c = rmCost(h);
  return c > RM_EPS ? rmProceeds(h) / c : 0;
}
function rmLastExit(h) {
  return h.exits.reduce((a, e) => Math.max(a, e.year), 0);
}
function rmLastCall(h) {
  return h.followOn > 0 ? Math.max(h.entry, h.followOnYear) : h.entry;
}
function rmWithMoic(h, m) {
  const cur = rmMoic(h);
  if (cur > RM_EPS) {
    const k = m / cur;
    return { ...h, exits: h.exits.map((e) => ({ ...e, moic: e.moic * k })) };
  }
  return { ...h, exits: h.exits.map((e) => ({ ...e, moic: m })) };
}

/* A preset may name its exits. The multiple in m[] is applied afterwards,
   so the base and upside keep the same sale years and the same split. */
function rmPresetExitsMatch(h, exits) {
  if (!exits) return true;
  if (h.exits.length !== exits.length) return false;
  return exits.every((e, i) => {
    const cur = h.exits[i];
    return cur.year === e.year && Math.abs(cur.share - e.share) < 0.001;
  });
}
function rmPresetMatches(ps, houses) {
  return houses.every((h, i) => {
    if (ps.m[i] != null && Math.abs(rmMoic(h) - ps.m[i]) >= 0.01) return false;
    return rmPresetExitsMatch(h, ps.exits && ps.exits[i]);
  });
}
function rmApplyPresetHouses(houses, preset, term) {
  return houses.map((h, i) => {
    const exits = preset.exits && preset.exits[i];
    let next = h;
    if (exits) {
      next = {
        ...h,
        exits: exits.map((e) => ({
          year: e.year,
          share: e.share,
          moic: e.moic != null ? e.moic : 1,
        })),
      };
    }
    if (preset.m[i] != null) next = rmWithMoic(next, preset.m[i]);
    return exits ? rmNormalize(next, term) : next;
  });
}
function rmNormalize(h, term) {
  const entry = rmClamp(Math.round(h.entry) || 1, 1, term - 1);
  const followOn = Math.max(0, Number(h.followOn) || 0);
  const followOnYear = rmClamp(
    Math.round(h.followOnYear) || entry,
    entry,
    term - 1
  );
  const minExit = (followOn > 0 ? followOnYear : entry) + 1;
  const exits = h.exits.map((e) => ({
    ...e,
    year: rmClamp(Math.round(e.year) || minExit, minExit, term),
  }));
  for (let i = 1; i < exits.length; i++) {
    if (exits[i].year < exits[i - 1].year) exits[i].year = exits[i - 1].year;
  }
  return { ...h, entry, followOn, followOnYear, exits };
}
function rmFreshHouses() {
  return RM_BASE_HOUSES.map((h, i) => ({
    id: i + 1,
    ...h,
    exits: h.exits.map((e) => ({ ...e })),
  }));
}

/* Sell a house in two tranches (40% early, 60% late at the same multiple),
   or merge two tranches back into a single exit at the blended multiple. */
function rmToggleSplit(h, term) {
  if (h.exits.length > 1) {
    return { ...h, exits: [{ year: rmLastExit(h), share: 1, moic: rmMoic(h) }] };
  }
  const e = h.exits[0];
  const late = Math.min(term, e.year + 4);
  const early =
    late > e.year ? e.year : Math.max(rmLastCall(h) + 1, late - 4);
  return rmNormalize(
    {
      ...h,
      exits: [
        { year: early, share: RM_SPLIT_EARLY_SHARE, moic: e.moic },
        { year: late, share: 1 - RM_SPLIT_EARLY_SHARE, moic: e.moic },
      ],
    },
    term
  );
}

/* Scale cheques to the investable capital of a new term, in half-millions,
   trimming the largest cheque if rounding leaves the portfolio over. */
function rmRescaleHouses(houses, prevTerm, nextTerm) {
  const from = rmShape(prevTerm).invested;
  const to = rmShape(nextTerm).invested;
  const scale = from > RM_EPS ? to / from : 1;
  const half = (v) => Math.max(0.5, Math.round(v * scale * 2) / 2);
  let out = houses.map((h) =>
    rmNormalize(
      {
        ...h,
        cheque: half(h.cheque),
        followOn: h.followOn > 0 ? half(h.followOn) : 0,
      },
      nextTerm
    )
  );
  let total = out.reduce((a, h) => a + rmCost(h), 0);
  let guard = 0;
  while (total > to + RM_EPS && guard < 200) {
    const i = out.reduce(
      (bi, h, idx) => (h.cheque > out[bi].cheque ? idx : bi),
      0
    );
    if (out[i].cheque <= 0.5) break;
    out = out.map((h, idx) =>
      idx === i ? { ...h, cheque: h.cheque - 0.5 } : h
    );
    total -= 0.5;
    guard += 1;
  }
  return out;
}

/* ---------- Outcome ---------- */
function rmOutcome(term, houses, recycleIn) {
  const s = rmShape(term);
  const T = term;
  const recycle = { ...RM_RECYCLE_DEFAULT, ...(recycleIn || {}) };
  const H = houses || [];
  const yrs = Array.from({ length: T + 1 }, () => 0);

  const houseCall = yrs.slice();
  const houseDist = yrs.slice();
  let invested = 0;
  let houseProceeds = 0;
  let lastCall = 1;
  let lastExit = 1;
  H.forEach((h) => {
    const cost = rmCost(h);
    invested += cost;
    houseCall[h.entry] += h.cheque;
    if (h.followOn > 0) houseCall[h.followOnYear] += h.followOn;
    lastCall = Math.max(lastCall, rmLastCall(h));
    h.exits.forEach((e) => {
      const p = cost * e.share * e.moic;
      houseDist[e.year] += p;
      houseProceeds += p;
      lastExit = Math.max(lastExit, e.year);
    });
  });

  const reserve = s.invested - invested;
  const over = reserve < -RM_EPS;

  const call = houseCall.slice();
  const dist = houseDist.slice();
  if (reserve > RM_EPS) {
    call[lastCall] += reserve;
    dist[lastExit] += reserve;
  }

  const failures = H.filter((h) => rmMoic(h) < 1 - RM_EPS);
  const ripcordCash = failures.reduce((a, h) => a + rmProceeds(h), 0);
  let recycled = 0;
  let recycledBack = 0;
  let recycleYear = null;
  if (recycle.on) {
    failures.forEach((h) => {
      const y = rmLastExit(h);
      if (y >= T) return;
      const amt = rmProceeds(h);
      dist[y] -= amt;
      dist[T] += amt * recycle.moic;
      recycled += amt;
      recycledBack += amt * recycle.moic;
      recycleYear = recycleYear == null ? y : Math.min(recycleYear, y);
    });
    if (recycled > RM_EPS) lastExit = T;
  }
  const proceeds = houseProceeds - recycled + recycledBack;

  const rows = [];
  let hurdle = 0;
  let capital = 0;
  let prefPaid = 0;
  let cuPaid = 0;
  let cumLpCall = 0;
  let cumLpDist = 0;
  const lpCfs = yrs.slice();
  const tot = {
    called: 0,
    lpCalled: 0,
    gpCalled: 0,
    fees: 0,
    expenses: 0,
    roc: 0,
    pref: 0,
    catchUp: 0,
    partner: 0,
    residual: 0,
    lpDist: 0,
    gpDist: 0,
    distributed: 0,
  };
  const lpFrac = RM_LP / RM_FUND;

  for (let y = 1; y <= T; y++) {
    if (y > 1) hurdle *= 1 + RM_PREF;
    const fee = rmFeeInYear(y, s);
    const exp = rmExpInYear(y, term);
    const contrib = call[y] + fee + exp;
    const lpCall = call[y] * s.lpShare + fee + exp * lpFrac;
    const gpCall = contrib - lpCall;
    hurdle += contrib;
    capital += contrib;

    let cash = dist[y];
    const roc = Math.min(cash, capital);
    capital -= roc;
    hurdle -= roc;
    cash -= roc;
    if (capital < RM_EPS) capital = 0;

    let pref = 0;
    if (capital === 0) {
      const prefOut = Math.max(0, hurdle);
      pref = Math.min(cash, prefOut);
      hurdle -= pref;
      cash -= pref;
      prefPaid += pref;
      if (hurdle < RM_EPS) hurdle = 0;
    }

    let catchUp = 0;
    let partner = 0;
    let residual = 0;
    if (capital === 0 && hurdle === 0 && cash > RM_EPS) {
      const need = Math.max(0, (RM_CARRY / (1 - RM_CARRY)) * prefPaid - cuPaid);
      catchUp = Math.min(cash, need);
      cuPaid += catchUp;
      cash -= catchUp;
      partner = cash * (1 - RM_CARRY);
      residual = cash * RM_CARRY;
    }

    const lpDist = (roc + pref) * lpFrac + partner * s.lpShare;
    const gpDist = dist[y] - lpDist;
    cumLpCall += lpCall;
    cumLpDist += lpDist;
    lpCfs[y] = lpDist - lpCall;

    tot.called += contrib;
    tot.lpCalled += lpCall;
    tot.gpCalled += gpCall;
    tot.fees += fee;
    tot.expenses += exp;
    tot.roc += roc;
    tot.pref += pref;
    tot.catchUp += catchUp;
    tot.partner += partner;
    tot.residual += residual;
    tot.lpDist += lpDist;
    tot.gpDist += gpDist;
    tot.distributed += dist[y];

    rows.push({
      year: y,
      houseCall: houseCall[y],
      call: call[y],
      fee,
      exp,
      contrib,
      lpCall,
      gpCall,
      dist: dist[y],
      roc,
      pref,
      catchUp,
      partner,
      residual,
      lpDist,
      gpDist,
      cumLpCall,
      cumLpDist,
      dpi: cumLpCall > RM_EPS ? cumLpDist / cumLpCall : 0,
    });
  }

  const prefDue = prefPaid + Math.max(0, hurdle - capital);
  const prefFull = hurdle === 0 && capital === 0 && prefPaid > RM_EPS;
  const catchUpDue = prefPaid * (RM_CARRY / (1 - RM_CARRY));
  const carry = tot.catchUp + tot.residual;
  const lpResidual = tot.partner * s.lpShare;
  const gpResidualShare = tot.partner - lpResidual;
  const lpRoc = tot.roc * lpFrac;
  const gpRoc = tot.roc - lpRoc;
  const lpPref = tot.pref * lpFrac;
  const gpPref = tot.pref - lpPref;
  const lpCalled = tot.lpCalled;
  const profit = tot.distributed - tot.called;

  const firstDistYear = rows.find((r) => r.lpDist > RM_EPS);
  const capitalBackYear = rows.find(
    (r) => r.cumLpDist >= r.cumLpCall - 1e-4 && r.cumLpDist > RM_EPS
  );
  let lastDistYear = null;
  rows.forEach((r) => {
    if (r.lpDist > RM_EPS) lastDistYear = r.year;
  });
  const firstEntry = H.reduce((a, h) => Math.min(a, h.entry), T);
  const lastEntry = H.reduce((a, h) => Math.max(a, h.entry), 1);
  const topHouse = H.reduce(
    (best, h) => (!best || rmProceeds(h) > rmProceeds(best) ? h : best),
    null
  );
  const survivors = H.filter((h) => rmMoic(h) >= 1 - RM_EPS);
  const flatThree =
    survivors.reduce((a, h) => a + rmCost(h) * 3, 0) + ripcordCash;
  const flatGap = houseProceeds - flatThree;
  const topGap =
    topHouse && rmMoic(topHouse) > 3
      ? rmProceeds(topHouse) - rmCost(topHouse) * 3
      : 0;
  const spx = rmSpxTerminal(rows, RM_SPX);

  return {
    term,
    fees: tot.fees,
    expenses: tot.expenses,
    investable: s.invested,
    L: s.L,
    G: s.G,
    lpShare: s.lpShare,
    ext: s.ext,
    invested,
    houseProceeds,
    proceeds,
    distributed: tot.distributed,
    reserve,
    over,
    called: tot.called,
    lpCalled,
    gpCalled: tot.gpCalled,
    roc: tot.roc,
    lpRoc,
    gpRoc,
    prefPaid: tot.pref,
    lpPref,
    gpPref,
    prefDue,
    prefFull,
    catchUp: tot.catchUp,
    catchUpDue,
    partner: tot.partner,
    residual: tot.residual,
    lpResidual,
    gpResidualShare,
    carry,
    lpDist: tot.lpDist,
    gpTotal: tot.gpDist,
    gpInvestorShare: tot.gpDist - carry,
    grossInv: invested > RM_EPS ? proceeds / invested : 0,
    grossCom: proceeds / RM_FUND,
    net: lpCalled > RM_EPS ? tot.lpDist / lpCalled : 0,
    irr: rmSolveIrr(lpCfs),
    profit,
    rows,
    failures,
    survivors,
    ripcordCash,
    recycle,
    recycled,
    recycledBack,
    recycleYear,
    firstDistYear: firstDistYear ? firstDistYear.year : null,
    capitalBackYear: capitalBackYear ? capitalBackYear.year : null,
    lastDistYear,
    lastExit,
    firstEntry,
    lastEntry,
    topHouse,
    flatThree,
    flatGap,
    topGap,
    spxFvCalls: spx.spxFvCalls,
    spxFvDists: spx.spxFvDists,
    spxRatio: spx.spxRatio,
  };
}

function rmCompute(state) {
  const { term, houses, mine, recycle } = state;
  const o = rmOutcome(term, houses, recycle);
  const share = mine / RM_LP;
  const mineBack = o.lpDist * share;
  const mineSpxCalls = o.spxFvCalls * share;
  const mineSpxDists = o.spxFvDists * share;
  const chequeMax = Math.max(10, Math.ceil((o.investable * 0.6) / 5) * 5);
  return { ...o, houses, mine, share, mineBack, mineSpxCalls, mineSpxDists, chequeMax };
}

/* ---------- Reading copy ---------- */
function rmReading(d) {
  const n = d.houses.length;
  const words = RM_WORDS[n] || String(n);
  const per = (v) => "$" + ((v / RM_FUND) * 100).toFixed(2);
  const yr = (y) => `year ${y}`;
  const lines = [];

  if (d.over) {
    lines.push(
      `Of every $100 committed, ${per(d.fees)} pays the management fee and ${per(d.expenses)} meets fund expenses, leaving ${per(d.investable)} to invest. This portfolio asks for ${per(d.invested)} — more than the fund can deploy, so reduce a cheque before relying on what follows.`
    );
  } else {
    const entryText =
      n > 0
        ? d.firstEntry === d.lastEntry
          ? ` entered in ${yr(d.firstEntry)}`
          : ` entered between ${yr(d.firstEntry)} and ${yr(d.lastEntry)}`
        : "";
    lines.push(
      `Of every $100 committed, ${per(d.fees)} pays the management fee over the life of the fund and ${per(d.expenses)} meets fund expenses. ${per(d.invested)} is invested across ${words} ${n === 1 ? "house" : "houses"}${entryText}${d.reserve > 0.05 ? `, with ${per(d.reserve)} held in reserve and returned with the final distribution` : ""}.`
    );
  }

  const recycleText =
    d.recycled > RM_EPS
      ? ` ${rmFm(d.recycled)} of ripcord cash is recycled in ${yr(d.recycleYear)} and returns ${rmFm(d.recycledBack)} in ${yr(d.term)}.`
      : "";
  lines.push(
    `The houses return ${rmFm(d.proceeds)}: ${rmFx(d.grossInv)} the capital invested, but ${rmFx(d.grossCom)} the capital committed — the difference is the cost of fees, expenses and any capital left uninvested.${recycleText}`
  );

  if (d.firstDistYear) {
    lines.push(
      `The first distribution arrives in ${yr(d.firstDistYear)}.${d.capitalBackYear ? ` Limited partners have their called capital back by ${yr(d.capitalBackYear)}` : " Called capital is never fully returned"}${d.lastDistYear ? `, and the last distribution is in ${yr(d.lastDistYear)}.` : "."}`
    );
  }

  if (d.distributed < d.called - RM_EPS) {
    lines.push(
      `Proceeds fall short of the ${rmFm(d.called)} called, so every dollar returns to the partnership. No preferred return or carried interest is paid.`
    );
  } else if (!d.prefFull) {
    lines.push(
      `Limited partners recover their ${rmFm(d.lpCalled)}, then ${rmFm(d.lpPref)} of the ${rmFm(d.prefDue * (RM_LP / RM_FUND))} preferred return. The 8% hurdle is not cleared, so the general partner earns no carried interest.`
    );
  } else if (d.catchUp < d.catchUpDue - 0.05 && d.carry > RM_EPS) {
    lines.push(
      `Limited partners recover their ${rmFm(d.lpCalled)} and the full ${rmFm(d.lpPref)} preferred return. Proceeds run out during the catch-up, with ${rmFm(d.carry)} paid to the general partner.`
    );
  } else if (d.carry > RM_EPS) {
    lines.push(
      `Limited partners recover their ${rmFm(d.lpCalled)} and the full ${rmFm(d.lpPref)} preferred return. The general partner then catches up ${rmFm(d.catchUp)}, and the remaining ${rmFm(d.partner + d.residual)} is split 80 / 20.`
    );
  } else {
    lines.push(
      `Limited partners recover their ${rmFm(d.lpCalled)}. Preferred return and carried interest are not reached.`
    );
  }

  lines.push(
    `In all, limited partners receive ${rmFm(d.lpDist)} — ${rmFx(d.net)} net, a ${rmFp(d.irr)} net IRR on capital called as the houses are entered. ${d.carry > RM_EPS ? `The general partner’s carried interest is ${rmFm(d.carry)}${d.profit > RM_EPS ? `, ${rmFp(d.carry / d.profit)} of the fund’s profit above capital` : ""}.` : "The general partner receives no carried interest."}`
  );

  lines.push(
    `On a ${rmFm(d.mine, 2)} commitment, you would receive ${rmFm(d.mineBack, 2)}${d.mineBack > d.mine + 0.005 ? ` — ${rmFm(d.mineBack - d.mine, 2)} more than you put in` : d.mineBack < d.mine - 0.005 ? ` — ${rmFm(d.mine - d.mineBack, 2)} less than you put in` : ""}.`
  );

  return lines;
}

/* ---------- Small pieces ---------- */
function RmSelect({ value, onChange, options, label }) {
  return (
    <select
      className="rm-select"
      value={value}
      aria-label={label}
      onChange={(e) => onChange(e.target.value)}
    >
      {options.map((o) => (
        <option key={o.v} value={o.v}>
          {o.t}
        </option>
      ))}
    </select>
  );
}

function rmYearOptions(from, to) {
  const out = [];
  for (let y = from; y <= to; y++) out.push({ v: y, t: `Year ${y}` });
  return out;
}

const RM_FOLLOW_OPTIONS = (() => {
  const out = [{ v: 0, t: "None" }];
  for (let v = 0.5; v <= 10; v += 0.5) out.push({ v, t: rmFm(v) });
  return out;
})();

/* ---------- View ---------- */
function ReturnModelView({ t, navigate }) {
  const copy = (t && t.dataroom && t.dataroom.returnModel) || {};
  const [term, setTerm] = useState(12);
  const [houses, setHouses] = useState(() => rmFreshHouses());
  const [nextId, setNextId] = useState(RM_BASE_HOUSES.length + 1);
  const [mine, setMine] = useState(5);
  const [recycle, setRecycle] = useState(RM_RECYCLE_DEFAULT);
  const [active, setActive] = useState("top");
  const [navHover, setNavHover] = useState(null);
  const [vw, setVw] = useState(
    typeof window !== "undefined" ? window.innerWidth : 1440
  );

  const secEls = useRef({});
  const stripEl = useRef(null);
  const lockSpy = useRef(0);
  const mainEl = useRef(null);

  const result = useMemo(
    () => rmCompute({ term, houses, mine, recycle }),
    [term, houses, mine, recycle]
  );

  useEffect(() => {
    mainEl.current = document.querySelector(".dr-main");
  }, []);

  useEffect(() => {
    const onResize = () => setVw(window.innerWidth);
    window.addEventListener("resize", onResize);
    return () => window.removeEventListener("resize", onResize);
  }, []);

  const offset = useCallback(() => {
    return 24 + (stripEl.current ? stripEl.current.offsetHeight : 80);
  }, []);

  useEffect(() => {
    const scroller = mainEl.current || window;
    let raf = null;
    const onScroll = () => {
      if (raf) return;
      raf = requestAnimationFrame(() => {
        raf = null;
        if (Date.now() < lockSpy.current) return;
        const off = offset();
        let cur = "top";
        const root = mainEl.current;
        const scrollBottom = root
          ? root.scrollTop + root.clientHeight >= root.scrollHeight - 4
          : window.innerHeight + window.scrollY >=
            document.documentElement.scrollHeight - 4;
        if (scrollBottom) cur = RM_LAST_SEC;
        else {
          for (const [k] of RM_NAV) {
            const el = secEls.current[k];
            if (k === "top" || !el) continue;
            const top = el.getBoundingClientRect().top;
            const base = root ? root.getBoundingClientRect().top : 0;
            if (top - base - off <= 8) cur = k;
          }
        }
        setActive((prev) => (prev === cur ? prev : cur));
      });
    };
    scroller.addEventListener("scroll", onScroll, { passive: true });
    return () => {
      scroller.removeEventListener("scroll", onScroll);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [offset]);

  const goTo = useCallback(
    (k) => {
      setActive(k);
      lockSpy.current = Date.now() + 900;
      const el = secEls.current[k];
      const root = mainEl.current;
      if (k === "top" || !el) {
        if (root) root.scrollTo({ top: 0, behavior: "smooth" });
        else window.scrollTo({ top: 0, behavior: "smooth" });
        return;
      }
      if (root) {
        const top =
          el.getBoundingClientRect().top -
          root.getBoundingClientRect().top +
          root.scrollTop -
          offset() +
          8;
        root.scrollTo({ top, behavior: "smooth" });
      } else {
        const top =
          el.getBoundingClientRect().top + window.scrollY - offset() + 8;
        window.scrollTo({ top, behavior: "smooth" });
      }
    },
    [offset]
  );

  const setHouse = useCallback(
    (id, patch) => {
      setHouses((prev) =>
        prev.map((h) =>
          h.id === id
            ? rmNormalize(
                typeof patch === "function" ? patch(h) : { ...h, ...patch },
                term
              )
            : h
        )
      );
    },
    [term]
  );

  const setHouseMoic = useCallback(
    (id, m) => setHouse(id, (h) => rmWithMoic(h, m)),
    [setHouse]
  );

  const setExitYear = useCallback(
    (id, idx, year) =>
      setHouse(id, (h) => ({
        ...h,
        exits: h.exits.map((e, i) => (i === idx ? { ...e, year } : e)),
      })),
    [setHouse]
  );

  const toggleSplit = useCallback(
    (id) => setHouse(id, (h) => rmToggleSplit(h, term)),
    [setHouse, term]
  );

  const changeTerm = useCallback(
    (next) => {
      if (next === term) return;
      setHouses((hs) => rmRescaleHouses(hs, term, next));
      setTerm(next);
    },
    [term]
  );

  const addHouse = useCallback(() => {
    setHouses((prev) => {
      if (prev.length >= RM_MAX_HOUSES) return prev;
      const used = new Set(prev.map((h) => h.name));
      const name =
        RM_SPARE_NAMES.find((n) => !used.has(n)) || `House ${nextId}`;
      const left = result.investable - prev.reduce((a, h) => a + rmCost(h), 0);
      const cheque = rmClamp(Math.floor(left * 2) / 2, 0.5, 10);
      const entry = Math.min(term - 2, 5);
      setNextId((n) => n + 1);
      return [
        ...prev,
        rmNormalize(
          {
            id: nextId,
            name,
            entry,
            cheque,
            followOn: 0,
            followOnYear: entry,
            exits: [{ year: Math.min(term, entry + 5), share: 1, moic: 2 }],
          },
          term
        ),
      ];
    });
  }, [nextId, result.investable, term]);

  const removeHouse = useCallback((id) => {
    setHouses((prev) =>
      prev.length <= 1 ? prev : prev.filter((h) => h.id !== id)
    );
  }, []);

  const applyPreset = useCallback(
    (preset) => {
      setHouses((prev) => rmApplyPresetHouses(prev, preset, term));
    },
    [term]
  );

  const resetAll = useCallback(() => {
    setTerm(12);
    setHouses(rmFreshHouses());
    setNextId(RM_BASE_HOUSES.length + 1);
    setMine(5);
    setRecycle(RM_RECYCLE_DEFAULT);
  }, []);

  const activePreset = RM_PRESETS.find((ps) => rmPresetMatches(ps, houses));

  const reading = useMemo(() => rmReading(result), [result]);
  const showRail = vw >= 900;
  const showNames = vw >= 1280;

  const feeRule =
    term === 12
      ? "2% a year on $147M for five years, then 1.5% a year on invested LP capital for seven years"
      : "2% a year on $147M for five years, then 1.5% a year on invested LP capital for seven years, plus 1% a year in each of two extension years";
  const expRule =
    "$3.0M that does not change with the term, plus $0.4M a year over the life of the fund";

  const allocScale = Math.max(
    RM_FUND,
    result.fees + result.expenses + result.invested
  );
  const lbl = (v, scale, text) => (v / scale >= 0.07 ? text : "");

  const allocSegs = [
    {
      v: result.fees,
      bg: "var(--sapphire-400)",
      bd: "0",
      fg: "var(--alabaster)",
      t: "Fees",
    },
    {
      v: result.expenses,
      bg: "var(--sapphire-600)",
      bd: "0",
      fg: "var(--sapphire-200)",
      t: "Expenses",
    },
    ...houses.map((h, i) => ({
      v: rmCost(h),
      bg: "var(--silk-200)",
      bd: "0",
      fg: "var(--sapphire-800)",
      t: rmCode(i),
    })),
    ...(result.reserve > RM_EPS
      ? [
          {
            v: result.reserve,
            bg: "transparent",
            bd: "1px dashed rgba(169,178,196,.5)",
            fg: "var(--sapphire-200)",
            t: "Reserve",
          },
        ]
      : []),
  ]
    .filter((g) => g.v > RM_EPS)
    .map((g) => ({
      grow: g.v,
      bg: g.bg,
      bd: g.bd,
      fg: g.fg,
      label: lbl(g.v, allocScale, g.t),
    }));

  const pc = (v) => rmFp(v / RM_FUND);
  const allocRows = [
    {
      name: "Management fees",
      rule: feeRule,
      v: rmFm(result.fees),
      pct: pc(result.fees),
      bg: "var(--sapphire-400)",
      bd: "0",
      vFg: "var(--alabaster)",
    },
    {
      name: "Fund expenses",
      rule: expRule,
      v: rmFm(result.expenses),
      pct: pc(result.expenses),
      bg: "var(--sapphire-600)",
      bd: "0",
      vFg: "var(--alabaster)",
    },
    {
      name: "Invested in houses",
      rule: `${houses.length} ${houses.length === 1 ? "house" : "houses"}, entered in years ${result.firstEntry}–${result.lastEntry} · ${rmFm(result.investable)} investable after fees and expenses`,
      v: rmFm(result.invested),
      pct: pc(result.invested),
      bg: "var(--silk-200)",
      bd: "0",
      vFg: result.over ? "var(--gold-200)" : "var(--alabaster)",
    },
    result.over
      ? {
          name: "Over-allocated",
          rule: "Cheques exceed what the fund can invest",
          v: rmFm(-result.reserve),
          pct: pc(-result.reserve),
          bg: "transparent",
          bd: "1px solid var(--gold-500)",
          vFg: "var(--gold-200)",
        }
      : {
          name: "Reserve",
          rule: "Investable but unallocated — called with the last cheque, returned with the final distribution",
          v: rmFm(Math.max(0, result.reserve)),
          pct: pc(Math.max(0, result.reserve)),
          bg: "transparent",
          bd: "1px dashed rgba(169,178,196,.5)",
          vFg: "var(--alabaster)",
        },
  ];

  const wfScale = Math.max(result.distributed, result.called);
  const wfSegsFixed = [
    { v: result.roc, bg: "var(--alabaster)", fg: "var(--sapphire-800)", t: "Capital" },
    { v: result.prefPaid, bg: "var(--gold-200)", fg: "var(--sapphire-800)", t: "Preferred" },
    { v: result.catchUp, bg: "var(--sapphire-400)", fg: "var(--alabaster)", t: "Catch-up" },
    { v: result.partner, bg: "var(--gold-500)", fg: "var(--sapphire-800)", t: "LP 80%" },
    { v: result.residual, bg: "var(--sapphire-200)", fg: "var(--sapphire-800)", t: "GP 20%" },
    {
      v: result.called - result.distributed,
      bg: "transparent",
      bd: "1px dashed rgba(169,178,196,.5)",
      fg: "var(--sapphire-200)",
      t: "Shortfall",
    },
  ]
    .filter((g) => g.v > RM_EPS)
    .map((g) => ({
      grow: g.v,
      bg: g.bg,
      bd: g.bd || "0",
      fg: g.fg,
      label: lbl(g.v, wfScale, g.t),
    }));

  const capF = wfScale ? result.called / wfScale : 0;
  const hurF = wfScale ? (result.called + result.prefDue) / wfScale : 2;
  const tf = (f) => (f > 0.6 ? "translateX(-100%)" : "none");

  const status = (paid, due) =>
    due <= RM_EPS
      ? { st: "Not applicable", stFg: "var(--sapphire-200)" }
      : paid >= due - 0.005
        ? { st: "Met in full", stFg: "var(--gold-400)" }
        : paid > RM_EPS
          ? {
              st: `Partly met · ${rmFm(paid)} of ${rmFm(due)}`,
              stFg: "var(--alabaster)",
            }
          : { st: "Not reached", stFg: "var(--sapphire-200)" };

  const amt = (v) => ({
    s: rmDash(v),
    fg: v > RM_EPS ? "var(--alabaster)" : "var(--sapphire-400)",
  });

  const tiers = [
    {
      code: "01",
      name: "Return of capital",
      sw: ["var(--alabaster)"],
      ...status(result.roc, result.called),
      rule: `Every dollar called is repaid first — including the ${rmFm(result.fees + result.expenses)} that met fees and expenses. Each distribution returns capital before anything else.`,
      lp: amt(result.lpRoc).s,
      lpFg: amt(result.lpRoc).fg,
      gp: amt(result.gpRoc).s,
      gpFg: amt(result.gpRoc).fg,
    },
    {
      code: "02",
      name: "Preferred return",
      sw: ["var(--gold-200)"],
      ...status(result.prefPaid, result.prefDue),
      rule: `8% a year, compounded on capital from the year it is called until it is returned — ${rmFm(result.prefDue)} accrues across the partnership on this schedule.`,
      lp: amt(result.lpPref).s,
      lpFg: amt(result.lpPref).fg,
      gp: amt(result.gpPref).s,
      gpFg: amt(result.gpPref).fg,
    },
    {
      code: "03",
      name: "General partner catch-up",
      sw: ["var(--sapphire-400)"],
      ...status(result.catchUp, result.prefFull ? result.catchUpDue : 0),
      rule: `All proceeds to the GP until it holds 20% of profit above capital — ${rmFm(result.prefFull ? result.catchUpDue : 0)} due.`,
      lp: "—",
      lpFg: "var(--sapphire-400)",
      gp: amt(result.catchUp).s,
      gpFg: amt(result.catchUp).fg,
    },
    {
      code: "04",
      name: "Residual split",
      sw: ["var(--gold-500)", "var(--sapphire-200)"],
      ...(result.partner + result.residual > RM_EPS
        ? { st: "Reached", stFg: "var(--gold-400)" }
        : { st: "Not reached", stFg: "var(--sapphire-200)" }),
      rule: "80% to limited partners (and the GP as investor), 20% to the general partner as carried interest.",
      lp: amt(result.lpResidual).s,
      lpFg: amt(result.lpResidual).fg,
      gp: amt(result.residual + result.gpResidualShare).s,
      gpFg: amt(result.residual + result.gpResidualShare).fg,
    },
  ];

  const strip = [
    {
      k: "Invested",
      v: rmFm(result.invested),
      sub: result.over
        ? `Over by ${rmFm(-result.reserve)}`
        : `of ${rmFm(result.investable)} investable`,
      gold: false,
    },
    {
      k: "Gross proceeds",
      v: rmFm(result.proceeds),
      sub: `${rmFx(result.grossInv)} on invested${result.recycled > RM_EPS ? " · incl. recycled" : ""}`,
      gold: false,
    },
    {
      k: "To limited partners",
      v: rmFm(result.lpDist),
      sub: `${rmFx(result.net)} net · ${rmFp(result.irr)} IRR`,
      gold: false,
    },
    {
      k: "Your distributions",
      v: rmFm(result.mineBack, 2),
      sub: `on ${rmFm(result.mine, 2)} committed`,
      gold: true,
    },
    {
      k: "Your distributions compared to S&P 500",
      v: result.spxFvCalls > RM_EPS ? rmFx(result.spxRatio) : "—",
      sub:
        result.spxFvCalls > RM_EPS
          ? `${rmFm(result.mineSpxDists, 2)} by year ${term} · index ${rmFm(result.mineSpxCalls, 2)}`
          : "Same calls, grown at 10%",
      gold: false,
    },
  ];

  const terms = [
    { k: "Fund size", v: rmFm(RM_FUND, 0), sub: "Target commitments" },
    {
      k: "Management fee",
      v: term === 12 ? "2% → 1.5%" : "2% → 1.5% + ext.",
      sub: "On LP capital, then invested",
    },
    { k: "Fund expenses", v: rmFm(result.expenses), sub: "Life of fund" },
    { k: "Preferred return", v: "8%", sub: "Compounded annually" },
    { k: "Carried interest", v: "20%", sub: "Full catch-up" },
    { k: "Target", v: rmFx(RM_TARGET_NET, 1) + " net", sub: "To limited partners" },
  ];

  const leftN = RM_MAX_HOUSES - houses.length;
  const capPct = result.investable
    ? Math.min(100, (result.invested / result.investable) * 100) + "%"
    : "0%";

  const years = Array.from({ length: term }, (_, i) => i + 1);
  const tlCols = { gridTemplateColumns: `repeat(${term}, minmax(0, 1fr))` };
  const tlLabels = vw >= 1024;

  const toTarget = result.net - RM_TARGET_NET;
  const toTargetM = toTarget * result.lpCalled;
  const failureNames = result.failures.map((h) => rmLower(h.name));
  const top = result.topHouse;

  const setSecRef = (k) => (el) => {
    secEls.current[k] = el;
  };

  return (
    <div className="rm">
      <section className="rm-hero" ref={setSecRef("top")} data-rm-sec="top">
        <span className="rm-eyebrow">
          {copy.eyebrow || "Fund I · Return model"}
        </span>
        <h1 className="rm-title">{copy.title || "Return model"}</h1>
        <p className="rm-italic">
          {copy.italic || "— follow a dollar from commitment to distribution."}
        </p>
        <p className="rm-lede">
          {copy.lede ||
            "An illustrative model of Folclore Latin Fund I, built on the base case the fund puts forward: five houses entered over the first four years, one of which fails, sold between years 8 and 12. Change the cheques, the timing and the multiples, and see how proceeds pass through the waterfall to limited partners and the general partner."}
        </p>

        <div className="rm-terms">
          {terms.map((tRow) => (
            <div key={tRow.k} className="rm-term">
              <span className="rm-term-k">{tRow.k}</span>
              <span className="rm-term-v">{tRow.v}</span>
              <span className="rm-term-sub">{tRow.sub}</span>
            </div>
          ))}
        </div>

        <div className="rm-term-toggle" role="group" aria-label="Fund term">
          <button
            type="button"
            className={"rm-chip" + (term === 12 ? " active" : "")}
            aria-pressed={term === 12}
            onClick={() => changeTerm(12)}
          >
            12-year term
          </button>
          <button
            type="button"
            className={"rm-chip" + (term === 14 ? " active" : "")}
            aria-pressed={term === 14}
            onClick={() => changeTerm(14)}
          >
            + two extension years
          </button>
        </div>

        <p className="rm-disclaimer-top">
          {copy.disclaimer ||
            "Illustrative only. The figures are hypothetical and do not forecast Fund I performance. Capital is called as houses are entered and as fees and expenses fall due; each house is sold in the year shown and proceeds are distributed that year. The limited partnership agreement governs."}
        </p>
      </section>

      <div className="rm-strip" ref={stripEl}>
        {strip.map((k) => (
          <div key={k.k} className="rm-strip-item">
            <span className={"rm-strip-k" + (k.gold ? " gold" : "")}>
              {k.k}
            </span>
            <span className="rm-strip-v">{k.v}</span>
            <span className="rm-strip-sub">{k.sub}</span>
          </div>
        ))}
      </div>

      {/* 01 */}
      <section className="rm-section" ref={setSecRef("01")} data-rm-sec="01">
        <div className="rm-section-head">
          <span className="rm-section-kicker">
            <span className="rm-section-num">01</span>
            Where the capital goes
          </span>
          <h2 className="rm-section-title">
            Every committed dollar, accounted for.
          </h2>
        </div>
        <p className="rm-section-lede">
          Limited partners commit the full amount; the fund calls it as houses
          are entered and as fees fall due. The management fee and fund
          expenses are met from commitments, so only what remains is invested
          in houses.
        </p>

        <div className="rm-bar-label">
          <span>Commitments</span>
          <span>{rmFm(RM_FUND, 0)}</span>
        </div>
        <div className="rm-stack" role="img" aria-label="Capital allocation">
          {allocSegs.map((g, i) => (
            <div
              key={i}
              className="rm-stack-seg"
              style={{
                flexGrow: g.grow,
                background: g.bg,
                border: g.bd,
                color: g.fg,
              }}
            >
              <span>{g.label}</span>
            </div>
          ))}
        </div>

        <div className="rm-alloc-rows">
          {allocRows.map((r) => (
            <div key={r.name} className="rm-alloc-row">
              <span
                className="rm-swatch"
                style={{ background: r.bg, border: r.bd }}
              />
              <div className="rm-alloc-meta">
                <span className="rm-alloc-name">{r.name}</span>
                <span className="rm-alloc-rule">{r.rule}</span>
              </div>
              <span className="rm-alloc-v" style={{ color: r.vFg }}>
                {r.v}
              </span>
              <span className="rm-alloc-pct">{r.pct}</span>
            </div>
          ))}
        </div>

        {result.over && (
          <div className="rm-warn" role="status">
            <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
              <circle
                cx="12"
                cy="12"
                r="10"
                fill="none"
                stroke="currentColor"
                strokeWidth="1.5"
              />
              <path
                d="M12 8v4"
                fill="none"
                stroke="currentColor"
                strokeWidth="1.5"
                strokeLinecap="round"
              />
              <path
                d="M12 16h.01"
                fill="none"
                stroke="currentColor"
                strokeWidth="1.5"
                strokeLinecap="round"
              />
            </svg>
            <span>
              The cheques below exceed investable capital by{" "}
              {rmFm(-result.reserve)}. After fees and expenses, Fund I can
              invest {rmFm(result.investable)} — reduce a cheque to bring the
              portfolio within it.
            </span>
          </div>
        )}
      </section>

      {/* 02 */}
      <section className="rm-section" ref={setSecRef("02")} data-rm-sec="02">
        <div className="rm-section-head">
          <span className="rm-section-kicker">
            <span className="rm-section-num">02</span>
            The houses
          </span>
          <h2 className="rm-section-title">
            Five houses, entered over four years, sold over five.
          </h2>
        </div>
        <p className="rm-section-lede">
          For each house, set the cheque Fund I writes and the gross multiple
          it returns at exit — 1.0× returns the cheque, 0× is a write-off —
          and the years it is entered, topped up and sold. A house can be sold
          in two tranches. Or start from a scenario.
        </p>

        <div className="rm-scenario-row">
          <div className="rm-scenario-group">
            <span className="rm-scenario-label">Scenario</span>
            {RM_PRESETS.map((ps) => {
              const on = activePreset === ps;
              const g = result.invested
                ? houses.reduce(
                    (a, h, i) =>
                      a + rmCost(h) * (ps.m[i] != null ? ps.m[i] : rmMoic(h)),
                    0
                  ) / result.invested
                : 0;
              return (
                <button
                  key={ps.key}
                  type="button"
                  className={"rm-chip" + (on ? " active" : "")}
                  onClick={() => applyPreset(ps)}
                >
                  <span>{ps.name}</span>
                  <span className="rm-chip-sub">{rmFx(g, 2)}</span>
                </button>
              );
            })}
          </div>
          <button type="button" className="rm-chip ghost" onClick={resetAll}>
            Reset
          </button>
        </div>
        <p className="rm-preset-note">
          {activePreset
            ? `${activePreset.name} — ${activePreset.note}`
            : "Custom — your own multiples and timing. Choose a scenario, or reset to the base case."}
        </p>

        <div className="rm-cap">
          <div className="rm-cap-row">
            <span>
              <span className="rm-cap-strong">{rmFm(result.invested)}</span>{" "}
              allocated of {rmFm(result.investable)} investable
            </span>
            <span className={"rm-cap-note" + (result.over ? " over" : "")}>
              {result.over
                ? `Over by ${rmFm(-result.reserve)}`
                : `${rmFm(Math.max(0, result.reserve))} in reserve`}
            </span>
          </div>
          <div className="rm-cap-track">
            <span
              className={"rm-cap-fill" + (result.over ? " over" : "")}
              style={{ width: capPct }}
            />
          </div>
        </div>

        <div className="rm-tl" role="img" aria-label="Investment and exit schedule">
          <div className="rm-tl-head">
            <span>Schedule</span>
            <span>
              Cheques in <span className="rm-tl-key call">sapphire</span>, exits
              in <span className="rm-tl-key exit">gold</span> · years 1–{term}
            </span>
          </div>
          {houses.map((h, i) => {
            const cost = rmCost(h);
            const last = rmLastExit(h);
            return (
              <div key={h.id} className="rm-tl-row">
                <span className="rm-tl-name">
                  <span className="rm-tl-code">{rmCode(i)}</span>
                  <span className="rm-tl-text">{h.name}</span>
                </span>
                <div className="rm-tl-track" style={tlCols}>
                  {years.map((y) => {
                    const call =
                      (y === h.entry ? h.cheque : 0) +
                      (h.followOn > 0 && y === h.followOnYear ? h.followOn : 0);
                    const exit = h.exits
                      .filter((e) => e.year === y)
                      .reduce((a, e) => a + cost * e.share * e.moic, 0);
                    const hold = y >= h.entry && y <= last;
                    return (
                      <div
                        key={y}
                        className={
                          "rm-tl-cell" +
                          (hold ? " hold" : "") +
                          (y === h.entry ? " start" : "") +
                          (y === last ? " end" : "")
                        }
                      >
                        {call > RM_EPS && (
                          <span className="rm-tl-mark call" title={`Year ${y}: ${rmFm(call)} invested`}>
                            <i />
                            {tlLabels && <b>{rmFm(call, call % 1 ? 1 : 0)}</b>}
                          </span>
                        )}
                        {exit > RM_EPS && (
                          <span className="rm-tl-mark exit" title={`Year ${y}: ${rmFm(exit)} returned`}>
                            {tlLabels && <b>{rmFm(exit, exit >= 100 ? 0 : 1)}</b>}
                            <i />
                          </span>
                        )}
                      </div>
                    );
                  })}
                </div>
              </div>
            );
          })}
          {result.recycled > RM_EPS && (
            <div className="rm-tl-row recycle">
              <span className="rm-tl-name">
                <span className="rm-tl-code">↻</span>
                <span className="rm-tl-text">Recycled ripcord cash</span>
              </span>
              <div className="rm-tl-track" style={tlCols}>
                {years.map((y) => {
                  const hold = y >= result.recycleYear && y <= term;
                  return (
                    <div
                      key={y}
                      className={
                        "rm-tl-cell" +
                        (hold ? " hold" : "") +
                        (y === result.recycleYear ? " start" : "") +
                        (y === term ? " end" : "")
                      }
                    >
                      {y === result.recycleYear && (
                        <span className="rm-tl-mark call">
                          <i />
                          {tlLabels && <b>{rmFm(result.recycled)}</b>}
                        </span>
                      )}
                      {y === term && (
                        <span className="rm-tl-mark exit">
                          {tlLabels && <b>{rmFm(result.recycledBack)}</b>}
                          <i />
                        </span>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          )}
          <div className="rm-tl-row axis">
            <span className="rm-tl-name" />
            <div className="rm-tl-track" style={tlCols}>
              {years.map((y) => (
                <span key={y} className="rm-tl-year">
                  {y}
                </span>
              ))}
            </div>
          </div>
        </div>

        <div className="rm-houses">
          {houses.map((h, i) => {
            const cost = rmCost(h);
            const back = rmProceeds(h);
            const moic = rmMoic(h);
            const gain = back - cost;
            const split = h.exits.length > 1;
            const outcome =
              Math.abs(gain) < 0.05
                ? "Capital returned"
                : gain > 0
                  ? `Gain of ${rmFm(gain)}`
                  : moic < 0.05
                    ? "Written off"
                    : `Loss of ${rmFm(-gain)}`;
            const overPlatform = cost > RM_PLATFORM_LIMIT + 0.05;
            const exitText = split
              ? h.exits
                  .map(
                    (e) =>
                      `${Math.round(e.share * 100)}% in year ${e.year} at ${rmFxs(e.moic)}`
                  )
                  .join(", ")
              : `sold in year ${h.exits[0].year}`;
            const minExit = rmLastCall(h) + 1;
            return (
              <div key={h.id} className="rm-house">
                <div className="rm-house-id">
                  <span className="rm-house-code">{rmCode(i)}</span>
                  <div className="rm-house-name-wrap">
                    <input
                      className="rm-house-name"
                      value={h.name}
                      aria-label="House name"
                      onChange={(e) =>
                        setHouse(h.id, { name: e.target.value })
                      }
                    />
                    <span
                      className={
                        "rm-house-outcome" + (gain > 0.05 ? " up" : "")
                      }
                    >
                      {outcome} · {exitText}
                      {overPlatform
                        ? " · above the $30M platform limit"
                        : ""}
                    </span>
                  </div>
                </div>
                <div className="rm-house-ctrl">
                  <div className="rm-slider-head">
                    <span>Cheque</span>
                    <span>{rmFm(cost)}</span>
                  </div>
                  <input
                    type="range"
                    min="0.5"
                    max={result.chequeMax}
                    step="0.5"
                    value={Math.min(h.cheque, result.chequeMax)}
                    aria-label="Initial cheque"
                    onChange={(e) =>
                      setHouse(h.id, { cheque: +e.target.value })
                    }
                  />
                  <span className="rm-slider-sub">
                    {rmFm(h.cheque)} in year {h.entry}
                    {h.followOn > 0
                      ? ` + ${rmFm(h.followOn)} follow-on in year ${h.followOnYear}`
                      : ""}
                    {result.investable
                      ? ` · ${rmFp(cost / result.investable)} of investable capital`
                      : ""}
                  </span>
                </div>
                <div className="rm-house-ctrl">
                  <div className="rm-slider-head">
                    <span>Gross multiple</span>
                    <span>{rmFxs(moic)}</span>
                  </div>
                  <input
                    type="range"
                    min="0"
                    max="12"
                    step="0.1"
                    value={moic}
                    aria-label="Gross multiple"
                    onChange={(e) => setHouseMoic(h.id, +e.target.value)}
                  />
                  <span className="rm-slider-sub">
                    {rmFm(back)} returned
                    {split ? " across two tranches" : ` in year ${h.exits[0].year}`}
                  </span>
                </div>
                <div className="rm-house-remove">
                  {houses.length > 1 && (
                    <button
                      type="button"
                      className="rm-icon-btn"
                      aria-label="Remove house"
                      onClick={() => removeHouse(h.id)}
                    >
                      <svg width="14" height="14" viewBox="0 0 24 24" aria-hidden="true">
                        <path
                          d="M18 6 6 18"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth="1.5"
                          strokeLinecap="round"
                        />
                        <path
                          d="m6 6 12 12"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth="1.5"
                          strokeLinecap="round"
                        />
                      </svg>
                    </button>
                  )}
                </div>

                <div className="rm-timing">
                  <label className="rm-timing-field">
                    <span>Entry</span>
                    <RmSelect
                      label="Entry year"
                      value={h.entry}
                      options={rmYearOptions(1, term - 1)}
                      onChange={(v) => setHouse(h.id, { entry: +v })}
                    />
                  </label>
                  <label className="rm-timing-field">
                    <span>Follow-on</span>
                    <RmSelect
                      label="Follow-on cheque"
                      value={h.followOn}
                      options={RM_FOLLOW_OPTIONS}
                      onChange={(v) => setHouse(h.id, { followOn: +v })}
                    />
                    {h.followOn > 0 && (
                      <RmSelect
                        label="Follow-on year"
                        value={h.followOnYear}
                        options={rmYearOptions(h.entry, term - 1)}
                        onChange={(v) => setHouse(h.id, { followOnYear: +v })}
                      />
                    )}
                  </label>
                  <label className="rm-timing-field">
                    <span>{split ? "Exits" : "Exit"}</span>
                    {h.exits.map((e, idx) => (
                      <React.Fragment key={idx}>
                        {split && (
                          <span className="rm-timing-share">
                            {Math.round(e.share * 100)}%
                          </span>
                        )}
                        <RmSelect
                          label={split ? `Exit year, tranche ${idx + 1}` : "Exit year"}
                          value={e.year}
                          options={rmYearOptions(
                            idx > 0 ? h.exits[idx - 1].year : minExit,
                            term
                          )}
                          onChange={(v) => setExitYear(h.id, idx, +v)}
                        />
                      </React.Fragment>
                    ))}
                  </label>
                  <button
                    type="button"
                    className={"rm-chip small" + (split ? " active" : "")}
                    aria-pressed={split}
                    onClick={() => toggleSplit(h.id)}
                  >
                    {split ? "Sold in two tranches" : "Split the exit"}
                  </button>
                </div>
              </div>
            );
          })}
        </div>

        {houses.length < RM_MAX_HOUSES ? (
          <button type="button" className="rm-add" onClick={addHouse}>
            <span>Add a house</span>
            <span className="rm-add-note">
              ·{" "}
              {(RM_WORDS[leftN] || String(leftN)).replace(/^./, (m) =>
                m.toUpperCase()
              )}{" "}
              more {leftN === 1 ? "place" : "places"}
            </span>
          </button>
        ) : (
          <p className="rm-at-max">
            Six houses — the most this model holds. Remove one to add another.
          </p>
        )}
      </section>

      {/* 03 */}
      <section className="rm-section" ref={setSecRef("03")} data-rm-sec="03">
        <div className="rm-section-head">
          <span className="rm-section-kicker">
            <span className="rm-section-num">03</span>
            Key assumptions
          </span>
          <h2 className="rm-section-title">
            What the base case asks you to believe.
          </h2>
        </div>
        <p className="rm-section-lede">
          Five houses, one of which fails. The multiples above are the claims
          an investor should test. Here is how we think about each, and where
          the evidence sits against the record.
        </p>

        <div className="rm-ka-grid">
          <article className="rm-ka">
            <span className="rm-ka-kicker">01 · The failure</span>
            <h3 className="rm-ka-title">One house in five returns 0.9×.</h3>
            <p>
              Cambridge Associates found that in top-quartile funds, 26% of
              investments returned less than 1.0× — about 1.3 deals in every
              five. One failure in five is in line with that record, and
              arguably conservative.
            </p>
            <p>
              The structure also cushions a failure. The fund holds a minority
              stake in the home-market company, a call option, and a holding
              company with standalone value, so a house that does not travel is
              more likely to return 0.5–0.9× than nothing.
            </p>
            <p className="rm-ka-live">
              {result.failures.length === 0
                ? "In this case, no house returns less than its cost."
                : `In this case, the ${rmList(failureNames)} ${result.failures.length === 1 ? "returns" : "return"} ${rmList(result.failures.map((h) => rmFxs(rmMoic(h))))} — ${rmFm(result.ripcordCash)} back on ${rmFm(result.failures.reduce((a, h) => a + rmCost(h), 0))} invested.`}
            </p>
          </article>

          <article className="rm-ka">
            <span className="rm-ka-kicker">02 · The outlier</span>
            <h3 className="rm-ka-title">
              The claim an investor should test hardest.
            </h3>
            <p>
              In venture, about 4% of financings return 10× or more. Even in
              top-quartile private equity, only 13% of investments exceeded 5×,
              and those datasets have no 10× bucket at all. Give each of five
              independent deals venture’s 4% chance of reaching 10×, and the
              probability that at least one does is about 18% — an illustrative
              figure, and a generous one for growth equity.
            </p>
            {top && result.flatGap > 0.05 && (
              <p className="rm-ka-live">
                A flat 3× on {result.survivors.length === 1 ? "the surviving house" : `the ${RM_WORDS[result.survivors.length] || result.survivors.length} surviving houses`} would
                return {rmFm(result.flatThree)}. This case returns{" "}
                {rmFm(result.houseProceeds)}
                {result.topGap > 0.05
                  ? `, and ${rmFp(result.topGap / result.flatGap, 0)} of the ${rmFm(result.flatGap)} difference comes from the ${rmLower(top.name)}`
                  : ""}
                .
              </p>
            )}
            <p>
              The fund proposes to earn that difference through executed U.S.
              and European market entry rather than passive multiple expansion
              — sourcing access, the fixed-strike call option, cultural
              arbitrage, and the operating playbook. Each is a claim that needs
              evidence rather than assertion; the sector theses and worked
              cases in this room are where we set it out.
            </p>
          </article>

          <article className="rm-ka">
            <span className="rm-ka-kicker">03 · The ripcord</span>
            <h3 className="rm-ka-title">
              Not catching fire by year four? We pull it.
            </h3>
            <p>
              If a house is not catching fire in the United States or Europe by
              its fourth year of hold, the fund triggers the salvage clause: it
              takes 0.9× of its capital back in cash, stops paying the
              $500,000-a-year licence fee, and recycles the cash
              {result.ripcordCash > RM_EPS
                ? ` — ${rmFm(result.ripcordCash)} in this case —`
                : ""}{" "}
              into a new house or into the winners.
            </p>
            <div className="rm-recycle">
              <div className="rm-recycle-row">
                <button
                  type="button"
                  className={"rm-chip" + (recycle.on ? " active" : "")}
                  aria-pressed={recycle.on}
                  disabled={result.ripcordCash <= RM_EPS}
                  onClick={() =>
                    setRecycle((r) => ({ ...r, on: !r.on }))
                  }
                >
                  {recycle.on ? "Recycling on" : "Recycle the ripcord cash"}
                </button>
                {recycle.on && (
                  <label className="rm-recycle-slider">
                    <span className="rm-slider-head">
                      <span>Returned at</span>
                      <span>{rmFx(recycle.moic, 1)}</span>
                    </span>
                    <input
                      type="range"
                      min="0.5"
                      max="6"
                      step="0.1"
                      value={recycle.moic}
                      aria-label="Multiple on recycled capital"
                      onChange={(e) =>
                        setRecycle((r) => ({ ...r, moic: +e.target.value }))
                      }
                    />
                  </label>
                )}
              </div>
              <p className="rm-ka-live">
                {result.ripcordCash <= RM_EPS
                  ? "No house fails in this case, so there is nothing to recycle."
                  : result.recycled > RM_EPS
                    ? `${rmFm(result.recycled)} redeployed in year ${result.recycleYear} returns ${rmFm(result.recycledBack)} in year ${term} — ${rmFm(result.recycledBack - result.recycled)} of additional profit.`
                    : recycle.on
                      ? "The failure is sold in the final year, so there is no time left to recycle."
                      : `This case leaves the recycled capital out: the ${rmList(failureNames)} simply ${result.failures.length === 1 ? "returns" : "return"} ${rmFm(result.ripcordCash)} and the money is distributed.`}
              </p>
            </div>
          </article>

          <article className="rm-ka">
            <span className="rm-ka-kicker">04 · The benchmark and the team</span>
            <h3 className="rm-ka-title">
              Sol de Janeiro, and the people who scaled it.
            </h3>
            <p>
              Sol de Janeiro’s acquisition by L’Occitane is the primary
              benchmark transaction: a Brazilian brand launched in the United
              States in 2015, rolled out nationally with Sephora, and sold a
              majority stake in 2021 at a US$450 million valuation.
            </p>
            <p>
              Fund I is led by two first-time fund managers who are experienced
              operators, supported by advisors with direct luxury-brand scaling
              experience — including a former Chief Marketing Officer of Sol de
              Janeiro.
            </p>
          </article>

          <article className="rm-ka">
            <span className="rm-ka-kicker">05 · What we look for</span>
            <h3 className="rm-ka-title">
              Distressed is a bonus. Too hot means too late.
            </h3>
            <p>
              A house in need of refurbishment is a bonus — if a brand is
              already hot, we are too late. A second bonus: a small
              international presence that is growing on its own, as Farm Rio’s
              was in its early days.
            </p>
          </article>

          <article className="rm-ka target">
            <span className="rm-ka-kicker">06 · The target</span>
            <h3 className="rm-ka-title">
              {rmFx(RM_TARGET_NET, 1)} net to limited partners.
            </h3>
            <p>
              Fund I targets {rmFx(RM_TARGET_NET, 1)} net of management fees,
              fund expenses and carried interest.
            </p>
            <div className="rm-ka-target">
              <span className="rm-ka-target-v">{rmFx(result.net)}</span>
              <span className="rm-ka-target-sub">
                {Math.abs(toTarget) < 0.005
                  ? "This case lands on the target."
                  : toTarget > 0
                    ? `This case is ${rmFx(toTarget)} ahead of the target — ${rmFm(toTargetM)} more to limited partners.`
                    : `This case is ${rmFx(-toTarget)} short of the target — ${rmFm(-toTargetM)} less to limited partners.`}
              </span>
            </div>
            <p>
              {result.spxFvCalls > RM_EPS
                ? `Grown to year ${term} at the S&P 500’s long-run 10% a year, with dividends reinvested, the calls become ${rmFm(result.spxFvCalls)} and the distributions become ${rmFm(result.spxFvDists)} — ${rmFx(result.spxRatio)} the index on the same clock.`
                : "There are no calls on this case to set against the S&P 500."}
            </p>
          </article>
        </div>
      </section>

      {/* 04 */}
      <section className="rm-section" ref={setSecRef("04")} data-rm-sec="04">
        <div className="rm-section-head">
          <span className="rm-section-kicker">
            <span className="rm-section-num">04</span>
            The waterfall
          </span>
          <h2 className="rm-section-title">
            What limited partners receive, tier by tier.
          </h2>
        </div>
        <p className="rm-section-lede">
          Proceeds are distributed in a fixed order across the whole fund, as
          each house is sold. Limited partners are repaid first, then earn a
          preferred return; only after that does the general partner share in
          profit.
        </p>

        <div className="rm-hold-grid">
          <div className="rm-hold-card">
            <div className="rm-slider-head">
              <span>Fund term</span>
              <span>{term} years</span>
            </div>
            <p className="rm-slider-sub">
              The 8% preferred return accrues on capital from the year it is
              called until it is returned: {rmFm(result.prefDue)} on this
              schedule. Capital is called in years {result.firstEntry}–
              {term} as houses are entered and fees fall due
              {result.firstDistYear
                ? `; distributions run from year ${result.firstDistYear} to year ${result.lastDistYear}.`
                : "."}
            </p>
          </div>
          <div className="rm-hold-card mine">
            <div className="rm-slider-head">
              <span className="gold">Your commitment</span>
              <span>{rmFm(mine, 2)}</span>
            </div>
            <input
              type="range"
              min="0.25"
              max="25"
              step="0.25"
              value={mine}
              aria-label="Your commitment"
              onChange={(e) => setMine(+e.target.value)}
            />
            <span className="rm-slider-sub">
              {rmFp(result.share, 2)} of Fund I — you would receive{" "}
              {rmFm(result.mineBack, 2)}, {rmFx(result.net)}.
            </span>
          </div>
        </div>

        <div className="rm-bar-label">
          <span>Distributed</span>
          <span>{rmFm(result.distributed)}</span>
        </div>
        <div className="rm-wf-wrap">
          <div className="rm-stack" role="img" aria-label="Waterfall">
            {wfSegsFixed.map((g, i) => (
              <div
                key={i}
                className="rm-stack-seg"
                style={{
                  flexGrow: g.grow,
                  background: g.bg,
                  border: g.bd,
                  color: g.fg,
                }}
              >
                <span>{g.label}</span>
              </div>
            ))}
          </div>
          <span
            className="rm-mark-line"
            style={{ left: Math.min(1, capF) * 100 + "%" }}
          />
          <span
            className="rm-mark-label top"
            style={{
              left: Math.min(1, capF) * 100 + "%",
              transform: tf(capF),
            }}
          >
            Capital returned · {rmFm(result.called)}
          </span>
          {hurF <= 1 + RM_EPS && result.prefDue > RM_EPS && (
            <React.Fragment>
              <span
                className="rm-mark-line"
                style={{ left: Math.min(1, hurF) * 100 + "%" }}
              />
              <span
                className="rm-mark-label bottom"
                style={{
                  left: Math.min(1, hurF) * 100 + "%",
                  transform: tf(hurF),
                }}
              >
                Hurdle cleared · {rmFm(result.called + result.prefDue)}
              </span>
            </React.Fragment>
          )}
        </div>

        <div className="rm-tiers">
          <div className="rm-tier-head">
            <span />
            <span>Tier</span>
            <span className="right">To limited partners</span>
            <span className="right">To the GP</span>
          </div>
          {tiers.map((tier) => (
            <div key={tier.code} className="rm-tier">
              <div className="rm-tier-code">
                <span>{tier.code}</span>
                <span className="rm-tier-swatches">
                  {tier.sw.map((c, i) => (
                    <span key={i} style={{ background: c }} />
                  ))}
                </span>
              </div>
              <div className="rm-tier-body">
                <span className="rm-tier-name">{tier.name}</span>
                <span className="rm-tier-rule">{tier.rule}</span>
                <span className="rm-tier-st" style={{ color: tier.stFg }}>
                  {tier.st}
                </span>
              </div>
              <span className="rm-tier-amt" style={{ color: tier.lpFg }}>
                {tier.lp}
              </span>
              <span className="rm-tier-amt" style={{ color: tier.gpFg }}>
                {tier.gp}
              </span>
            </div>
          ))}
          <div className="rm-tier-total">
            <span />
            <div>
              <span className="rm-tier-total-k">Total distributions</span>
              <span className="rm-tier-rule">
                {rmFm(result.distributed)} distributed ·{" "}
                {result.distributed > RM_EPS
                  ? rmFp(result.lpDist / result.distributed)
                  : "—"}{" "}
                to limited partners
              </span>
            </div>
            <div className="rm-tier-total-col">
              <span className="rm-tier-total-v">{rmFm(result.lpDist)}</span>
              <span className="rm-tier-total-sub gold">
                {rmFx(result.net)} net
              </span>
            </div>
            <div className="rm-tier-total-col">
              <span className="rm-tier-total-v">{rmFm(result.carry)}</span>
              <span className="rm-tier-total-sub">
                {result.profit > RM_EPS && result.carry > RM_EPS
                  ? `${rmFp(result.carry / result.profit)} of profit`
                  : "No carry"}
              </span>
            </div>
          </div>
        </div>

        <div className="rm-cf">
          <div className="rm-cf-head">
            <span>Year</span>
            <span className="right">Called</span>
            <span className="right">Distributed</span>
            <span className="right">Net</span>
            <span className="right">DPI</span>
            <span className="rm-cf-bar-h" />
          </div>
          {result.rows.map((r) => {
            const scale = result.rows.reduce(
              (a, x) => Math.max(a, x.lpCall, x.lpDist),
              1
            );
            const back = result.capitalBackYear === r.year;
            return (
              <div key={r.year} className={"rm-cf-row" + (back ? " back" : "")}>
                <span className="rm-cf-year">
                  {r.year}
                  {back && <em>capital back</em>}
                </span>
                <span className="right dim">{rmDash(r.lpCall)}</span>
                <span className="right">{rmDash(r.lpDist)}</span>
                <span
                  className={
                    "right" + (r.lpDist - r.lpCall < -RM_EPS ? " dim" : " up")
                  }
                >
                  {rmFm(r.lpDist - r.lpCall)}
                </span>
                <span className="right">
                  {r.dpi > RM_EPS ? rmFx(r.dpi) : "—"}
                </span>
                <span className="rm-cf-bar">
                  <span className="rm-cf-half l">
                    <i
                      className="call"
                      style={{ width: (r.lpCall / scale) * 100 + "%" }}
                    />
                  </span>
                  <span className="rm-cf-half r">
                    <i
                      className="dist"
                      style={{ width: (r.lpDist / scale) * 100 + "%" }}
                    />
                  </span>
                </span>
              </div>
            );
          })}
          <div className="rm-cf-row total">
            <span className="rm-cf-year">Total</span>
            <span className="right dim">{rmFm(result.lpCalled)}</span>
            <span className="right">{rmFm(result.lpDist)}</span>
            <span className="right up">
              {rmFm(result.lpDist - result.lpCalled)}
            </span>
            <span className="right">{rmFx(result.net)}</span>
            <span className="rm-cf-bar" />
          </div>
          <p className="rm-cf-note">
            Limited partners’ cash, year by year: capital called for houses,
            fees and expenses against distributions after carried interest.
            DPI is cumulative distributions over cumulative calls.
          </p>
        </div>
      </section>

      {/* 05 */}
      <section className="rm-section" ref={setSecRef("05")} data-rm-sec="05">
        <div className="rm-section-head">
          <span className="rm-section-kicker">
            <span className="rm-section-num">05</span>
            Reading the picture
          </span>
          <h2 className="rm-section-title">In plain terms.</h2>
        </div>

        <div className="rm-reading">
          <div className="rm-bridge">
            {[
              {
                k: "Gross · on invested",
                v: rmFx(result.grossInv),
                sub: "What the houses return",
                gold: false,
                arrow: true,
              },
              {
                k: "Gross · on committed",
                v: rmFx(result.grossCom),
                sub: "After fees and expenses",
                gold: false,
                arrow: true,
              },
              {
                k: "Net · to limited partners",
                v: rmFx(result.net),
                sub: `After carried interest · target ${rmFx(RM_TARGET_NET, 1)}`,
                gold: true,
                arrow: false,
              },
            ].map((b) => (
              <div key={b.k} className="rm-bridge-item">
                <div className="rm-bridge-body">
                  <span className={"rm-strip-k" + (b.gold ? " gold" : "")}>
                    {b.k}
                  </span>
                  <span className={"rm-bridge-v" + (b.gold ? " gold" : "")}>
                    {b.v}
                  </span>
                  <span className="rm-strip-sub">{b.sub}</span>
                </div>
                {b.arrow && (
                  <svg
                    className="rm-bridge-arrow"
                    width="20"
                    height="20"
                    viewBox="0 0 24 24"
                    aria-hidden="true"
                  >
                    <path
                      d="M5 12h14"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.5"
                      strokeLinecap="round"
                    />
                    <path
                      d="m12 5 7 7-7 7"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.5"
                      strokeLinecap="round"
                    />
                  </svg>
                )}
              </div>
            ))}
          </div>

          <div className="rm-reading-lines">
            {reading.map((line, i) => (
              <div key={i} className="rm-reading-line">
                <span className="rm-reading-n">{rmCode(i)}</span>
                <p>{line}</p>
              </div>
            ))}
          </div>

          <p className="rm-summary">
            — each dollar committed returns ${result.net.toFixed(2)}
            {Math.abs(toTarget) < 0.005
              ? ", on the target."
              : toTarget > 0
                ? `, ahead of the $${RM_TARGET_NET.toFixed(2)} target.`
                : `, against a target of $${RM_TARGET_NET.toFixed(2)}.`}
          </p>
        </div>

        <div className="rm-reading-foot">
          <p>
            Hypothetical. Net returns are before taxes. Capital is called in
            the year each cheque is written, with fees and expenses called as
            they fall due; each house is sold in the year shown and the
            proceeds distributed through the waterfall that year. Recycling is
            modelled only where switched on above. The limited partnership
            agreement governs.
          </p>
          <a href="mailto:dolly.moorheard@folclore.capital">
            Ask the Fund I team about this model
          </a>
        </div>
      </section>

      <footer className="rm-footer">
        <div className="rm-footer-meta">
          <div>
            <span className="rm-term-k">Model basis</span>
            <span className="rm-footer-v">
              Fund I base case · five houses, timed cash flows · draft
            </span>
          </div>
          <div>
            <span className="rm-term-k">Updated</span>
            <span className="rm-footer-v">4 November 2026</span>
          </div>
          <div>
            <span className="rm-term-k">Questions</span>
            <a
              className="rm-footer-v link"
              href="mailto:dolly.moorheard@folclore.capital"
            >
              Write to Dolly Moorhead
            </a>
          </div>
        </div>
        <div className="rm-footer-nav">
          <button
            type="button"
            className="rm-footer-card"
            onClick={() => navigate && navigate("strategy")}
          >
            <span className="rm-term-k">← Previous</span>
            <span className="rm-footer-card-title">Strategy &amp; structure</span>
          </button>
          <button
            type="button"
            className="rm-footer-card next"
            onClick={() =>
              navigate && navigate("verticals", { vertical: "fashion" })
            }
          >
            <span className="rm-term-k">Next →</span>
            <span className="rm-footer-card-title">Sector theses</span>
          </button>
        </div>
      </footer>

      {showRail && (
        <nav className="rm-rail" aria-label="Sections">
          {RM_NAV.map(([k, cd, label]) => {
            const on = active === k;
            const hov = navHover === k;
            return (
              <button
                key={k}
                type="button"
                className={"rm-rail-btn" + (on ? " active" : "")}
                aria-label={label}
                onClick={() => goTo(k)}
                onMouseEnter={() => setNavHover(k)}
                onMouseLeave={() => setNavHover(null)}
              >
                {showNames && (
                  <span
                    className="rm-rail-name"
                    style={{
                      color: on
                        ? "var(--gold-200)"
                        : hov
                          ? "var(--alabaster)"
                          : "var(--sapphire-200)",
                    }}
                  >
                    {label}
                  </span>
                )}
                {!showNames && hov && (
                  <span className="rm-rail-tip">{label}</span>
                )}
                <span
                  className="rm-rail-code"
                  style={{
                    color: on
                      ? "var(--gold-400)"
                      : hov
                        ? "var(--alabaster)"
                        : "var(--sapphire-400)",
                  }}
                >
                  {cd}
                </span>
                <span className="rm-rail-tick-wrap">
                  <span
                    className="rm-rail-tick"
                    style={{
                      width: on ? 20 : hov ? 16 : 10,
                      background: on
                        ? "var(--gold-500)"
                        : hov
                          ? "var(--alabaster)"
                          : "var(--sapphire-400)",
                    }}
                  />
                </span>
              </button>
            );
          })}
        </nav>
      )}
    </div>
  );
}

Object.assign(window, {
  ReturnModelView,
  rmShape,
  rmOutcome,
  rmCompute,
  rmReading,
  rmFreshHouses,
  rmNormalize,
  rmWithMoic,
  rmToggleSplit,
  rmRescaleHouses,
  rmCost,
  rmProceeds,
  rmMoic,
  rmSolveIrr,
  RM_LP,
  RM_GP,
  RM_FUND,
  RM_BASE_HOUSES,
  RM_PRESETS,
  RM_TARGET_NET,
});
