// payout-shared.jsx
// Shared data, primitives, and visual helpers for the 12 payout guidance treatments.
// Scoped names (prefixed pg/PG) so they don't collide with anything else loaded.

const { useState: pgU, useMemo: pgM, useRef: pgR, useEffect: pgE } = React;

// ─────────────────────────────────────────────────────────────────
// DYNAMIC CONTEXT — these are reassigned by pgApplyContext() from
// whatever the main app knows (uploaded policies + confirmed
// beneficiaries + canonical Suzie household). All payout components
// read these bindings on every render, so they always reflect the
// latest state.
//
// In a fresh page the bindings start empty; the main App must call
// pgApplyContext() before the payout screens render.
// ─────────────────────────────────────────────────────────────────
let PG_POLICIES = [];
let PG_PEOPLE = [];
let PG_TOTAL_BY = {};
let PG_GRAND_TOTAL = 0;
let PG_HOUSEHOLD = {};
let PG_EXPENSES = [];
let PG_FAMILY_FACTS = [];
let PG_PERSON_TINTS = {};
let PG_GUIDE_ORDER = [];

// Brand-palette tints, same DNA as BS_PALETTE in beneficiaries-screen
// so the Sankey colors match the avatar chips upstream.
const PG_PALETTE = [
  { light: "#FFE4CB", base: "#FFB876", deep: "#D38D54" }, // peach/orange
  { light: "#CFEDF1", base: "#A1DCE2", deep: "#70B6BA" }, // sky blue
  { light: "#E5F0AE", base: "#C0C983", deep: "#8D9933" }, // soft chartreuse
  { light: "#E2D7F4", base: "#C6B3EB", deep: "#9888BF" }, // soft purple
  { light: "#FAE8B0", base: "#F2D26B", deep: "#C9A93C" }, // honey
  { light: "#F5C9C9", base: "#E89C9C", deep: "#B86A6A" }, // dusty rose
];

