document.addEventListener("DOMContentLoaded", function () { const calculators = document.querySelectorAll("[data-fm-hero-calculator]"); if (!calculators.length) return; calculators.forEach(function (root) { const currentInput = root.querySelector("[data-fm-current]"); const monthlyInput = root.querySelector("[data-fm-monthly]"); const ageInput = root.querySelector("[data-fm-age]"); const returnInput = root.querySelector("[data-fm-return]"); const situationSelect = root.querySelector("[data-fm-situation]"); const stateSelect = root.querySelector("[data-fm-state]"); const currentValue = root.querySelector("[data-fm-current-value]"); const monthlyValue = root.querySelector("[data-fm-monthly-value]"); const ageValue = root.querySelector("[data-fm-age-value]"); const returnValue = root.querySelector("[data-fm-return-value]"); // Headline outcome (with / without sidecar) + tax difference const withBalanceEl = root.querySelector("[data-fm-with-balance]"); const withoutBalanceEl = root.querySelector("[data-fm-without-balance]"); const taxDiffEl = root.querySelector("[data-fm-tax-diff]"); // Roth IRA Sidecar (advanced, on demand) const advConversionEl = root.querySelector("[data-fm-adv-conversion]"); const advTaxEl = root.querySelector("[data-fm-adv-tax]"); const advTaxLabelEl = root.querySelector("[data-fm-adv-tax-label]"); const advSidecarEl = root.querySelector("[data-fm-adv-sidecar]"); const advToggle = root.querySelector("[data-fm-advanced-toggle]"); const advBody = root.querySelector("[data-fm-advanced-body]"); // Chart elements const gridGroup = root.querySelector(".fm-grid-lines"); const ageLabelsGroup = root.querySelector(".fm-age-labels"); const taxAreaEl = root.querySelector(".fm-tax-area"); const withoutPathEl = root.querySelector(".fm-without-path"); const withPathEl = root.querySelector(".fm-with-path"); const conversionLine = root.querySelector(".fm-conversion-line"); const conversionDot = root.querySelector(".fm-conversion-dot"); const conversionLabel = root.querySelector(".fm-conversion-label"); const withFinalCircle = root.querySelector(".fm-with-final"); const withoutFinalCircle = root.querySelector(".fm-without-final"); const taxBracket = root.querySelector(".fm-tax-bracket"); const taxBracketLabel = root.querySelector(".fm-tax-bracket-label"); if (!currentInput || !monthlyInput || !ageInput || !returnInput || !situationSelect || !stateSelect) return; const FEDERAL_DEDUCTION_SINGLE_2026 = 16100; const WORKING_INCOME_AT_18 = 34000; const STUDENT_INCOME_AT_24 = 60000; const AGE_65_INCOME = 72500; const MAX_MONTHLY_CONTRIBUTION = 416.66; const federalBrackets = [ { min: 0, max: 12400, rate: 0.10 }, { min: 12400, max: 50400, rate: 0.12 }, { min: 50400, max: 105700, rate: 0.22 }, { min: 105700, max: 201775, rate: 0.24 }, { min: 201775, max: 256225, rate: 0.32 }, { min: 256225, max: 640600, rate: 0.35 }, { min: 640600, max: 999999999, rate: 0.37 } ]; const stateTaxData = { "Alabama": { "deduction": 3000, "brackets": [ { "min": 0, "max": 500, "rate": 0.02 }, { "min": 500, "max": 3000, "rate": 0.04 }, { "min": 3000, "max": 999999999, "rate": 0.05 } ] }, "Alaska": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] }, "Arizona": { "deduction": 14600, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.025 } ] }, "Arkansas": { "deduction": 2340, "brackets": [ { "min": 0, "max": 5100, "rate": 0.02 }, { "min": 5100, "max": 10300, "rate": 0.04 }, { "min": 10300, "max": 999999999, "rate": 0.039 } ] }, "California": { "deduction": 5363, "brackets": [ { "min": 0, "max": 10756, "rate": 0.01 }, { "min": 10756, "max": 25499, "rate": 0.02 }, { "min": 25499, "max": 40245, "rate": 0.04 }, { "min": 40245, "max": 55866, "rate": 0.06 }, { "min": 55866, "max": 70606, "rate": 0.08 }, { "min": 70606, "max": 360659, "rate": 0.093 }, { "min": 360659, "max": 432787, "rate": 0.103 }, { "min": 432787, "max": 721314, "rate": 0.113 }, { "min": 721314, "max": 999999999, "rate": 0.133 } ] }, "Colorado": { "deduction": 14600, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.044 } ] }, "Connecticut": { "deduction": 15000, "brackets": [ { "min": 0, "max": 10000, "rate": 0.02 }, { "min": 10000, "max": 50000, "rate": 0.045 }, { "min": 50000, "max": 100000, "rate": 0.055 }, { "min": 100000, "max": 200000, "rate": 0.06 }, { "min": 200000, "max": 250000, "rate": 0.065 }, { "min": 250000, "max": 500000, "rate": 0.069 }, { "min": 500000, "max": 999999999, "rate": 0.0699 } ] }, "Delaware": { "deduction": 3250, "brackets": [ { "min": 0, "max": 2000, "rate": 0.0 }, { "min": 2000, "max": 5000, "rate": 0.022 }, { "min": 5000, "max": 10000, "rate": 0.039 }, { "min": 10000, "max": 20000, "rate": 0.048 }, { "min": 20000, "max": 25000, "rate": 0.052 }, { "min": 25000, "max": 60000, "rate": 0.055 }, { "min": 60000, "max": 999999999, "rate": 0.066 } ] }, "District of Columbia": { "deduction": 0, "brackets": [ { "min": 0, "max": 10000, "rate": 0.04 }, { "min": 10000, "max": 40000, "rate": 0.06 }, { "min": 40000, "max": 60000, "rate": 0.065 }, { "min": 60000, "max": 350000, "rate": 0.085 }, { "min": 350000, "max": 1000000, "rate": 0.0925 }, { "min": 1000000, "max": 999999999, "rate": 0.1075 } ] }, "Florida": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] }, "Georgia": { "deduction": 12000, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0519 } ] }, "Hawaii": { "deduction": 2200, "brackets": [ { "min": 0, "max": 2400, "rate": 0.014 }, { "min": 2400, "max": 4800, "rate": 0.032 }, { "min": 4800, "max": 9600, "rate": 0.055 }, { "min": 9600, "max": 14400, "rate": 0.064 }, { "min": 14400, "max": 19200, "rate": 0.068 }, { "min": 19200, "max": 24000, "rate": 0.072 }, { "min": 24000, "max": 48000, "rate": 0.076 }, { "min": 48000, "max": 150000, "rate": 0.079 }, { "min": 150000, "max": 175000, "rate": 0.0825 }, { "min": 175000, "max": 200000, "rate": 0.09 }, { "min": 200000, "max": 999999999, "rate": 0.11 } ] }, "Idaho": { "deduction": 14600, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.053 } ] }, "Illinois": { "deduction": 2775, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0495 } ] }, "Indiana": { "deduction": 1000, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0295 } ] }, "Iowa": { "deduction": 2280, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.038 } ] }, "Kansas": { "deduction": 3500, "brackets": [ { "min": 0, "max": 15000, "rate": 0.031 }, { "min": 15000, "max": 30000, "rate": 0.0525 }, { "min": 30000, "max": 999999999, "rate": 0.0558 } ] }, "Kentucky": { "deduction": 3160, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.035 } ] }, "Louisiana": { "deduction": 4500, "brackets": [ { "min": 0, "max": 12500, "rate": 0.0185 }, { "min": 12500, "max": 50000, "rate": 0.035 }, { "min": 50000, "max": 999999999, "rate": 0.0425 } ] }, "Maine": { "deduction": 14600, "brackets": [ { "min": 0, "max": 24500, "rate": 0.058 }, { "min": 24500, "max": 58050, "rate": 0.0675 }, { "min": 58050, "max": 999999999, "rate": 0.0715 } ] }, "Maryland": { "deduction": 2400, "brackets": [ { "min": 0, "max": 1000, "rate": 0.02 }, { "min": 1000, "max": 2000, "rate": 0.03 }, { "min": 2000, "max": 3000, "rate": 0.04 }, { "min": 3000, "max": 100000, "rate": 0.0475 }, { "min": 100000, "max": 125000, "rate": 0.05 }, { "min": 125000, "max": 150000, "rate": 0.0525 }, { "min": 150000, "max": 250000, "rate": 0.055 }, { "min": 250000, "max": 999999999, "rate": 0.065 } ] }, "Massachusetts": { "deduction": 0, "brackets": [ { "min": 0, "max": 1000000, "rate": 0.05 }, { "min": 1000000, "max": 999999999, "rate": 0.09 } ] }, "Michigan": { "deduction": 5600, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0425 } ] }, "Minnesota": { "deduction": 14575, "brackets": [ { "min": 0, "max": 31690, "rate": 0.0535 }, { "min": 31690, "max": 104090, "rate": 0.068 }, { "min": 104090, "max": 193240, "rate": 0.0785 }, { "min": 193240, "max": 999999999, "rate": 0.0985 } ] }, "Mississippi": { "deduction": 6000, "brackets": [ { "min": 0, "max": 10000, "rate": 0.0 }, { "min": 10000, "max": 999999999, "rate": 0.04 } ] }, "Missouri": { "deduction": 12950, "brackets": [ { "min": 0, "max": 1207, "rate": 0.0 }, { "min": 1207, "max": 2414, "rate": 0.02 }, { "min": 2414, "max": 3621, "rate": 0.025 }, { "min": 3621, "max": 4828, "rate": 0.03 }, { "min": 4828, "max": 6035, "rate": 0.035 }, { "min": 6035, "max": 7242, "rate": 0.04 }, { "min": 7242, "max": 8449, "rate": 0.045 }, { "min": 8449, "max": 999999999, "rate": 0.047 } ] }, "Montana": { "deduction": 14600, "brackets": [ { "min": 0, "max": 20500, "rate": 0.047 }, { "min": 20500, "max": 999999999, "rate": 0.0565 } ] }, "Nebraska": { "deduction": 7900, "brackets": [ { "min": 0, "max": 3700, "rate": 0.0246 }, { "min": 3700, "max": 22170, "rate": 0.0351 }, { "min": 22170, "max": 35730, "rate": 0.0501 }, { "min": 35730, "max": 999999999, "rate": 0.0464 } ] }, "Nevada": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] }, "New Hampshire": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] }, "New Jersey": { "deduction": 1000, "brackets": [ { "min": 0, "max": 20000, "rate": 0.014 }, { "min": 20000, "max": 35000, "rate": 0.0175 }, { "min": 35000, "max": 40000, "rate": 0.035 }, { "min": 40000, "max": 75000, "rate": 0.0553 }, { "min": 75000, "max": 500000, "rate": 0.0637 }, { "min": 500000, "max": 999999999, "rate": 0.1075 } ] }, "New Mexico": { "deduction": 14600, "brackets": [ { "min": 0, "max": 5500, "rate": 0.017 }, { "min": 5500, "max": 11000, "rate": 0.032 }, { "min": 11000, "max": 16000, "rate": 0.047 }, { "min": 16000, "max": 210000, "rate": 0.049 }, { "min": 210000, "max": 999999999, "rate": 0.059 } ] }, "New York": { "deduction": 8000, "brackets": [ { "min": 0, "max": 17150, "rate": 0.04 }, { "min": 17150, "max": 23600, "rate": 0.045 }, { "min": 23600, "max": 27900, "rate": 0.0525 }, { "min": 27900, "max": 161550, "rate": 0.055 }, { "min": 161550, "max": 323200, "rate": 0.06 }, { "min": 323200, "max": 2155350, "rate": 0.0685 }, { "min": 2155350, "max": 25000000, "rate": 0.0965 }, { "min": 25000000, "max": 999999999, "rate": 0.109 } ] }, "North Carolina": { "deduction": 12750, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0399 } ] }, "North Dakota": { "deduction": 14600, "brackets": [ { "min": 0, "max": 44725, "rate": 0.0 }, { "min": 44725, "max": 225975, "rate": 0.019 }, { "min": 225975, "max": 999999999, "rate": 0.025 } ] }, "Ohio": { "deduction": 0, "brackets": [ { "min": 0, "max": 26050, "rate": 0.0 }, { "min": 26050, "max": 100000, "rate": 0.0275 }, { "min": 100000, "max": 999999999, "rate": 0.035 } ] }, "Oklahoma": { "deduction": 6350, "brackets": [ { "min": 0, "max": 1000, "rate": 0.0025 }, { "min": 1000, "max": 2500, "rate": 0.0075 }, { "min": 2500, "max": 3750, "rate": 0.0175 }, { "min": 3750, "max": 4900, "rate": 0.0275 }, { "min": 4900, "max": 7200, "rate": 0.0375 }, { "min": 7200, "max": 999999999, "rate": 0.045 } ] }, "Oregon": { "deduction": 2745, "brackets": [ { "min": 0, "max": 18400, "rate": 0.0475 }, { "min": 18400, "max": 46200, "rate": 0.0675 }, { "min": 46200, "max": 250000, "rate": 0.0875 }, { "min": 250000, "max": 999999999, "rate": 0.099 } ] }, "Pennsylvania": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0307 } ] }, "Rhode Island": { "deduction": 10550, "brackets": [ { "min": 0, "max": 77450, "rate": 0.0375 }, { "min": 77450, "max": 176050, "rate": 0.0475 }, { "min": 176050, "max": 999999999, "rate": 0.0599 } ] }, "South Carolina": { "deduction": 14600, "brackets": [ { "min": 0, "max": 3460, "rate": 0.0 }, { "min": 3460, "max": 17330, "rate": 0.03 }, { "min": 17330, "max": 999999999, "rate": 0.064 } ] }, "South Dakota": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] }, "Tennessee": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] }, "Texas": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] }, "Utah": { "deduction": 886, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.045 } ] }, "Vermont": { "deduction": 6500, "brackets": [ { "min": 0, "max": 45400, "rate": 0.0335 }, { "min": 45400, "max": 110050, "rate": 0.066 }, { "min": 110050, "max": 227050, "rate": 0.076 }, { "min": 227050, "max": 999999999, "rate": 0.0875 } ] }, "Virginia": { "deduction": 8500, "brackets": [ { "min": 0, "max": 3000, "rate": 0.02 }, { "min": 3000, "max": 5000, "rate": 0.03 }, { "min": 5000, "max": 17000, "rate": 0.05 }, { "min": 17000, "max": 999999999, "rate": 0.0575 } ] }, "Washington": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] }, "West Virginia": { "deduction": 0, "brackets": [ { "min": 0, "max": 10000, "rate": 0.0236 }, { "min": 10000, "max": 25000, "rate": 0.0315 }, { "min": 25000, "max": 40000, "rate": 0.0354 }, { "min": 40000, "max": 60000, "rate": 0.0472 }, { "min": 60000, "max": 999999999, "rate": 0.0482 } ] }, "Wisconsin": { "deduction": 13150, "brackets": [ { "min": 0, "max": 14320, "rate": 0.035 }, { "min": 14320, "max": 28640, "rate": 0.044 }, { "min": 28640, "max": 315310, "rate": 0.053 }, { "min": 315310, "max": 999999999, "rate": 0.0765 } ] }, "Wyoming": { "deduction": 0, "brackets": [ { "min": 0, "max": 999999999, "rate": 0.0 } ] } }; const plot = { left: 46, right: 520, top: 30, bottom: 206 }; function money(value) { return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(Math.round(value || 0)); } function compactMoney(value) { if (value >= 1000000) return "$" + (value / 1000000).toFixed(1) + "M"; if (value >= 1000) return "$" + Math.round(value / 1000) + "K"; return money(value); } function percent(value) { if (!Number.isFinite(value)) return "0%"; return (value * 100).toFixed(1).replace(".0", "") + "%"; } function path(points) { return points.map(function (point, index) { return (index === 0 ? "M" : "L") + " " + point.x.toFixed(1) + " " + point.y.toFixed(1); }).join(" "); } function createSvgElement(tag, attributes) { const element = document.createElementNS("http://www.w3.org/2000/svg", tag); Object.keys(attributes || {}).forEach(function (key) { element.setAttribute(key, attributes[key]); }); return element; } function setCirclePosition(circle, point) { if (!circle || !point) return; circle.setAttribute("cx", point.x); circle.setAttribute("cy", point.y); } function updateRangeBackground(input) { const min = Number(input.min || 0); const max = Number(input.max || 100); const value = Number(input.value || 0); const progress = ((value - min) / Math.max(1, max - min)) * 100; input.style.background = "linear-gradient(to right, #0B1F3A 0%, #0B1F3A " + progress + "%, #E2E8F0 " + progress + "%, #E2E8F0 100%)"; } function calculateBracketTax(taxableIncome, brackets) { const income = Math.max(0, taxableIncome || 0); let tax = 0; brackets.forEach(function (bracket) { if (income > bracket.min) { const amountInBracket = Math.min(income, bracket.max) - bracket.min; tax += Math.max(0, amountInBracket) * bracket.rate; } }); return tax; } function getFutureValue(currentBalance, monthlyContribution, annualReturn, years) { const annualContribution = monthlyContribution * 12; let balance = currentBalance; for (let year = 0; year < years; year++) { balance = balance * (1 + annualReturn) + annualContribution * Math.pow(1 + annualReturn, 0.5); } return balance; } function calculateSidecarMonthlySavings(targetTax, annualReturn, monthsUntilConversion) { if (!targetTax || targetTax <= 0 || !monthsUntilConversion || monthsUntilConversion <= 0) return 0; const monthlyRate = Math.pow(1 + annualReturn, 1 / 12) - 1; if (monthlyRate <= 0) return targetTax / monthsUntilConversion; return targetTax * monthlyRate / (Math.pow(1 + monthlyRate, monthsUntilConversion) - 1); } // --------------------------------------------------------------------- // CORE TAX MODEL — unchanged. Scenario A (convert / "with sidecar") and // Scenario B2 (hold + 20-year drawdown / "without sidecar") use the same // verified incremental method. // --------------------------------------------------------------------- function calculateModel() { const currentBalance = Number(currentInput.value || 0); const monthlyContribution = Math.min(Number(monthlyInput.value || 0), MAX_MONTHLY_CONTRIBUTION); const childAge = Number(ageInput.value || 0); const annualReturn = Number(returnInput.value || 0) / 100; const situation = situationSelect.value || "working"; const state = stateSelect.value || "New York"; const stateData = stateTaxData[state] || stateTaxData["New York"]; const isStudent = situation === "student"; const conversionAge = isStudent ? 24 : 18; const earnedIncomeAtConversion = isStudent ? STUDENT_INCOME_AT_24 : WORKING_INCOME_AT_18; const yearsTo18 = Math.max(0, 18 - childAge); const yearsToConversion = Math.max(0, conversionAge - childAge); const monthsToConversion = yearsToConversion * 12; const balanceAt18 = getFutureValue(currentBalance, monthlyContribution, annualReturn, yearsTo18); const cumulativeBasisAt18 = monthlyContribution * yearsTo18 * 12; const balanceAtConversion = isStudent ? balanceAt18 * Math.pow(1 + annualReturn, 6) : balanceAt18; const taxableGainAtConversion = Math.max(0, balanceAtConversion - cumulativeBasisAt18); const federalTaxableBeforeConversion = Math.max(0, earnedIncomeAtConversion - FEDERAL_DEDUCTION_SINGLE_2026); const federalTaxableWithConversion = Math.max(0, earnedIncomeAtConversion + taxableGainAtConversion - FEDERAL_DEDUCTION_SINGLE_2026); const federalTaxWithoutConversion = calculateBracketTax(federalTaxableBeforeConversion, federalBrackets); const federalTaxWithConversion = calculateBracketTax(federalTaxableWithConversion, federalBrackets); const federalConversionTax = Math.max(0, federalTaxWithConversion - federalTaxWithoutConversion); const stateTaxableBeforeConversion = Math.max(0, earnedIncomeAtConversion - stateData.deduction); const stateTaxableWithConversion = Math.max(0, earnedIncomeAtConversion + taxableGainAtConversion - stateData.deduction); const stateTaxWithoutConversion = calculateBracketTax(stateTaxableBeforeConversion, stateData.brackets); const stateTaxWithConversion = calculateBracketTax(stateTaxableWithConversion, stateData.brackets); const stateConversionTax = Math.max(0, stateTaxWithConversion - stateTaxWithoutConversion); const totalConversionTax = federalConversionTax + stateConversionTax; const effectiveConversionRate = taxableGainAtConversion > 0 ? totalConversionTax / taxableGainAtConversion : 0; const rothBalanceAt65 = balanceAtConversion * Math.pow(1 + annualReturn, 65 - conversionAge); const traditionalBalanceAt65 = balanceAt18 * Math.pow(1 + annualReturn, 47); const taxableGainsAt65 = Math.max(0, traditionalBalanceAt65 - cumulativeBasisAt18); const annualWithdrawal = traditionalBalanceAt65 / 20; const annualTaxableWithdrawal = taxableGainsAt65 / 20; const federalB2TaxableIncome = Math.max(0, AGE_65_INCOME + annualTaxableWithdrawal - FEDERAL_DEDUCTION_SINGLE_2026); const federalB2TaxableIncomeWithoutWithdrawal = Math.max(0, AGE_65_INCOME - FEDERAL_DEDUCTION_SINGLE_2026); const federalB2TaxPerYear = Math.max(0, calculateBracketTax(federalB2TaxableIncome, federalBrackets) - calculateBracketTax(federalB2TaxableIncomeWithoutWithdrawal, federalBrackets)); const stateB2TaxableIncome = Math.max(0, AGE_65_INCOME + annualTaxableWithdrawal - stateData.deduction); const stateB2TaxableIncomeWithoutWithdrawal = Math.max(0, AGE_65_INCOME - stateData.deduction); const stateB2TaxPerYear = Math.max(0, calculateBracketTax(stateB2TaxableIncome, stateData.brackets) - calculateBracketTax(stateB2TaxableIncomeWithoutWithdrawal, stateData.brackets)); const totalB2TaxOver20Years = (federalB2TaxPerYear + stateB2TaxPerYear) * 20; const b2NetSpendable = traditionalBalanceAt65 - totalB2TaxOver20Years; const scenarioAAdvantageOverB2 = rothBalanceAt65 - b2NetSpendable; const sidecarMonthlySavings = calculateSidecarMonthlySavings(totalConversionTax, annualReturn, monthsToConversion); return { currentBalance: currentBalance, monthlyContribution: monthlyContribution, childAge: childAge, annualReturn: annualReturn, situation: situation, state: state, conversionAge: conversionAge, yearsToConversion: yearsToConversion, monthsToConversion: monthsToConversion, balanceAt18: balanceAt18, balanceAtConversion: balanceAtConversion, taxableGainAtConversion: taxableGainAtConversion, totalConversionTax: totalConversionTax, effectiveConversionRate: effectiveConversionRate, sidecarMonthlySavings: sidecarMonthlySavings, rothBalanceAt65: rothBalanceAt65, traditionalBalanceAt65: traditionalBalanceAt65, totalB2TaxOver20Years: totalB2TaxOver20Years, b2NetSpendable: b2NetSpendable, scenarioAAdvantageOverB2: scenarioAAdvantageOverB2 }; } // Single account-balance trajectory: contributions until 18, then growth only. // Both strategies hold the same pot until 65 — they only differ in the // after-tax amount that is actually spendable at 65. function balanceAtAge(model, age) { if (age <= 18) { return getFutureValue(model.currentBalance, model.monthlyContribution, model.annualReturn, Math.max(0, age - model.childAge)); } return model.balanceAt18 * Math.pow(1 + model.annualReturn, age - 18); } function renderChart(model) { const startAge = model.childAge; const ages = []; for (let a = startAge; a <= 65; a++) ages.push(a); // With sidecar = full balance grows tax-free, spendable in full at 65. // Without sidecar = same pot until 65, then reduced by the drawdown tax. const withVals = ages.map(function (a) { return a >= 65 ? model.rothBalanceAt65 : balanceAtAge(model, a); }); const withoutVals = ages.map(function (a) { return a >= 65 ? model.b2NetSpendable : balanceAtAge(model, a); }); let peak = 1; withVals.forEach(function (v) { if (v > peak) peak = v; }); const maxY = peak * 1.16; function xFor(age) { return plot.left + ((age - startAge) / Math.max(1, 65 - startAge)) * (plot.right - plot.left); } function yFor(value) { return plot.bottom - (value / maxY) * (plot.bottom - plot.top); } const withPts = ages.map(function (a, i) { return { age: a, x: xFor(a), y: yFor(withVals[i]) }; }); const withoutPts = ages.map(function (a, i) { return { age: a, x: xFor(a), y: yFor(withoutVals[i]) }; }); const withPath = path(withPts); const withoutPath = path(withoutPts); // Tax area = region between the two lines (only opens up at age 65). const areaPath = withPath + " " + ("L " + withoutPts[withoutPts.length - 1].x.toFixed(1) + " " + withoutPts[withoutPts.length - 1].y.toFixed(1)) + " " + withoutPts.slice().reverse().map(function (p) { return "L " + p.x.toFixed(1) + " " + p.y.toFixed(1); }).join(" ") + " Z"; // Grid gridGroup.innerHTML = ""; const gridYs = [plot.top, plot.top + (plot.bottom - plot.top) * 0.25, plot.top + (plot.bottom - plot.top) * 0.5, plot.top + (plot.bottom - plot.top) * 0.75, plot.bottom]; gridYs.forEach(function (y, index) { const group = createSvgElement("g"); group.appendChild(createSvgElement("line", { x1: plot.left, x2: plot.right, y1: y, y2: y, stroke: "#E2E8F0", "stroke-width": "1", "stroke-dasharray": index === gridYs.length - 1 ? "0" : "4 6" })); if (index === 0) { const t = createSvgElement("text", { x: 10, y: y + 4, fill: "#94A3B8", "font-size": "10", "font-weight": "600" }); t.textContent = compactMoney(maxY); group.appendChild(t); } if (index === gridYs.length - 1) { const t = createSvgElement("text", { x: 22, y: y - 6, fill: "#94A3B8", "font-size": "10", "font-weight": "600" }); t.textContent = "$0"; group.appendChild(t); } gridGroup.appendChild(group); }); taxAreaEl.setAttribute("d", areaPath); withoutPathEl.setAttribute("d", withoutPath); withPathEl.setAttribute("d", withPath); // Conversion marker sits on the (shared) growth curve. const convPoint = { age: model.conversionAge, x: xFor(model.conversionAge), y: yFor(balanceAtAge(model, model.conversionAge)) }; conversionLine.setAttribute("x1", convPoint.x); conversionLine.setAttribute("x2", convPoint.x); setCirclePosition(conversionDot, convPoint); conversionLabel.textContent = "Convert · age " + model.conversionAge; conversionLabel.setAttribute("x", Math.min(convPoint.x + 9, plot.right - 70)); conversionLabel.setAttribute("y", Math.max(20, convPoint.y - 12)); // End points at 65 const withEnd = withPts[withPts.length - 1]; const withoutEnd = withoutPts[withoutPts.length - 1]; setCirclePosition(withFinalCircle, withEnd); setCirclePosition(withoutFinalCircle, withoutEnd); // Tax bracket between the two outcomes at 65 taxBracket.setAttribute("x1", withEnd.x); taxBracket.setAttribute("x2", withEnd.x); taxBracket.setAttribute("y1", withEnd.y); taxBracket.setAttribute("y2", withoutEnd.y); taxBracketLabel.textContent = "Tax " + compactMoney(model.totalB2TaxOver20Years); taxBracketLabel.setAttribute("x", withEnd.x - 8); taxBracketLabel.setAttribute("y", (withEnd.y + withoutEnd.y) / 2 + 3); // Age ticks ageLabelsGroup.innerHTML = ""; const ticks = Array.from(new Set([startAge, model.conversionAge, Math.round((startAge + 65) / 2), 65])) .filter(function (a) { return a >= startAge && a <= 65; }) .sort(function (a, b) { return a - b; }); ticks.forEach(function (age) { const t = createSvgElement("text", { x: xFor(age), y: 230, "text-anchor": "middle", fill: age === model.conversionAge ? "#DC2626" : "#64748B", "font-size": "10.5", "font-weight": age === model.conversionAge ? "800" : "600" }); t.textContent = age; ageLabelsGroup.appendChild(t); }); } function render() { const model = calculateModel(); if (Number(monthlyInput.value) > MAX_MONTHLY_CONTRIBUTION) { monthlyInput.value = MAX_MONTHLY_CONTRIBUTION; } currentValue.textContent = money(model.currentBalance); monthlyValue.textContent = money(model.monthlyContribution); ageValue.textContent = model.childAge + " yrs"; returnValue.textContent = percent(model.annualReturn); if (withBalanceEl) withBalanceEl.textContent = money(model.rothBalanceAt65); if (withoutBalanceEl) withoutBalanceEl.textContent = money(model.b2NetSpendable); if (taxDiffEl) taxDiffEl.textContent = money(model.totalB2TaxOver20Years); if (advConversionEl) advConversionEl.textContent = money(model.balanceAtConversion); if (advTaxEl) advTaxEl.textContent = money(model.totalConversionTax); if (advTaxLabelEl) advTaxLabelEl.textContent = "Estimated conversion tax at " + model.conversionAge; if (advSidecarEl) advSidecarEl.textContent = money(model.sidecarMonthlySavings) + "/mo"; renderChart(model); [currentInput, monthlyInput, ageInput, returnInput].forEach(updateRangeBackground); } Object.keys(stateTaxData).forEach(function (stateName) { const option = document.createElement("option"); option.value = stateName; option.textContent = stateName; if (stateName === "New York") option.selected = true; stateSelect.appendChild(option); }); [currentInput, monthlyInput, ageInput, returnInput, situationSelect, stateSelect].forEach(function (element) { element.addEventListener("input", render); element.addEventListener("change", render); }); if (advToggle && advBody) { advToggle.addEventListener("click", function () { const isHidden = advBody.hasAttribute("hidden"); if (isHidden) { advBody.removeAttribute("hidden"); advToggle.setAttribute("aria-expanded", "true"); root.setAttribute("data-fm-adv-open", "true"); } else { advBody.setAttribute("hidden", ""); advToggle.setAttribute("aria-expanded", "false"); root.removeAttribute("data-fm-adv-open"); } }); } render(); }); });