// ─────────────────────────────────────────────────────────────────
// EXPENSE TEMPLATES — by-kind library of things life-insurance money
// commonly pays for. Each template is materialized per actual
// beneficiary in pgApplyContext().
//
// `appliesTo` controls who gets the line:
//   "anchor"   → the household-anchor (spouse if any, else 1st bene)
//   "child"    → instantiated per child (one row each)
//   "parent"   → instantiated per parent — gift now, care reserve, invest
//   "other"    → instantiated per other-kind bene (sibling, friend, niece…)
//
// `amount` and `rationale` may be a function (household, person) => ...
// so they can pull live numbers from PG_HOUSEHOLD. Per-person templates
// also receive `person.share` (their $ share across all policies) so the
// amounts scale intelligently to what they're actually inheriting.
// ─────────────────────────────────────────────────────────────────
const PG_EXPENSE_TEMPLATES = [
  // Immediate — for the household-anchor (spouse if any, else 1st bene)
  { tid: "funeral", label: "Funeral & memorial",
    appliesTo: "anchor", amount: 9000, horizon: "now",
    rationale: () => "NFDA 2023 median for a traditional funeral with viewing & burial.",
    source: "NFDA · 2023", category: "Final expenses" },

  { tid: "mortgage", label: "Mortgage payoff",
    appliesTo: "anchor",
    amount: (h) => h.mortgageBalance || 0, horizon: "now",
    rationale: (h, p) => `${h.mortgageYearsLeft || 0} years left on your mortgage. Paying it off means ${p.firstName} and the family stay in the house.`,
    source: (h) => `Your ${h.mortgageBank || "mortgage"} · pulled today`, category: "Debt & home",
    swap: (h) => [
      { label: "Pay it off in full", amount: h.mortgageBalance || 0 },
      { label: "Pay down by half",   amount: Math.round((h.mortgageBalance || 0) / 2) },
      { label: "Keep current payment", amount: 0 },
    ] },

  { tid: "auto", label: "Auto loan payoff",
    appliesTo: "anchor",
    amount: (h) => h.autoLoan || 0, horizon: "now",
    rationale: (h) => `Balance on your ${h.autoVehicle || "auto"} financing.`,
    source: (h) => `Your ${h.autoBank || "lender"} auto loan`, category: "Debt & home",
    skipIfZero: true },

  { tid: "cards", label: "Credit card & revolving debt",
    appliesTo: "anchor",
    amount: (h) => (h.creditCard || 0) + (h.studentLoans || 0), horizon: "now",
    rationale: (h) => {
      const parts = [];
      if (h.creditCard) parts.push(`revolving card balance ($${(h.creditCard / 1000).toFixed(1)}K)`);
      if (h.studentLoans) parts.push(`student loans ($${(h.studentLoans / 1000).toFixed(0)}K)`);
      return `Clears ${parts.join(" + ")} so they don't accrue.`;
    },
    source: "Pulled from Plaid", category: "Debt & home",
    skipIfZero: true },

  // Near-term — anchor only
  { tid: "income", label: "Income replacement",
    appliesTo: "anchor",
    amount: (h) => Math.round((h.suzieIncome || 0) * 3.5),
    horizon: "near",
    rationale: (h, p) => `About 3.5 years at $${(h.suzieIncome || 0).toLocaleString()}/yr — replaces your share of household income while ${p.firstName} recalibrates.`,
    source: "Based on your tax filing", category: "Family support" },

  { tid: "therapy", label: "Family therapy & grief support",
    appliesTo: "anchor", amount: 15000, horizon: "near",
    rationale: (h, p) => `Two years of weekly sessions for ${p.firstName} and the family. Most carriers reimburse some — this covers the gap.`,
    source: "Avg out-of-pocket · Psychology Today", category: "Family support" },

  { tid: "travel", label: "A trip together, every year",
    appliesTo: "anchor", amount: 25000, horizon: "near",
    rationale: () => "$5K/yr × 5 years. So they keep traveling together.",
    source: "Your guidance", category: "Family support" },

  // Children — one row each, scaled by age
  { tid: "college", label: (p) => `${p.firstName}'s college`,
    appliesTo: "child", amount: 115360, horizon: "long", flagged: true,
    rationale: (h, p) => {
      const yrs = window.suzieMilestonesFor ? window.suzieMilestonesFor(p).collegeInYears : 6;
      return `Avg total 4-yr in-state public cost of attendance — tuition, room & board. ${p.firstName} starts in ~${yrs} years.`;
    },
    source: "College Board 2023-24", category: "Education",
    swap: () => [
      { label: "In-state public (4 yr)",   amount: 115360, note: "$28,840/yr × 4" },
      { label: "Out-of-state public",      amount: 184600, note: "$46,150/yr × 4" },
      { label: "Private 4-year",           amount: 241680, note: "$60,420/yr × 4" },
    ] },

  { tid: "grad", label: (p) => `${p.firstName}'s grad school fund`,
    appliesTo: "child", amount: 40000, horizon: "long", flagged: true,
    rationale: (h, p) => `About a third of bachelor's grads pursue grad school. Covers a year of in-state tuition if ${p.firstName} goes.`,
    source: "NCES · 2023", category: "Education" },

  { tid: "launch", label: (p) => `${p.firstName}'s launch fund`,
    appliesTo: "child", amount: 35000, horizon: "long",
    rationale: () => "First apartment, car, the soft landing into adulthood after college.",
    source: "Your guidance", category: "Adulthood" },

  { tid: "invest", label: (p) => `Investment for ${p.firstName}'s growth`,
    appliesTo: "child", amount: 80000, horizon: "long", flagged: true,
    rationale: (h, p) => `Held in ${p.firstName}'s name in low-cost index funds. At ~7% real return, compounds for decades.`,
    source: "Vanguard 7% projection", category: "Investment & growth" },

  { tid: "milestone", label: (p) => `${p.firstName}'s wedding or big milestone`,
    appliesTo: "child", amount: 25000, horizon: "long", flagged: true,
    rationale: () => "The Knot 2023 median wedding was $35K. This covers most of it, if they want one.",
    source: "The Knot · 2023", category: "Adulthood" },

  // Long-horizon anchor things
  { tid: "emergency", label: "Emergency fund",
    appliesTo: "anchor",
    amount: (h) => Math.round((h.monthlySpend || h.annualSpend / 12 || 0) * 6),
    horizon: "long",
    rationale: () => "Six months of household expenses. Genuinely-rainy-day money.",
    source: "Based on your spend · Plaid", category: "Investment & growth" },

  { tid: "retirement", label: (p) => `Retirement top-up for ${p.firstName}`,
    appliesTo: "anchor", amount: 60000, horizon: "long",
    rationale: (h, p) => `Five years of Roth contributions you were making. Compounds in ${p.firstName}'s 401(k) for the rest of their career.`,
    source: "Based on your filing", category: "Investment & growth" },

  { tid: "ltc", label: "Long-term care reserve",
    appliesTo: "anchor", amount: 104000, horizon: "long", flagged: true,
    rationale: (h, p) => `Genworth's median for a year of nursing care is ~$104K. Gives ${p.firstName} a year of coverage if needed later in life.`,
    source: "Genworth Cost of Care · 2023", category: "Investment & growth" },

  { tid: "propertytax", label: "Property tax buffer",
    appliesTo: "anchor", amount: 28000, horizon: "long",
    rationale: () => "Ten years of property tax at your current rate. Keeps the house safe without a forced sale.",
    source: "San Mateo County · 2024", category: "Debt & home" },

  // ── Parents (kind: "parent") ────────────────────────────────────
  // Three buckets that fit nearly every parent-as-beneficiary:
  // an immediate gift, a care reserve, and a long-term comfort pool.
  // Amounts scale to the parent's own share of the policies so a
  // small allocation doesn't get over-budgeted.
  { tid: "parent_gift", label: (p) => `An immediate gift to ${p.firstName}`,
    appliesTo: "parent", horizon: "now",
    amount: (h, p) => Math.min(50000, Math.round((p.share || 0) * 0.20)),
    rationale: (h, p) => `A lump ${p.firstName} can use right away — to pay down debts, take a trip, or just breathe. Sized to ~20% of what they receive.`,
    source: "Your guidance", category: "Family support" },

  { tid: "parent_care", label: (p) => `Care reserve for ${p.firstName}`,
    appliesTo: "parent", horizon: "long", flagged: true,
    amount: (h, p) => Math.round((p.share || 0) * 0.35),
    rationale: (h, p) => `Held in reserve for ${p.firstName}'s future medical and long-term care needs. Genworth's median for a year of nursing care is ~$104K — this is roughly a year and a half.`,
    source: "Genworth Cost of Care · 2023", category: "Family support" },

  { tid: "parent_invest", label: (p) => `Invested for ${p.firstName}'s comfort`,
    appliesTo: "parent", horizon: "long",
    amount: (h, p) => Math.round((p.share || 0) * 0.20),
    rationale: (h, p) => `A conservative income-generating portfolio in ${p.firstName}'s name. At ~4% real, throws off a small annuity for them long term.`,
    source: "Vanguard income portfolio", category: "Investment & growth" },

  // ── Other kinds (siblings, friends, nieces, etc.) ───────────────
  // One single, meaningful gift line — the rest naturally falls into
  // "Invest for growth" which the plan adds automatically.
  { tid: "other_gift", label: (p) => `Legacy gift to ${p.firstName}`,
    appliesTo: "other", horizon: "near",
    amount: (h, p) => Math.round((p.share || 0) * 0.50),
    rationale: (h, p) => `A meaningful lump for ${p.firstName}. The remainder of their share is invested for long-term growth in their name.`,
    source: "Your guidance", category: "Family support" },
];

// ─────────────────────────────────────────────────────────────────
// pgApplyContext — main entry point, called by the App whenever
// uploaded policies or confirmed beneficiaries change. Rebuilds
// every PG_* binding from inputs.
//
// state: {
//   policies:         [<analyzed policy objects from HelmAI>],
//   beneficiaryPeople:[<rows from bsBuildPeople, each with .policies>],
//   household:        SUZIE.household,
// }
// ─────────────────────────────────────────────────────────────────
function pgApplyContext(state) {
  const policies = state.policies || [];
  const benes    = state.beneficiaryPeople || [];
  const household= state.household || {};

  // 1. PG_HOUSEHOLD — pass-through
  PG_HOUSEHOLD = { ...household };

  // 2. PG_PEOPLE — one row per beneficiary, in the order they appear
  PG_PEOPLE = benes.map((b, idx) => {
    const enriched = window.suzieEnrich ? window.suzieEnrich(b) : b;
    const tint = PG_PALETTE[idx % PG_PALETTE.length];
    // Prefer the user-confirmed relationship; if blank, fall back to
    // what we know about a named match (John → "Spouse", etc).
    const relationship = b.relationship || enriched.relationship || "";
    return {
      id: b.name, // person id = full name
      name: b.name,
      firstName: enriched.firstName || (b.name || "").split(" ")[0],
      relationship,
      age: enriched.age || null,
      kind: window.suzieKindOf ? window.suzieKindOf(relationship) : "other",
      avatarColor: tint.base,
      tint,
      // Same gendered stock portrait the Last Wishes step uses, so a
      // person looks identical across the whole flow. (Absent in the
      // standalone payout files, which don't load the Last Wishes module.)
      photo: window.lwsPhotoFor ? window.lwsPhotoFor(b.name, relationship) : null,
    };
  });

  // 3. PG_PERSON_TINTS — same palette as the avatars (matches BS_PALETTE)
  PG_PERSON_TINTS = {};
  PG_PEOPLE.forEach((p) => { PG_PERSON_TINTS[p.id] = p.tint; });

  // 4. PG_POLICIES — invert the bene→policy mapping into policy.splits
  // by summing each person's $share of each policy.
  const polMap = new Map();
  policies.forEach((pol) => {
    polMap.set(pol.policyNumber, {
      id: pol.policyNumber,
      carrier: pol.carrier,
      coverage: pol.coverage || pol.coverageType || "Policy",
      faceAmount: pol.faceAmountNum || 0,
      faceAmountStr: pol.faceAmount || `$${(pol.faceAmountNum || 0).toLocaleString()}`,
      splits: {},
    });
  });
  benes.forEach((b) => {
    (b.policies || []).forEach((pp) => {
      const row = polMap.get(pp.policyNumber);
      if (!row) return;
      const pct = row.faceAmount > 0 ? (pp.dollarShare || 0) / row.faceAmount : 0;
      row.splits[b.name] = (row.splits[b.name] || 0) + pct;
    });
  });
  PG_POLICIES = [...polMap.values()];

  // 5. PG_TOTAL_BY — sum each person's $share across all their policies
  PG_TOTAL_BY = {};
  PG_PEOPLE.forEach((p) => { PG_TOTAL_BY[p.id] = 0; });
  benes.forEach((b) => {
    PG_TOTAL_BY[b.name] = (b.policies || []).reduce((s, pp) => s + (pp.dollarShare || 0), 0);
  });
  PG_GRAND_TOTAL = Object.values(PG_TOTAL_BY).reduce((s, v) => s + v, 0);

  // 6. PG_EXPENSES — materialize templates per actual beneficiary
  PG_EXPENSES = pgBuildExpenses(PG_PEOPLE, PG_HOUSEHOLD);
  PG_GUIDE_ORDER = PG_EXPENSES.map((e) => e.id);

  // 7. PG_FAMILY_FACTS — derived from people + household
  PG_FAMILY_FACTS = pgBuildFamilyFacts(PG_PEOPLE, PG_HOUSEHOLD);

  // 8. Re-expose on window for any downstream consumers and so the main
  //    App can read updated values back if needed.
  Object.assign(window, {
    PG_POLICIES, PG_PEOPLE, PG_TOTAL_BY, PG_GRAND_TOTAL,
    PG_HOUSEHOLD, PG_EXPENSES, PG_FAMILY_FACTS, PG_PERSON_TINTS,
    PG_GUIDE_ORDER,
  });

  return {
    policies: PG_POLICIES, people: PG_PEOPLE, totalBy: PG_TOTAL_BY,
    grandTotal: PG_GRAND_TOTAL, expenses: PG_EXPENSES,
    familyFacts: PG_FAMILY_FACTS, guideOrder: PG_GUIDE_ORDER,
  };
}

// Materialize expense templates against the actual people. The
// household-anchor (spouse-or-first-bene) gets anchor-kind templates;
// each child/parent/other gets their own kind's templates. We inject
// `person.share` (their $ share of the policies) so per-person amounts
// can scale intelligently to what they're actually inheriting.
function pgBuildExpenses(people, household) {
  const anchor = people.find((p) => p.kind === "spouse") || people[0];
  const out = [];

  const resolve = (v, h, p) => typeof v === "function" ? v(h, p) : v;

  const apply = (tpl, person, suffix) => {
    if (!person) return;
    // Enrich the person row with their share so per-person amount/rationale
    // functions can size lines relative to what they're inheriting.
    const personShare = { ...person, share: PG_TOTAL_BY[person.id] || 0 };
    const amount = resolve(tpl.amount, household, personShare);
    if (tpl.skipIfZero && (!amount || amount === 0)) return;
    out.push({
      id: `${tpl.tid}__${suffix}`,
      label: resolve(tpl.label, personShare),
      to: person.id,
      amount,
      horizon: tpl.horizon,
      category: tpl.category,
      flagged: !!tpl.flagged,
      rationale: resolve(tpl.rationale, household, personShare) || "",
      source: resolve(tpl.source, household, personShare) || "",
      swap: tpl.swap ? resolve(tpl.swap, household, personShare) : undefined,
    });
  };

  // Anchor (spouse/primary) — pull all anchor templates
  PG_EXPENSE_TEMPLATES
    .filter((t) => t.appliesTo === "anchor")
    .forEach((t) => apply(t, anchor, anchor ? anchor.firstName.toLowerCase() : "anchor"));

  // Everyone else — kind-specific templates. Iterating people in
  // order keeps the resulting PG_EXPENSES (and the guide walk-through)
  // grouped by person: anchor first, then each non-anchor beneficiary.
  people.forEach((person) => {
    if (person === anchor) return;
    PG_EXPENSE_TEMPLATES
      .filter((t) => t.appliesTo === person.kind)
      .forEach((t) => apply(t, person, person.firstName.toLowerCase()));
  });

  return out;
}

// Build the "what we know about your family" tiles from household
// + people. Hard-synced items (Plaid, etc.) get `confidence: "hard"`;
// projected milestones get `confidence: "estimate"`.
function pgBuildFamilyFacts(people, h) {
  const facts = [];
  // People tiles
  people.forEach((p) => {
    facts.push({
      id: `ff-p-${p.id}`,
      label: p.name,
      value: `${p.relationship || "Beneficiary"}${p.age != null ? ` · ${p.age}` : ""}`,
      sub: p.kind === "child" ? "From your policy" : (p.relationship || "From your policy"),
      icon: "user", category: "People", confidence: "self",
    });
  });
  // Debts & home
  if (h.mortgageBalance) facts.push({
    id: "ff-mortgage", label: "Mortgage",
    value: `$${h.mortgageBalance.toLocaleString()}`,
    sub: `${h.mortgageYearsLeft || ""} yrs left · $${(h.mortgageMonthly || 0).toLocaleString()}/mo${h.mortgageBank ? ` · ${h.mortgageBank}` : ""}`,
    icon: "home", category: "Debts & home", confidence: "hard",
  });
  if (h.autoLoan) facts.push({
    id: "ff-auto", label: "Auto loan",
    value: `$${h.autoLoan.toLocaleString()}`,
    sub: `${h.autoVehicle || ""}${h.autoBank ? ` · ${h.autoBank}` : ""}`.trim(),
    icon: "bag", category: "Debts & home", confidence: "hard",
  });
  if (h.studentLoans) facts.push({
    id: "ff-student", label: "Student loans",
    value: `$${h.studentLoans.toLocaleString()}`,
    sub: "FedLoan", icon: "book", category: "Debts & home", confidence: "hard",
  });
  if (h.creditCard) facts.push({
    id: "ff-cards", label: "Credit cards",
    value: `$${h.creditCard.toLocaleString()}`,
    sub: "Plaid · revolving",
    icon: "bag", category: "Debts & home", confidence: "hard",
  });
  // Income & spend
  if (h.annualSpend) facts.push({
    id: "ff-spend", label: "Annual household spend",
    value: `$${h.annualSpend.toLocaleString()}`,
    sub: `~$${(h.monthlySpend || Math.round(h.annualSpend / 12)).toLocaleString()}/mo · Plaid`,
    icon: "heart", category: "Income & spend", confidence: "hard",
  });
  if (h.savings) facts.push({
    id: "ff-savings", label: "Liquid savings",
    value: `$${h.savings.toLocaleString()}`,
    sub: "Across all accounts · Plaid",
    icon: "bag", category: "Income & spend", confidence: "hard",
  });
  if (h.suzieIncome) facts.push({
    id: "ff-income", label: "Your income",
    value: `$${h.suzieIncome.toLocaleString()}/yr`,
    sub: h.jointIncome ? `Of joint $${h.jointIncome.toLocaleString()} · taxes` : "Per your filing",
    icon: "heart", category: "Income & spend", confidence: "hard",
  });
  // Milestones ahead (per child)
  people.filter((p) => p.kind === "child").forEach((c) => {
    const m = window.suzieMilestonesFor ? window.suzieMilestonesFor(c) : {};
    if (m.collegeInYears != null) facts.push({
      id: `ff-college-${c.id}`,
      label: `${c.firstName} starts college`,
      value: `in ~${m.collegeInYears} yrs`,
      sub: "school choice unknown",
      icon: "book", category: "Milestones ahead", confidence: "estimate",
    });
  });
  // Spouse retirement
  const spouse = people.find((p) => p.kind === "spouse");
  if (spouse) {
    const m = window.suzieMilestonesFor ? window.suzieMilestonesFor(spouse) : {};
    if (m.retirementInYears != null) facts.push({
      id: `ff-retire-${spouse.id}`,
      label: `${spouse.firstName}'s retirement`,
      value: `in ~${m.retirementInYears} yrs`,
      sub: "401(k) growing",
      icon: "clock", category: "Milestones ahead", confidence: "estimate",
    });
  }
  return facts;
}

// Aggregate the AI's draft per-beneficiary
const pgDraftFor = (id) => {
  const items = PG_EXPENSES.filter(e => e.to === id);
  const total = items.reduce((s, e) => s + e.amount, 0);
  return { items, total, share: PG_TOTAL_BY[id], remaining: PG_TOTAL_BY[id] - total };
};

const PG_HORIZONS = [
  { id: "now",  label: "Now & next year",  short: "Now",       desc: "Bills due in the first 12 months." },
  { id: "near", label: "Next 1–5 years",   short: "Soon",      desc: "While the family is rebuilding rhythm." },
  { id: "long", label: "5–30 years out",   short: "Later",     desc: "College, retirement, the slow build." },
];

const PG_CATEGORY_TINTS = {
  "Final expenses":       { light: "#F0E9CB", base: "#E0C786", deep: "#A8843A" },
  "Debt & home":          { light: "#FFD6B5", base: "#FFB876", deep: "#D38D54" },
  "Family support":       { light: "#E2D7F4", base: "#C6B3EB", deep: "#9888BF" },
  "Education":            { light: "#CFEDF1", base: "#A1DCE2", deep: "#70B6BA" },
  "Investment & growth":  { light: "#E5F0AE", base: "#C0C983", deep: "#8D9933" },
  "Adulthood":            { light: "#FFD6D6", base: "#E89B9B", deep: "#B36868" },
};

// ─────────────────────────────────────────────────────────────────
// PRIMITIVES
// ─────────────────────────────────────────────────────────────────

const pgInitials = (n) => n.split(" ").map(s => s[0]).join("").slice(0, 2).toUpperCase();

const PGAvatar = ({ id, name, size = 40, ring }) => {
  const p = PG_PEOPLE.find(x => x.id === id) || { name, avatarColor: "var(--helm-ecru)" };
  const ringShadow = ring ? `0 0 0 2px var(--helm-white), 0 0 0 3px ${ring}` : "none";
  if (p.photo) {
    return (
      <img src={p.photo} alt={p.name || name || ""} style={{
        width: size, height: size, borderRadius: "50%", objectFit: "cover",
        flexShrink: 0, display: "inline-block", boxShadow: ringShadow,
      }} />
    );
  }
  return (
    <div style={{
      width: size, height: size, borderRadius: "50%",
      background: p.avatarColor,
      color: "var(--helm-dark-green)",
      display: "inline-flex", alignItems: "center", justifyContent: "center",
      fontFamily: "var(--font-sans)", fontWeight: 600,
      fontSize: size * 0.38, letterSpacing: "-0.01em",
      flexShrink: 0,
      boxShadow: ringShadow,
    }}>{pgInitials(p.name || name || "?")}</div>
  );
};

const PGCarrier = ({ name, size = "sm" }) => {
  const palette = {
    "Banner Life":   { bg: "#FFFFFF", fg: "#006EF5", border: true },
    "New York Life": { bg: "#0033A0", fg: "#FFFFFF" },
    "Pacific Life":  { bg: "#0F2A4F", fg: "#FFFFFF" },
  }[name] || { bg: "var(--helm-ecru)", fg: "var(--helm-dark-green)" };
  const [h, fs] = size === "md" ? [22, 11] : [18, 9.5];
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", justifyContent: "center",
      height: h, padding: "0 8px", borderRadius: 3,
      background: palette.bg, color: palette.fg,
      border: palette.border ? "1px solid var(--helm-dark-ecru)" : "none",
      fontFamily: "var(--font-sans)", fontWeight: 600, fontSize: fs, letterSpacing: "-0.01em",
      whiteSpace: "nowrap",
    }}>{name}</span>
  );
};

const PGMoney = ({ value, size = 14, weight = 600, opacity = 1 }) => (
  <span style={{
    fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums",
    fontSize: size, fontWeight: weight, opacity,
    letterSpacing: "-0.01em",
  }}>${Math.round(value).toLocaleString()}</span>
);

// Compact money like $385K / $1.5M for tight layouts
const pgK = (v) => {
  if (v >= 1000000) return `$${(v / 1000000).toFixed(v % 1000000 === 0 ? 0 : 2)}M`;
  if (v >= 1000) return `$${Math.round(v / 1000)}K`;
  return `$${v}`;
};

// Artboard wrapper (1280x900)
const PGArtboard = ({ children, bg = "var(--helm-white)" }) => (
  <div style={{
    width: 1280, height: 900, background: bg, overflow: "hidden",
    fontFamily: "var(--font-sans)", color: "var(--helm-dark-green)",
    display: "flex", flexDirection: "column",
  }}>{children}</div>
);

// Compact rationale header at the top of every treatment
const PGRationale = ({ tag, title, children }) => (
  <div style={{
    padding: "16px 32px 12px",
    background: "var(--helm-ecru)",
    borderBottom: "1px solid var(--helm-dark-ecru)",
    flexShrink: 0,
  }}>
    <div style={{ display: "flex", alignItems: "baseline", gap: 10, marginBottom: 3 }}>
      <span style={{
        fontFamily: "var(--font-sans)", fontSize: 10, fontWeight: 700,
        letterSpacing: "0.1em", textTransform: "uppercase", opacity: 0.55,
      }}>{tag}</span>
      <span style={{ fontFamily: "var(--font-sans)", fontSize: 15, fontWeight: 600 }}>{title}</span>
    </div>
    <div style={{
      fontFamily: "var(--font-sans)", fontSize: 12, lineHeight: "18px",
      color: "var(--helm-dark-green)", opacity: 0.7, maxWidth: 980,
    }}>{children}</div>
  </div>
);

// Tiny inline icons (Lucide-substituted; flag if replaced)
const PGIcon = ({ name, size = 16, stroke = 1.6, style }) => {
  const paths = {
    sparkle:  <><path d="M12 3l2.4 6.6L21 12l-6.6 2.4L12 21l-2.4-6.6L3 12l6.6-2.4z"/></>,
    edit:     <><path d="M11 4H4v16h16v-7"/><path d="M18.5 2.5a2.12 2.12 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></>,
    info:     <><circle cx="12" cy="12" r="9"/><path d="M12 8v.01M11 12h1v5h1"/></>,
    plus:     <><path d="M12 5v14M5 12h14"/></>,
    check:    <><path d="M5 13l4 4L19 7"/></>,
    chevron:  <><path d="M6 9l6 6 6-6"/></>,
    home:     <><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2h-4v-7H10v7H6a2 2 0 01-2-2z"/></>,
    book:     <><path d="M4 19.5A2.5 2.5 0 016.5 17H20V4H6.5A2.5 2.5 0 004 6.5z"/><path d="M4 19.5V22h16"/></>,
    heart:    <><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></>,
    bag:      <><path d="M6 7h12l-1 13H7z"/><path d="M9 7a3 3 0 016 0"/></>,
    plane:    <><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 00-3 0V9l-8 5v2l8-2.5V19l-3 2v1l4.5-1L16 22v-1l-3-2v-5.5z"/></>,
    coffin:   <><path d="M8 3h8l3 9-3 9H8l-3-9z"/></>,
    mic:      <><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0014 0M12 18v3M8 21h8"/></>,
    user:     <><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0116 0"/></>,
    list:     <><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></>,
    clock:    <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
    drag:     <><circle cx="9" cy="6" r="1.2"/><circle cx="15" cy="6" r="1.2"/><circle cx="9" cy="12" r="1.2"/><circle cx="15" cy="12" r="1.2"/><circle cx="9" cy="18" r="1.2"/><circle cx="15" cy="18" r="1.2"/></>,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round" style={style}>
      {paths[name] || <circle cx="12" cy="12" r="9"/>}
    </svg>
  );
};

// AI suggestion source pill (small attribution chip used everywhere)
const PGSourcePill = ({ source }) => (
  <span style={{
    display: "inline-flex", alignItems: "center", gap: 4,
    fontFamily: "var(--font-sans)", fontSize: 10, fontWeight: 500,
    color: "var(--helm-dark-green)", opacity: 0.55,
    background: "transparent",
  }}>
    <PGIcon name="sparkle" size={10} stroke={2} />
    {source}
  </span>
);

// Horizon chip
const PGHorizonChip = ({ horizon, size = "sm" }) => {
  const h = PG_HORIZONS.find(x => x.id === horizon) || PG_HORIZONS[0];
  return (
    <span style={{
      display: "inline-flex", alignItems: "center",
      height: size === "sm" ? 18 : 22, padding: "0 8px",
      borderRadius: 999,
      background: horizon === "now" ? "var(--helm-lemon)"
                : horizon === "near" ? "var(--helm-soft-chartreuse)"
                : "var(--helm-blue)",
      color: "var(--helm-dark-green)",
      fontFamily: "var(--font-sans)", fontSize: size === "sm" ? 10 : 11,
      fontWeight: 700, letterSpacing: "0.04em", textTransform: "uppercase",
    }}>{h.short}</span>
  );
};

// "Why this number" expandable note
const PGRationaleNote = ({ rationale, source, compact }) => (
  <div style={{
    display: "flex", gap: 8, alignItems: "flex-start",
    padding: compact ? "6px 10px" : "8px 12px",
    background: "var(--helm-ecru)", borderRadius: 8,
    fontFamily: "var(--font-sans)", fontSize: compact ? 11 : 12,
    lineHeight: 1.4, color: "var(--helm-dark-green)",
  }}>
    <PGIcon name="sparkle" size={compact ? 11 : 13} stroke={2} style={{ marginTop: 1, opacity: 0.6, flexShrink: 0 }} />
    <div style={{ flex: 1, minWidth: 0 }}>
      <div>{rationale}</div>
      {source && (
        <div style={{ marginTop: 4, opacity: 0.55, fontSize: compact ? 10 : 11, fontWeight: 500 }}>
          {source}
        </div>
      )}
    </div>
  </div>
);

// Money input (used in structured treatments)
const PGAmountField = ({ value, onChange, suggested, compact }) => {
  const isSuggested = suggested != null && value === suggested;
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 3, alignItems: "flex-end" }}>
      <div style={{
        display: "inline-flex", alignItems: "center", gap: 2,
        padding: compact ? "4px 8px" : "6px 10px",
        border: "1px solid var(--helm-dark-ecru)", borderRadius: 6,
        background: "var(--helm-white)",
        fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums",
        fontSize: compact ? 13 : 14, fontWeight: 600,
      }}>
        <span style={{ opacity: 0.55 }}>$</span>
        <input type="text" value={value.toLocaleString()}
          onChange={e => onChange(parseInt(e.target.value.replace(/[^0-9]/g, "") || "0", 10))}
          style={{
            width: compact ? 64 : 80, border: "none", outline: "none",
            background: "transparent", color: "var(--helm-dark-green)",
            fontFamily: "inherit", fontVariantNumeric: "tabular-nums",
            fontSize: "inherit", fontWeight: "inherit", textAlign: "right", padding: 0,
          }}
        />
      </div>
      {suggested != null && !isSuggested && (
        <button onClick={() => onChange(suggested)} style={{
          background: "transparent", border: "none", padding: 0,
          fontFamily: "var(--font-sans)", fontSize: 10, fontWeight: 500,
          color: "var(--helm-chartreuse-2)", cursor: "pointer",
        }}>↺ Helm suggests {pgK(suggested)}</button>
      )}
    </div>
  );
};

// Allocation progress bar (out of share)
const PGAllocBar = ({ allocated, share, color = "var(--helm-chartreuse-2)", height = 8, showLabel = false }) => {
  const pct = Math.min(100, Math.max(0, (allocated / share) * 100));
  return (
    <div style={{ width: "100%" }}>
      <div style={{
        height, borderRadius: 999, background: "var(--helm-dark-ecru)",
        overflow: "hidden", position: "relative",
      }}>
        <div style={{
          width: `${pct}%`, height: "100%", background: color,
          transition: "width .2s",
        }} />
      </div>
      {showLabel && (
        <div style={{
          marginTop: 4, display: "flex", justifyContent: "space-between",
          fontFamily: "var(--font-sans)", fontSize: 11, fontWeight: 500,
          color: "var(--helm-dark-green)", opacity: 0.7,
          fontVariantNumeric: "tabular-nums",
        }}>
          <span>{pgK(allocated)} allocated</span>
          <span>{pgK(share - allocated)} left</span>
        </div>
      )}
    </div>
  );
};

// AI bubble (serif-prose, like the rest of the prototype)
const PGAIBubble = ({ children, tight, dark }) => (
  <div style={{
    display: "flex", gap: 10, alignItems: "flex-start",
    padding: tight ? "0" : "0",
    fontFamily: "var(--font-serif)", fontSize: 14, lineHeight: "21px", fontWeight: 400,
    color: dark ? "var(--helm-white)" : "var(--helm-dark-green)",
  }}>
    <div style={{
      width: 24, height: 24, flexShrink: 0, borderRadius: "50%",
      background: dark ? "var(--helm-lemon)" : "var(--helm-dark-green)",
      color: dark ? "var(--helm-dark-green)" : "var(--helm-lemon)",
      display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      <PGIcon name="sparkle" size={14} stroke={2} />
    </div>
    <div style={{ flex: 1, minWidth: 0, paddingTop: 2 }}>{children}</div>
  </div>
);

// CTA button (matches main components.jsx Button styling, slimmer)
const PGButton = ({ variant = "primary", children, onClick, style, compact }) => {
  const variants = {
    primary:        { bg: "var(--helm-chartreuse-2)", fg: "var(--helm-white)", border: "none" },
    soft:           { bg: "var(--helm-soft-chartreuse)", fg: "var(--helm-white)", border: "none" },
    purple:         { bg: "var(--helm-dark-purple)", fg: "var(--helm-white)", border: "none" },
    secondary:      { bg: "transparent", fg: "var(--helm-dark-green)", border: "1px solid var(--helm-dark-green)" },
    ghost:          { bg: "transparent", fg: "var(--helm-dark-green)", border: "none" },
    inverse:        { bg: "var(--helm-dark-green)", fg: "var(--helm-white)", border: "none" },
    lemon:          { bg: "var(--helm-lemon)", fg: "var(--helm-dark-green)", border: "1px solid var(--helm-dark-green)" },
  }[variant];
  return (
    <button onClick={onClick} style={{
      display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8,
      fontFamily: "var(--font-sans)", fontWeight: 700,
      fontSize: compact ? 13 : 15, lineHeight: 1,
      padding: compact ? "10px 16px" : "13px 22px",
      borderRadius: 999,
      background: variants.bg, color: variants.fg, border: variants.border,
      cursor: "pointer", whiteSpace: "nowrap",
      transition: "transform .12s ease",
      ...style,
    }}>{children}</button>
  );
};

// ─────────────────────────────────────────────────────────────────
// FAMILY FACTS — the "what we know about your family" tiles are now
// generated dynamically in pgApplyContext() from PG_HOUSEHOLD + PG_PEOPLE,
// so they always reflect Suzie's real connected accounts + the
// beneficiaries she confirmed.
// ─────────────────────────────────────────────────────────────────

// Group helper for sidebar rendering
const PG_FACT_CATEGORIES = ["People", "Debts & home", "Income & spend", "Milestones ahead"];

// "Invest for growth" helper — for any beneficiary, computes the
// remainder of their share after current draft expenses. This is the
// MEANINGFUL bucket that catches anything not assigned — labeled and
// colored as growth, never "unallocated".
function pgInvestForGrowth(personId, expenses = PG_EXPENSES) {
  const share = PG_TOTAL_BY[personId];
  const used = expenses.filter(e => e.to === personId).reduce((s, e) => s + e.amount, 0);
  return Math.max(0, share - used);
}

// "Why this number" glyph — small, recognizable, opens a tooltip
// "Why this number" glyph — small, recognizable, opens a tooltip
// The tooltip is rendered via a React portal to document.body so it
// escapes any ancestor `overflow: hidden` (e.g. plan rows that need it
// for their delete animation). Position is computed from the glyph's
// bounding rect on open + on scroll/resize.
const PGWhyGlyph = ({ source, size = 14, style, onClick }) => {
  const [open, setOpen] = pgU(false);
  const [pos, setPos] = pgU(null);   // {left, bottom} viewport coords
  const btnRef = pgR(null);

  const updatePos = () => {
    if (!btnRef.current) return;
    const r = btnRef.current.getBoundingClientRect();
    setPos({
      left: r.left + r.width / 2,
      bottom: window.innerHeight - r.top + 6, // 6px gap above the glyph
    });
  };

  pgE(() => {
    if (!open) return;
    updatePos();
    const onScroll = () => updatePos();
    window.addEventListener("scroll", onScroll, true);
    window.addEventListener("resize", onScroll);
    return () => {
      window.removeEventListener("scroll", onScroll, true);
      window.removeEventListener("resize", onScroll);
    };
  }, [open]);

  return (
    <span style={{ display: "inline-flex", ...style }}>
      <button ref={btnRef}
        onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}
        onClick={(e) => {
          if (onClick) { setOpen(false); onClick(e); }
          else setOpen(o => !o);
        }}
        style={{
          width: size + 4, height: size + 4, borderRadius: "50%",
          background: "transparent", border: "1px solid var(--helm-dark-grey)",
          opacity: 0.55, padding: 0, cursor: "pointer", flexShrink: 0,
          display: "flex", alignItems: "center", justifyContent: "center",
          color: "var(--helm-dark-green)",
        }}>
        <PGIcon name="sparkle" size={size - 4} stroke={2} />
      </button>
      {open && source && pos && ReactDOM.createPortal(
        <div style={{
          position: "fixed",
          left: pos.left, bottom: pos.bottom,
          transform: "translateX(-50%)",
          background: "var(--helm-dark-green)", color: "var(--helm-white)",
          padding: "8px 12px", borderRadius: 8,
          fontFamily: "var(--font-sans)", fontSize: 11, lineHeight: 1.4, fontWeight: 500,
          width: 220, zIndex: 1000,
          boxShadow: "0 8px 24px -8px rgba(0,29,0,0.4)",
          whiteSpace: "normal", textAlign: "left",
          pointerEvents: "none",
        }}>{onClick ? <>{source} <span style={{ opacity: 0.65 }}>— click for details</span></> : source}</div>,
        document.body
      )}
    </span>
  );
};

// Estimate flag badge — used on flagged expenses
const PGEstimateFlag = ({ size = "sm", inline }) => (
  <span style={{
    display: "inline-flex", alignItems: "center", gap: 4,
    height: size === "sm" ? 16 : 20,
    padding: size === "sm" ? "0 6px" : "0 8px",
    borderRadius: 999,
    background: "var(--helm-orange)", color: "var(--helm-dark-green)",
    fontFamily: "var(--font-sans)", fontSize: size === "sm" ? 9 : 10, fontWeight: 700,
    letterSpacing: "0.04em", textTransform: "uppercase", whiteSpace: "nowrap",
    ...inline,
  }}>
    <span style={{ width: 4, height: 4, borderRadius: 999, background: "currentColor", opacity: 0.7 }} />
    Estimate
  </span>
);

// View-mode toggle — segmented control used in the main panel header
const PGViewToggle = ({ value, onChange, options }) => (
  <div style={{
    display: "inline-flex", gap: 2, padding: 3,
    background: "var(--helm-ecru)", borderRadius: 999,
  }}>
    {options.map(o => (
      <button key={o.value} onClick={() => onChange(o.value)} style={{
        padding: "6px 12px", borderRadius: 999, border: "none",
        background: value === o.value ? "var(--helm-dark-green)" : "transparent",
        color: value === o.value ? "var(--helm-white)" : "var(--helm-dark-green)",
        fontFamily: "var(--font-sans)", fontWeight: 600, fontSize: 11, cursor: "pointer",
        display: "flex", alignItems: "center", gap: 5,
      }}>
        {o.icon && <PGIcon name={o.icon} size={11} stroke={2} />}
        {o.label}
      </button>
    ))}
  </div>
);

// Invest-for-growth allocation pill — the "what's left grows" callout
const PGGrowthPill = ({ amount, person, size = "md" }) => (
  <div style={{
    display: "flex", alignItems: "center", gap: 8,
    padding: size === "sm" ? "6px 10px" : "10px 14px",
    background: "var(--helm-soft-chartreuse)", color: "var(--helm-white)",
    borderRadius: 10,
    fontFamily: "var(--font-sans)",
  }}>
    <div style={{
      width: size === "sm" ? 18 : 22, height: size === "sm" ? 18 : 22,
      borderRadius: "50%", background: "var(--helm-white)",
      color: "var(--helm-chartreuse-2)",
      display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      <PGIcon name="sparkle" size={size === "sm" ? 11 : 13} stroke={2.4} />
    </div>
    <div style={{ flex: 1, minWidth: 0 }}>
      <div style={{ fontSize: size === "sm" ? 10 : 11, fontWeight: 700,
        letterSpacing: "0.06em", textTransform: "uppercase", opacity: 0.85 }}>
        Invest for growth{person ? ` · ${person}` : ""}
      </div>
      <div style={{ fontSize: size === "sm" ? 11 : 12, opacity: 0.9, marginTop: 1 }}>
        Anything not assigned grows for the future
      </div>
    </div>
    <div style={{
      fontFamily: "var(--font-sans)", fontWeight: 700,
      fontSize: size === "sm" ? 13 : 16, letterSpacing: "-0.01em",
      fontVariantNumeric: "tabular-nums",
    }}>{pgK(amount)}</div>
  </div>
);

// "What we know" fact card — used in band & sidebar
const PGFactCard = ({ fact, layout = "tile", editable = true }) => {
  const isHard = fact.confidence === "hard";
  const isEstimate = fact.confidence === "estimate";
  return (
    <div style={{
      display: layout === "row" ? "flex" : "flex",
      flexDirection: layout === "row" ? "row" : "column",
      alignItems: layout === "row" ? "center" : "stretch",
      gap: layout === "row" ? 10 : 4,
      padding: layout === "row" ? "10px 12px" : 12,
      background: "var(--helm-white)", borderRadius: 10,
      border: "1px solid var(--helm-dark-ecru)",
      minWidth: layout === "tile" ? 160 : 0,
      flex: layout === "tile" ? "0 0 162px" : "1",
      position: "relative",
    }}>
      <div style={{
        display: "flex", alignItems: "center", gap: 6,
        flex: layout === "row" ? "0 0 auto" : "initial",
      }}>
        <div style={{
          width: 22, height: 22, borderRadius: 6,
          background: "var(--helm-ecru)",
          display: "flex", alignItems: "center", justifyContent: "center",
          flexShrink: 0,
        }}>
          <PGIcon name={fact.icon} size={12} stroke={1.6} />
        </div>
        {layout === "tile" && (
          <span style={{ fontSize: 11, fontWeight: 600, letterSpacing: "-0.005em" }}>{fact.label}</span>
        )}
      </div>
      <div style={{ flex: layout === "row" ? 1 : "initial", minWidth: 0 }}>
        {layout === "row" && (
          <div style={{ fontSize: 12, fontWeight: 600 }}>{fact.label}</div>
        )}
        <div style={{
          fontFamily: "var(--font-sans)",
          fontSize: layout === "row" ? 13 : 17, fontWeight: 700,
          letterSpacing: "-0.01em",
          fontVariantNumeric: "tabular-nums",
        }}>{fact.value}</div>
        <div style={{ fontSize: 10, opacity: 0.6 }}>{fact.sub}</div>
      </div>
      {(isHard || isEstimate) && (
        <div style={{
          position: "absolute", top: 6, right: 6,
          display: "inline-flex", alignItems: "center", gap: 3,
          padding: "1px 5px", borderRadius: 999,
          background: isHard ? "var(--helm-soft-chartreuse)" : "var(--helm-orange)",
          color: "var(--helm-dark-green)",
          fontSize: 8, fontWeight: 700, letterSpacing: "0.06em", textTransform: "uppercase",
        }}>{isHard ? "Synced" : "Estimate"}</div>
      )}
    </div>
  );
};

// "What we know about your family" — horizontal scrolling band
const PGWhatWeKnowBand = ({ collapsible = true }) => {
  const [open, setOpen] = pgU(true);
  return (
    <div style={{ background: "var(--helm-ecru)", borderRadius: 14, padding: 12 }}>
      <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: open ? 10 : 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <PGIcon name="sparkle" size={13} stroke={2} />
          <span style={{ fontSize: 11, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase" }}>
            What we know about your family
          </span>
          <span style={{ fontSize: 11, opacity: 0.55 }}>
            · pulled from your accounts, profile, and what you've told us
          </span>
        </div>
        {collapsible && (
          <button onClick={() => setOpen(o => !o)} style={{
            background: "transparent", border: "none", padding: "4px 10px",
            fontFamily: "var(--font-sans)", fontSize: 11, fontWeight: 600,
            color: "var(--helm-chartreuse-2)", cursor: "pointer",
          }}>
            {open ? "Hide" : "Show all"}
          </button>
        )}
      </div>
      {open && (
        <div style={{ display: "flex", gap: 8, overflowX: "auto", paddingBottom: 2 }}>
          {PG_FAMILY_FACTS.map(f => <PGFactCard key={f.id} fact={f} layout="tile" />)}
          <button style={{
            flex: "0 0 162px", background: "transparent",
            border: "1.5px dashed var(--helm-dark-ecru)", borderRadius: 10,
            display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
            color: "var(--helm-dark-grey)", fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 600,
            padding: 12, cursor: "pointer", minHeight: 76,
          }}>
            <PGIcon name="plus" size={14} stroke={2} style={{ marginBottom: 4 }} />
            Add something else
          </button>
        </div>
      )}
    </div>
  );
};

// "What we know" — sidebar version (grouped by category)
const PGWhatWeKnowSidebar = ({ width = 240 }) => (
  <div style={{
    width, flexShrink: 0,
    padding: 16, background: "var(--helm-ecru)",
    borderRadius: 14, overflow: "auto",
  }}>
    <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 12 }}>
      <PGIcon name="sparkle" size={13} stroke={2} />
      <span style={{ fontSize: 10, fontWeight: 700, letterSpacing: "0.1em", textTransform: "uppercase" }}>
        What we know
      </span>
    </div>
    {PG_FACT_CATEGORIES.map(cat => (
      <div key={cat} style={{ marginBottom: 14 }}>
        <div style={{
          fontSize: 9, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase",
          opacity: 0.55, marginBottom: 6,
        }}>{cat}</div>
        <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
          {PG_FAMILY_FACTS.filter(f => f.category === cat).map(f => (
            <PGFactCard key={f.id} fact={f} layout="row" />
          ))}
        </div>
      </div>
    ))}
    <button style={{
      width: "100%", padding: 10, background: "transparent",
      border: "1.5px dashed var(--helm-dark-ecru)", borderRadius: 10,
      fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 600,
      color: "var(--helm-dark-grey)", cursor: "pointer",
      display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
    }}>
      <PGIcon name="plus" size={12} stroke={2} /> Tell us something else
    </button>
  </div>
);

// Helm guide right rail — chat + current question, used in EVERY V2 treatment
const PGHelmRail = ({ title = "Helm guide", subtitle, messages = [], currentQuestion, sticky }) => (
  <div style={{
    width: 380, flexShrink: 0,
    background: "var(--helm-ecru)", borderLeft: "1px solid var(--helm-dark-ecru)",
    display: "flex", flexDirection: "column", minHeight: 0,
  }}>
    <div style={{
      padding: "14px 18px", borderBottom: "1px solid var(--helm-dark-ecru)",
      display: "flex", alignItems: "center", gap: 10, flexShrink: 0,
    }}>
      <div style={{
        width: 28, height: 28, borderRadius: "50%",
        background: "var(--helm-dark-green)", color: "var(--helm-lemon)",
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        <PGIcon name="sparkle" size={14} stroke={2} />
      </div>
      <div style={{ flex: 1 }}>
        <div style={{ fontSize: 13, fontWeight: 600 }}>{title}</div>
        {subtitle && <div style={{ fontSize: 11, opacity: 0.55 }}>{subtitle}</div>}
      </div>
    </div>
    <div style={{
      flex: 1, padding: 16, overflow: "auto",
      display: "flex", flexDirection: "column", gap: 12, minHeight: 0,
    }}>
      {messages.map((m, i) => m.from === "user" ? (
        <div key={i} style={{
          alignSelf: "flex-end", maxWidth: "92%",
          padding: "8px 12px", borderRadius: "14px 14px 4px 14px",
          background: "var(--helm-dark-green)", color: "var(--helm-white)",
          fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 500,
        }}>{m.text}</div>
      ) : (
        <PGAIBubble key={i}>{m.text}</PGAIBubble>
      ))}
      {currentQuestion}
    </div>
    <div style={{
      padding: 14, borderTop: "1px solid var(--helm-dark-ecru)", background: "var(--helm-white)",
      flexShrink: 0,
    }}>
      <div style={{
        display: "flex", alignItems: "center", gap: 8, padding: "8px 12px",
        background: "var(--helm-ecru)", borderRadius: 999,
      }}>
        <PGIcon name="sparkle" size={14} style={{ opacity: 0.5 }} />
        <span style={{ flex: 1, fontSize: 12, opacity: 0.55 }}>Or just type — "swap college to private"…</span>
        <PGIcon name="mic" size={14} stroke={1.6} style={{ opacity: 0.55 }} />
      </div>
    </div>
  </div>
);

// Export to window
Object.assign(window, {
  // Context API — what the main App calls
  pgApplyContext, PG_EXPENSE_TEMPLATES, PG_PALETTE,
  // Initial empty bindings — pgApplyContext fills these in
  PG_POLICIES, PG_PEOPLE, PG_TOTAL_BY, PG_GRAND_TOTAL, PG_HOUSEHOLD,
  PG_EXPENSES, PG_GUIDE_ORDER, PG_PERSON_TINTS, PG_FAMILY_FACTS,
  // Static helpers
  pgDraftFor, PG_HORIZONS, PG_CATEGORY_TINTS, PG_FACT_CATEGORIES,
  // Primitives
  PGArtboard, PGRationale, PGAvatar, PGCarrier, PGMoney, pgK, PGIcon,
  PGSourcePill, PGHorizonChip, PGRationaleNote, PGAmountField, PGAllocBar,
  PGAIBubble, PGButton, pgInitials,
  PGWhyGlyph, PGEstimateFlag, PGViewToggle, PGGrowthPill,
  PGFactCard, PGWhatWeKnowBand, PGWhatWeKnowSidebar, PGHelmRail,
  pgInvestForGrowth,
});
