S
Symphony
Retirement Calculator
Portfolio
Annual Spend
Success Rate
Ret. Duration
⚡ Actions
Profile
Time horizon, income streams & Social Security
Time Horizon
35
60
95
Years to Retire
25
Retirement Duration
35 yrs
Income Profile
Social Security
How likely is SS to exist when you retire?
70%
0% excludes SS entirely. 100% applies full benefit. Probability scales proportionally at all values between.
Spousal SS Benefit
Healthcare Bridge
Pre-Medicare out-of-pocket cost from early retirement to age 65. Applied as a discrete expense line inflating at 1.5× headline CPI.
Portfolio
Account-level breakdown · tax treatment · allocation · projected value at retirement
Total Portfolio
$0
Taxable
$0
Pre-Tax (Trad.)
$0
Roth (After-Tax)
$0
HSA
$0
Est. at Retirement
Taxable BrokerageCap Gains Rate on Withdrawal
Gains held >1 year taxed at 0/15/20% LTCG rate depending on income. Most tax-flexible account — no contribution limits, no withdrawal penalties, no RMDs.
Traditional 401k / IRAOrdinary Income on Withdrawal · RMDs at 73
Pre-tax contributions reduce taxable income now. Withdrawals taxed as ordinary income. Required Minimum Distributions begin at age 73. The Roth conversion window (retirement → age 73) is an opportunity to shift balances to tax-free status at potentially lower rates.
Roth 401k / IRATax-Free Growth & Withdrawal · No RMDs
After-tax contributions. Qualified withdrawals (age 59½+, account open 5+ years) are completely tax-free including all growth. No RMDs during the owner lifetime. The most powerful asset to leave last — every dollar grows tax-free indefinitely. 2025 limit: $7,000 ($8,000 if 50+).
HSA — Health Savings AccountTriple Tax Advantaged
The single most tax-efficient account available. Pre-tax contributions → tax-free growth → tax-free withdrawal for qualified medical. After age 65, non-medical withdrawals taxed as ordinary income (same as Traditional IRA, but with the medical-free bonus). 2025 limit: $4,300 single / $8,550 family. Requires HDHP enrollment.
Other Assets
Pension modeled as annual income stream from retirement date. Real estate equity included in net worth but not liquidated in projections unless specified. Other: crypto, commodities, private equity, etc.
Blended Allocation & Returns
ON
3.5%
Auto-calculated
7.0%
Blended Return
6.4%
Total at Retirement
Large Expenditures
One-time planned expenses at specific years — vehicles, renovations, travel, medical, education
Total Planned
$0
Active Items
0
Inflation-Adj. Total
$0
Common Retirement Expenditures
🚗
New Vehicle
~$45,000
🏠
Home Renovation
~$80,000
✈️
Travel / Sabbatical
~$35,000
🏥
Major Medical
~$20,000
🛡️
Long-Term Care
~$150,000
🎓
Family Gift / Education
~$50,000
Withdrawal Sequencing
Tax-optimized withdrawal order · SS timing analysis · Roth conversion window · account drawdown strategy
Social Security Timing Analysis
SS benefit grows approximately 6–8% per year of delay between age 62 and 70. Delaying from 67 → 70 permanently increases your monthly benefit by 24%. The breakeven age determines whether delaying pays off.
Claim at 62
70% of FRA benefit
Claim at 67 (FRA)
Full Retirement Age
Claim at 70
+24% vs FRA
BREAKEVEN: CLAIM 62 vs 67
Age ~78
If you live past this age, waiting to 67 wins
BREAKEVEN: CLAIM 67 vs 70
Age ~82
If you live past this age, waiting to 70 wins
RECOMMENDATION
Enter your SS benefit above to generate a personalized timing recommendation.
Roth Conversion Window
The years between early retirement and age 73 (RMD start) are a tax arbitrage opportunity. Your income drops, putting you in a lower bracket — ideal for converting Traditional IRA/401k to Roth at reduced cost.
Conversion Window
— yrs
Ret. age → 73
Trad. Balance
Available to convert
Annual Conversion
To stay in 12% bracket
CONVERSION STRATEGY
Configure your Traditional and Roth balances in Portfolio to generate a conversion recommendation.
Tax-Optimized Withdrawal Order
The sequence in which you draw from accounts determines your lifetime tax bill. This order minimizes taxes while preserving tax-advantaged growth as long as possible.
1
Required Minimum Distributions (Age 73+)
Non-negotiable. IRS mandates annual withdrawals from Traditional accounts starting at 73. Failure penalty is 25% of the required amount. Plan Roth conversions during the window before 73 to reduce future RMD burden.
Traditional 401k · Traditional IRA · Inherited IRAs
2
Taxable Brokerage Accounts
Draw next. Long-term capital gains (held >1 year) are taxed at 0%, 15%, or 20% depending on income — significantly lower than ordinary income rates. Selling losing positions offsets gains (tax-loss harvesting). No RMDs, no penalties, maximum flexibility.
Individual stocks · ETFs · Mutual funds · Cash
3
Traditional IRA / 401k (Non-RMD Withdrawals)
Draw pre-RMD age withdrawals to fill lower tax brackets. Coordinate with Roth conversions to avoid bracket creep. Every dollar withdrawn here before 73 reduces your future RMD burden and potential SS taxation. Penalty-free after 59½.
Traditional 401k · Traditional IRA · SEP-IRA
4
HSA — Non-Medical Withdrawals
After age 65, HSA non-medical withdrawals are taxed as ordinary income — identical to a Traditional IRA. But use HSA funds for qualified medical first (tax-free). Save receipts from prior years; you can reimburse yourself for old qualified expenses at any time, tax-free.
HSA · Tax-free for medical at any age
5
Roth IRA / 401k — Last Resort
Draw last. Tax-free and no RMDs during your lifetime. Every additional year of tax-free compounding in a Roth is worth more than the equivalent in a taxable account. The exception: Roth is ideal for large irregular expenses (travel, renovation) where a spike in ordinary income would push you into a higher bracket.
Roth IRA · Roth 401k · Roth conversions
Action Items
Personalized checklist based on your current inputs — what to do next and why
Annual Spending
Toggle categories · $ monthly or % of income · annual totals computed automatically
Monthly Total
$0
Annual Total
$0
10-Yr Inflation Adj.
$0
Active / Total
0/0
Withdrawal Strategy
Select your framework — each carries different risk, variability & longevity characteristics
4% Rule
Classic
Withdraw 4% of your initial portfolio in year one, then adjust each year for inflation only — regardless of market performance. Simple, predictable, historically tested over 30-year windows.
⚠ Why it may be outdated: Calibrated on 30-year retirements with historically higher bond yields. For 35–40 year retirements, the mathematically safer rate is closer to 3.2–3.5%. Withdrawals continue unchanged even as the portfolio erodes in down markets.
Guardrails Strategy
Recommended
Set an upper and lower withdrawal rate corridor. When your rate hits the upper guardrail (portfolio underperforming), cut spending by 10%. When it drops below the lower (outperforming), increase by 10%.
✓ Best for: Retirees who can tolerate modest spending variability in exchange for better portfolio longevity and higher sustainable initial rates (4.5–5.5%). Adapts to actual market conditions.
Variable Percentage (VPW)
Age-Scaled
Withdraw a percentage that increases with age based on remaining life expectancy and expected returns. At 65 you might withdraw 4.2%; at 80, you withdraw 6.8% of whatever remains in the portfolio.
✓ Best for: Retirees who want maximum spending efficiency — no large unspent balance, near-zero depletion risk. Requires comfort with year-to-year income variability correlated to portfolio performance.
Floor / Ceiling Method
Custom Bounds
You define a minimum annual spend (floor — essentials only) and a maximum (ceiling — full lifestyle). The model finds the sustainable path within your corridor, pulling to the floor in bad markets and the ceiling in good ones.
✓ Best for: Retirees with clear fixed versus discretionary expense separation who want guaranteed baseline coverage with lifestyle upside built in.
Comparison Matrix
StrategyInitial RateSpending Variability30-Yr Success*40-Yr Success*Ideal For
4% Rule4.0%Inflation-only~95%~78%Predictability first
Guardrails4.5–5.5%±10% adjustments~96%~91%Adaptive spenders
VPW4.2–5.0%Market-correlated~99%~99%Efficiency-focused
Floor/CeilingCustomWithin your corridorFloor-dependentFloor-dependentSplit spenders
*Peer-reviewed literature on mixed-asset portfolios. Your actual success rate is computed live in Projections via Monte Carlo simulation.
Inflation Engine
Full-basket CPI · PPI forward pressure · personal spending-weighted rate
Blended Effective Inflation Rate
3.6%
Headline CPI (Full Basket)3.2%
edit to override
Full basket including shelter, food, and energy. "Core CPI" strips food and energy — the items purchased most frequently. This platform always uses the full basket.
PPI — Producer Price Index2.8%
edit to override
Upstream input cost pressure. PPI leads CPI by 6–12 months. A wide PPI-CPI spread signals faster future purchasing power erosion than the current headline number shows.
Personal Inflation Rate3.8%
edit to override
Your actual inflation weighted to your spending categories. Healthcare-heavy and resort-housing budgets typically run 0.5–1.5% above headline CPI annually.
Signal Blend Weights
Weight of each signal in your effective blended rate. PPI weight acts as a forward-pressure modifier — raising it increases your effective rate when the PPI-CPI spread is wide.
Auto = 100% minus CPI and PPI weights
Category Inflation Overrides
Override inflation rate per spending category. Healthcare has historically inflated at 1.5–2× CPI. Resort-area housing can run 2–3× core CPI.
What "Core CPI" Strips Out
The BLS defines "Core CPI" by removing food and energy on the justification that they are volatile. In practice, this means the number most widely reported in media excludes exactly the goods purchased most frequently.

Hedonic quality adjustments additionally deflate the measured price of goods that improve in quality over time — electronics, vehicles — even when your out-of-pocket cost rises.

This platform uses full-basket CPI blended with PPI forward pressure and your personal spending weights. No hedonic stripping. No food-and-energy exclusion.
Geographic Intelligence
Side-by-side COL, housing appreciation & tax environment · 30+ metro areas in the lookup database
Location A
Sun Valley / Ketchum, ID
Blaine County · No State Income Tax
COL Index (US = 100)148
Median Home Price$1,240,000
5-Yr Avg Appreciation8.2%
State Income Tax0%
Spend Multiplier vs US Avg1.48×
Your Effective Annual Spend
Home Value in 30 Years
versus
Location B
Boise, ID
Ada County · No State Income Tax
COL Index (US = 100)108
Median Home Price$465,000
5-Yr Avg Appreciation6.1%
State Income Tax0%
Spend Multiplier vs US Avg1.08×
Your Effective Annual Spend
Home Value in 30 Years
Annual Cost Delta
A vs B per year
30-Year Compounded Delta
Total additional spend over 30-yr retirement
inflation-adjusted at blended rate
Projections
Monte Carlo simulation · 300 randomized return paths · bear, base & bull outcomes
Success Rate
% of simulations funded
Median End Value
at longevity target
Bear Outcome (P10)
worst 10% scenario
Bull Outcome (P90)
best 10% scenario
Year-1 Withdrawal
first year of retirement
Bear
10th percentile
Base
50th percentile
Bull
90th percentile
Portfolio Trajectory
Year-by-Year Base Case
YearAgePortfolioWithdrawalIncomeNet Change
Speed
How fast are you building wealth — savings rate, wealth velocity, years-to-FI, and accumulation trajectory
Savings Rate
% of gross income saved
Annual Wealth Added
contributions + growth
Monthly Wealth Added
avg per month this year
Years to FI Target
at current savings rate
FI Number
portfolio needed to retire
% of FI Achieved
where you are today
Savings Rate Analysis
Your Savings Rate
0%10% avg20% good40%+
Benchmark Comparison
Wealth Velocity
How fast your net worth is compounding right now — contributions plus investment growth on existing assets.
Contribution Velocity
what you add annually
Growth Velocity
portfolio earning for you
Crossover Point
When investment growth exceeds your annual contributions — the wealth flywheel takes over.
5-Year Wealth Projection
Year Portfolio Added Growth
FI Number — How It's Calculated
At 4% Rule
Spend × 25
At 3.3% (Safer)
Spend × 30 · 40-yr retirement
At Guardrails (5%)
Spend × 20 · adaptive
The FI Number is the portfolio size at which your investment growth sustains your spending indefinitely. Your effective FI Number depends on your withdrawal strategy. The 3.3% rate (spend × 30) is more appropriate for a ${Math.max(0,(35)}-year retirement than the classic 4% Rule.
Current progress to FI (3.3% target)
Intelligence Summary
AI-powered analysis synthesized from all inputs — specific, quantitative & actionable
Gemini API Key
Free at aistudio.google.com — never stored or sent anywhere except Google
Get Free Key →
Enter your Gemini API key, configure inputs across all tabs, then generate.
Enter your free Gemini API key above (from aistudio.google.com), configure your inputs across all tabs, then click Generate Summary for a comprehensive AI analysis of your retirement scenario. Gemini 2.0 Flash is free up to 15 requests/minute on the Google AI Studio free tier — no credit card required.
Guide & Education
What every input means, why it matters, and curated research on retirement planning
Input Glossary — What Every Field Means
⬤ Profile
Current AgeYour age today. Every projection anchors to this. A single year earlier means one additional year of compound growth — at 7% returns, starting at 34 vs. 35 adds roughly 7% to your terminal portfolio value.
Target Retirement AgeThe year you stop requiring employment income. Does not have to mean stopping all work — it means the portfolio begins funding your life. Earlier retirement dramatically increases the number of years the portfolio must sustain you, which is the primary driver of depletion risk.
Longevity TargetHow long you plan your money to last. Marathon runners and non-smokers in good health at 60 have a meaningful probability of reaching 95+. Plan short and run out of money; plan long and leave a larger estate. This is a planning ceiling, not a prediction.
SS Availability ConfidenceThe Social Security Trust Fund is projected to face insolvency between 2033–2035 under current law, at which point benefits would be cut to approximately 77 cents on the dollar — not eliminated. This slider lets you model anywhere from full exclusion (0%) to full benefit (100%). For someone retiring in 25+ years, 50–70% is a reasonable conservative assumption.
Healthcare BridgeMedicare eligibility begins at 65. If you retire before 65, you face a coverage gap. ACA marketplace premiums for a 60-year-old non-smoker in Idaho currently run $800–$1,400/month before subsidies. This is modeled as a discrete expense line inflating at 1.5× headline CPI because healthcare costs have historically outpaced general inflation by that margin or more.
⬤ Portfolio
Blended ReturnThe weighted average of your equity and bond return assumptions. This is the single number your accumulation phase runs on. At 7% equity / 3.5% bond with an 80/20 split, the blended rate is approximately 6.3%. Small changes compound dramatically over 25 years — a 6% vs. 7% blended return on $750K over 25 years is a difference of roughly $400,000 at retirement.
LLC — Income Stream ModeTreats your LLC as an ongoing business generating recurring income. This reduces the portfolio withdrawal burden dollar-for-dollar during the years the income is active. A $85K/yr LLC income at a 4% withdrawal rate is equivalent to having an additional $2.1M in portfolio assets.
LLC — Sellable Asset ModeTreats the LLC as a business with a defined exit value. The probability-weighted expected value is added to your portfolio as a lump sum in the target year. A $400K sale at 75% confidence is modeled as $300K in expected value — not the full $400K, which would overstate the projection.
Asymmetric IP / Liquidity EventA probability-weighted windfall — patent licensing, platform acquisition, or similar. The key discipline here is to never model these at 100% probability. A $1.5M event at 25% confidence contributes $375K in expected value to the projection. This allows you to understand what the upside means without planning around it.
⬤ Spending
Monthly vs. Annual InputBoth fields are live — editing monthly syncs the annual automatically and vice versa. Most people think in monthly terms for regular expenses and annual terms for infrequent ones. Enter whichever feels natural per category. The projection model uses the annual total.
10-Year Inflation AdjustmentWhat today's spending level will cost in 10 years at your current blended inflation rate. At 3.6% inflation, $83,760 today becomes approximately $118,000 in 10 years. This is the real purchasing power problem — your portfolio must grow faster than this number, or your withdrawal rate rises automatically over time.
COL Spend MultiplierYour spending is indexed to the geographic cost of living of Location A. Ketchum at COL 148 means every dollar of spending you have entered actually costs $1.48 relative to a national average household. Moving to Boise (COL 108) reduces that same lifestyle cost by approximately 27% — a real, compounded difference of hundreds of thousands of dollars over a 30-year retirement.
⬤ Withdrawal Strategy
Why 4% Is a Starting Point, Not a RuleThe "4% Rule" originated from the 1994 Bengen study and was calibrated on 30-year retirement windows with 1926–1992 market data — a period of unusually high bond yields. A 2013 update by Pfau and Kitces found that for today's interest rate environment, the safe rate is closer to 2.8–3.3% for 30-year retirements, and lower still for 35–40 year retirements. The rule is a useful reference, not a guarantee.
Guardrails — Upper and LowerThe upper guardrail triggers a 10% spending cut when your withdrawal rate rises above the ceiling (typically 5.5%). This happens when portfolio value drops and the same dollar withdrawal becomes a higher percentage. The lower guardrail triggers a 10% spending increase when the portfolio outperforms. These adjustments smooth the sequence-of-returns problem without requiring perfect market timing.
Sequence-of-Returns RiskThe order of returns matters as much as the average. A portfolio that loses 30% in year 1 of retirement and then recovers never fully catches up to one that gains first and loses later — because withdrawals during the down period permanently remove shares that never participate in the recovery. This is the dominant risk for early retirees with long durations. Guardrails and VPW both address this; the 4% Rule does not.
⬤ Inflation Engine
Full-Basket CPI vs. Core CPI"Core CPI" excludes food and energy — the two categories purchased with the highest frequency. The BLS justifies this as removing volatility, but for household planning purposes, this exclusion systematically underestimates the actual erosion of purchasing power. Full-basket CPI includes shelter (approximately 35% weight), food at home, food away from home, energy, and transportation. This platform always uses the full basket.
PPI as a Leading IndicatorThe Producer Price Index measures what businesses pay for inputs — raw materials, components, labor at the production level. Because producers pass these costs downstream, PPI changes typically show up in consumer prices 6–12 months later. When PPI runs significantly above CPI (as it did in 2021–2022), it signals that consumer inflation is about to accelerate. Weighting PPI in your blended rate is a conservative, forward-looking adjustment.
Personal Inflation RateThe national CPI basket is weighted to an average American household — which spends differently than you do. If healthcare is 20% of your retirement budget instead of the national average of 8%, and healthcare inflates at 5.5% while general inflation is 3.2%, your personal inflation rate is materially higher than the headline number. This field lets you set your own rate based on your actual spending composition.
Research Foundation — The Studies Behind This Platform
1994
Determining Withdrawal Rates Using Historical Data
The original paper that established the "4% Rule." Bengen analyzed rolling 30-year periods from 1926–1992 and found that a 4% initial withdrawal adjusted for inflation never depleted a 50/50 portfolio in historical data. The paper that every retirement calculator still references — and the one most commonly misapplied to longer retirement windows and lower-yield environments.
2013
The 4% Rule Is Not Safe in a Low-Yield World
The landmark challenge to Bengen. Using forward-looking return assumptions calibrated to 2013 interest rate levels, Pfau found that the safe withdrawal rate for a 30-year retirement was closer to 2.8%. For 40-year retirements — which apply to anyone retiring at 60 — the safe rate falls further. This is why this platform treats 4% as a benchmark, not a floor.
2006
Decision Rules and Maximum Initial Withdrawal Rates
The paper that formalized the Guardrails strategy used in this platform. Guyton and Klinger tested a set of "decision rules" — including the withdrawal rate guardrail — and found that applying them allowed initial withdrawal rates of 5.2–6.2% while maintaining portfolio survival rates above 90% over 40-year windows. The tradeoff is spending flexibility, not higher risk.
2015
Variable Percentage Withdrawal (VPW)
The formalization of VPW as a practical method. Rather than a fixed dollar amount adjusted for inflation, VPW withdraws a percentage that increases with age based on remaining life expectancy and expected portfolio returns. Near-zero probability of portfolio depletion, no wasted capital, and no spending floors — at the cost of income variability correlated to market performance.
2019
Estimating the True Cost of Retirement
Blanchett documented the "retirement spending smile" — that real spending typically declines in the middle years of retirement (less travel, less activity) before spiking in the final years due to healthcare costs. The implication: early retirement spending can be modeled higher, middle-year spending lower, and late-life healthcare as a separate discrete expense — which is why this platform treats the healthcare bridge and post-Medicare costs separately.
2022
Inflation and Retirement Income Security
A post-pandemic analysis of how sustained inflation above 5% affects retiree portfolios across different withdrawal strategies. Found that retirees who used fixed-dollar withdrawal strategies (like the 4% Rule without guardrails) saw their real purchasing power erode by 18–24% over a 3-year high-inflation period. Adaptive strategies — guardrails, VPW — preserved purchasing power significantly better. The paper that validates the inflation engine in this platform.
Key Concepts — Plain Language
Monte Carlo Simulation
Rather than projecting a single return path, Monte Carlo runs 300 randomized scenarios where annual returns are drawn from a normal distribution centered on your blended return assumption with a standard deviation of 14% (historical equity volatility). The success rate is the percentage of those 300 paths where the portfolio survives to your longevity target. An 85%+ success rate is conventionally considered robust. Below 75% is a planning warning.
Sequence-of-Returns Risk
If your portfolio drops 40% in the first three years of retirement, you are selling assets at depressed prices to fund withdrawals. Those sold shares never participate in the recovery. A portfolio that earns the same average return but in a favorable order — gains first, losses later — survives far longer. This is the primary argument against the static 4% Rule and the primary argument for adaptive withdrawal strategies.
Real vs. Nominal Returns
A 7% nominal return with 3.6% inflation is a 3.4% real return. Your portfolio must grow in real terms — above inflation — to maintain purchasing power. At 3.6% inflation, a dollar today buys only $0.34 worth of goods in 30 years. The blended inflation rate in this platform is subtracted from nominal returns when computing real purchasing power in the projections.
The LLC as a Capital Multiplier
At a 4% withdrawal rate, every dollar of reliable annual income is equivalent to $25 in portfolio assets. An LLC generating $85,000/year has the same retirement-funding power as $2.1M in invested assets — without the portfolio depletion risk. This is why the mode toggle matters: treating an LLC as income vs. a sellable asset produces dramatically different retirement scenarios and risk profiles.
Geographic Arbitrage
The difference in cost of living between Ketchum (COL 148) and Boise (COL 108) is 37%. Over a 30-year retirement at $83,760 base spending with 3.6% inflation, this difference compounds to over $800,000 in total additional spend in the higher-cost location. Geographic choice is one of the highest-leverage retirement decisions available — larger than most portfolio optimization choices.
// ── EXTENDED STATE ── const PA = { accounts: { stocks: 80000, etfs: 320000, cashTax: 50000, taxContrib: 12000, taxRet: 7.2, tradBal: 240000, tradContrib: 23000, tradMatch: 5000, tradRet: 7.0, rothBal: 45000, rothContrib: 7000, rothRet: 7.5, hsaBal: 18000, hsaContrib: 4300, hsaRet: 7.0, pension: 0, realEstate: 0, other: 0 }, expenditures: [], convBkt: 12 }; let expIdCtr = 100; function pN(s){return parseFloat(String(s).replace(/,/g,''))||0;} function setText(id,v){const el=document.getElementById(id);if(el)el.textContent=v;} function fC(n){if(n===null||n===undefined||isNaN(n))return'—';if(Math.abs(n)>=1e9)return'$'+(n/1e9).toFixed(2)+'B';if(Math.abs(n)>=1e6)return'$'+(n/1e6).toFixed(2)+'M';return'$'+Math.round(n).toLocaleString();} function fS(n){if(n===null||n===undefined||isNaN(n))return'—';if(Math.abs(n)>=1e9)return'$'+(n/1e9).toFixed(1)+'B';if(Math.abs(n)>=1e6)return'$'+(n/1e6).toFixed(1)+'M';if(Math.abs(n)>=1e3)return'$'+(n/1e3).toFixed(0)+'K';return'$'+Math.round(n);} // ── PORTFOLIO TOTALS ── function getPortTotal(){ const a=PA.accounts; return pN(document.getElementById('p-stocks')?.value||a.stocks) + pN(document.getElementById('p-etfs')?.value||a.etfs) + pN(document.getElementById('p-cash-tax')?.value||a.cashTax) + pN(document.getElementById('p-trad-bal')?.value||a.tradBal) + pN(document.getElementById('p-roth-bal')?.value||a.rothBal) + pN(document.getElementById('p-hsa-bal')?.value||a.hsaBal) + pN(document.getElementById('p-other')?.value||0) + pN(document.getElementById('p-realestate')?.value||0); } function getTaxable(){return pN(document.getElementById('p-stocks')?.value||0)+pN(document.getElementById('p-etfs')?.value||0)+pN(document.getElementById('p-cash-tax')?.value||0);} function getPreTax(){return pN(document.getElementById('p-trad-bal')?.value||0);} function getRoth(){return pN(document.getElementById('p-roth-bal')?.value||0);} function getHSA(){return pN(document.getElementById('p-hsa-bal')?.value||0);} function getTotalContrib(){ return pN(document.getElementById('p-tax-contrib')?.value||0) + pN(document.getElementById('p-trad-contrib')?.value||0) + pN(document.getElementById('p-trad-match')?.value||0) + pN(document.getElementById('p-roth-contrib')?.value||0) + pN(document.getElementById('p-hsa-contrib')?.value||0); } function updPortTotals(){ const total=getPortTotal(); setText('port-total-disp',fC(total)); setText('port-tax-disp',fC(getTaxable())); setText('port-pretax-disp',fC(getPreTax())); setText('port-roth-disp',fC(getRoth())); setText('port-hsa-disp',fC(getHSA())); S.port.total = total; // Sync savings from input const savEl=document.getElementById('i-savings'); if(savEl) S.port.sav=pN(savEl.value); updPortSum(); updSequencing(); updActions(); runProjection(); } // ── EXPENDITURES ── function renderExps(){ const list=document.getElementById('exp-list');if(!list)return; list.innerHTML = PA.expenditures.map(e=>`
${e.cat}
$
YR
INF ADJ
${e.infAdj?fC(e.amt*Math.pow(1+bInf()/100,Math.max(0,e.yr-new Date().getFullYear()))):fC(e.amt)}
${e.infAdj?'inf-adj':'nominal'}
`).join('') || '
No expenditures added. Use the quick-add buttons below or click + Add Expenditure.
'; updExpSummary(); } function togExp(id){const e=PA.expenditures.find(x=>x.id===id);if(e){e.on=!e.on;renderExps();}} function togExpInf(id){const e=PA.expenditures.find(x=>x.id===id);if(e){e.infAdj=!e.infAdj;renderExps();}} function updExp(id,k,v){const e=PA.expenditures.find(x=>x.id===id);if(e){e[k]=k==='amt'||k==='yr'?parseFloat(v)||0:v;renderExps();}} function rmExp(id){PA.expenditures=PA.expenditures.filter(x=>x.id!==id);renderExps();} function addExp(){PA.expenditures.push({id:expIdCtr++,name:'Custom Expense',cat:'other',on:true,amt:10000,yr:new Date().getFullYear()+5,infAdj:true});renderExps();} function quickExp(name,amt,yr,cat){PA.expenditures.push({id:expIdCtr++,name,cat,on:true,amt,yr,infAdj:true});renderExps(); const t=document.querySelector('[data-tab=expenditures]');if(t)t.click();} function updExpSummary(){ const active=PA.expenditures.filter(e=>e.on); const total=active.reduce((s,e)=>s+e.amt,0); const adj=active.reduce((s,e)=>s+(e.infAdj?e.amt*Math.pow(1+bInf()/100,Math.max(0,e.yr-new Date().getFullYear())):e.amt),0); setText('exp-total',fC(total));setText('exp-count',active.length);setText('exp-adj',fC(adj)); } // ── SS TIMING ── function updSequencing(){ const ben=S.ss.ben; const at62=Math.round(ben*0.70),at67=ben,at70=Math.round(ben*1.24); setText('ss-at62',fC(at62*12)+'/yr');setText('ss-at67',fC(at67*12)+'/yr');setText('ss-at70',fC(at70*12)+'/yr'); // Breakeven: months to recoup delayed benefit const diff6267=(at67-at62)*12,lostYrs62=(at62*12*60)/12,be6267=Math.round(60*at62/(at67-at62)/12+62); const diff6770=(at70-at67)*12,lostYrs67=(at67*12*36)/12,be6770=Math.round(36*at67/(at70-at67)/12+67); setText('ss-be-6267','Age ~'+be6267);setText('ss-be-6770','Age ~'+be6770); // SS recommendation const lon=S.p.lon,rage=S.p.rage; let rec=''; if(lon>=85&&rage<=62){rec=`Given your longevity target of ${lon} and early retirement at ${rage}, delaying SS to 70 adds $${Math.round((at70-at62)*12).toLocaleString()}/year permanently — a difference of approximately $${Math.round((at70-at62)*12*(lon-70)).toLocaleString()} over your retirement duration. With a ${lon-be6770}-year cushion beyond the 67→70 breakeven age, delay to 70 is strongly favored.`;} else if(lon>=82){rec=`Your longevity target of ${lon} puts you ${lon-be6770} years past the 67→70 breakeven age (~${be6770}). Delay to 70 is likely favorable. Use taxable or Traditional IRA withdrawals to bridge the gap between retirement at ${rage} and SS at 70.`;} else{rec=`With a longevity target of ${lon}, the 67→70 delay breakeven at age ~${be6770} leaves a smaller margin. Consider claiming at 67 (FRA) unless health or portfolio performance favors waiting.`;} setText('ss-rec',rec); // Roth conversion window const convWindow=Math.max(0,73-rage); const tradBal=getPreTax(); const bktLimits={12:59050,22:94300,24:201050}; const bktAmt=bktLimits[PA.convBkt]||59050; const annual=convWindow>0?Math.min(Math.round(bktAmt*0.7),Math.round(tradBal/convWindow)):0; setText('conv-window',convWindow+' yrs');setText('conv-trad',fC(tradBal));setText('conv-annual',fC(annual)+'/yr'); let crec=''; if(tradBal>0&&convWindow>0){crec=`You have ${convWindow} years between retirement at ${rage} and RMD start at 73. Converting approximately $${annual.toLocaleString()}/year stays within the ${PA.convBkt}% bracket. Total convertible in the window: $${Math.min(tradBal,annual*convWindow).toLocaleString()} — reducing your future RMD burden and potential bracket creep.`;} else if(tradBal===0){crec='No Traditional IRA/401k balance entered. If you have pre-tax accounts, enter them in Portfolio to see your conversion opportunity.';} else{crec='Enter your retirement age and Traditional balance to generate a conversion recommendation.';} setText('conv-rec',crec); } function setConvBkt(b){ PA.convBkt=b; [12,22,24].forEach(x=>{const el=document.getElementById('cbkt-'+x);if(el)el.classList.toggle('a',x===b);}); updSequencing(); } // ── ACTION ITEMS (dynamic, input-aware) ── function genActions(){ const items=[]; const total=getPortTotal(),roth=getRoth(),hsa=getHSA(),trad=getPreTax(),taxable=getTaxable(); const sc=parseFloat(document.getElementById('p-succ')?.textContent)||0; const rage=S.p.rage,cage=S.p.cage,lon=S.p.lon,dur=lon-rage; // Roth check if(roth<5000){items.push({type:'urgent',icon:'🔄',title:'Open or Fund a Roth IRA',body:`Your Roth balance is ${roth===0?'zero':'very low'} ($${roth.toLocaleString()}). Roth accounts are the most powerful long-term tax shelter — contributions grow tax-free indefinitely with no RMDs. The 2025 limit is $7,000 ($8,000 if you are 50+). At your blended return, $7,000/year for ${rage-cage} years compounds to approximately $${Math.round(7000*((Math.pow(1+bRet()/100,rage-cage)-1)/(bRet()/100))).toLocaleString()} tax-free at retirement.`,cta:'→ Add Roth contributions in Portfolio'});} // HSA check if(hsa<1000){items.push({type:'urgent',icon:'🏥',title:'Open an HSA — Triple Tax Advantage',body:'Your HSA balance appears to be zero or minimal. If you are on a High Deductible Health Plan (HDHP), you are leaving the most tax-efficient account available unused. Contributions are pre-tax, growth is tax-free, and qualified medical withdrawals are tax-free. After 65, it functions as a Traditional IRA for non-medical expenses. 2025 limits: $4,300 single / $8,550 family.',cta:'→ Add HSA balance in Portfolio'});} // Success rate if(sc>0&&sc<85){items.push({type:'urgent',icon:'⚠️',title:`Success Rate Below Target — ${sc}%`,body:`Your Monte Carlo success rate of ${sc}% is below the 85% planning threshold. The three highest-leverage adjustments: (1) Increase annual savings by $5,000–$10,000 during accumulation — at ${bRet().toFixed(1)}% returns this adds approximately $${Math.round(7500*((Math.pow(1+bRet()/100,rage-cage)-1)/(bRet()/100))).toLocaleString()} at retirement. (2) Delay retirement by 2–3 years. (3) Switch from 4% Rule to Guardrails strategy to allow portfolio to adapt to market conditions.`,cta:'→ Review Strategy tab'});} else if(sc>=85){items.push({type:'ok',icon:'✅',title:`Success Rate Strong — ${sc}%`,body:`Your ${sc}% Monte Carlo success rate across 300 simulations is above the 85% planning threshold. Your current inputs produce a plan with meaningful resilience to adverse market sequences. Continue monitoring as inputs change — particularly inflation assumptions and spending.`,cta:''});} // Retirement duration if(dur>35){items.push({type:'warn',icon:'📅',title:'40+ Year Retirement — Sequence Risk Is Elevated',body:`A ${dur}-year retirement is significantly longer than the 30-year window the 4% Rule was designed for. Historical safe withdrawal rates for ${dur}-year windows are closer to 3.2–3.5%. The Guardrails or VPW strategy materially outperforms the static 4% Rule at this duration. Ensure your strategy selection in the Strategy tab accounts for this.`,cta:'→ Review Strategy tab'});} // No expenditures if(PA.expenditures.filter(e=>e.on).length===0){items.push({type:'warn',icon:'💸',title:'No Large Expenditures Modeled',body:'You have not added any one-time large expenditures to your projection. Most retirees face major unplanned or planned costs: vehicle replacement ($40-$60K), home renovation ($50-$150K), long-term care ($150-$300K), major medical events, or family financial support. These can materially affect your success rate if unaccounted for.',cta:'→ Add items in Expenditures tab'});} // Pre-tax heavy if(trad>0&&roth40&&lon>=85){items.push({type:'warn',icon:'📋',title:`Consider Delaying SS to 70 — You're Targeting Age ${lon}`,body:`With a longevity target of ${lon}, your expected retirement duration of ${lon-rage} years makes delayed SS claiming favorable. Each year of delay from FRA to 70 adds 8% permanently to your benefit. Claiming at 70 vs. 67 adds $${Math.round(S.ss.ben*0.24*12).toLocaleString()}/year for life. Review the SS Timing analysis in the Sequencing tab.`,cta:'→ Review Sequencing tab'});} // Healthcare bridge if(cage<55&&rage<65){const hcprem=pN(document.getElementById('i-hcprem')?.value||0); if(hcprem<800){items.push({type:'warn',icon:'🏥',title:'Healthcare Bridge May Be Understated',body:`You are retiring at ${rage}, ${65-rage} years before Medicare eligibility. ACA marketplace premiums for a 60-year-old non-smoker in Idaho currently run $900–$1,400/month before subsidies, inflating at roughly 1.5× CPI. Your current healthcare bridge of $${hcprem}/month may understate your actual out-of-pocket cost. Model this carefully — one uncovered medical event can permanently impair a retirement portfolio.`,cta:'→ Update Profile → Healthcare Bridge'});}} // Good Roth if(roth>50000&&trad>0){items.push({type:'ok',icon:'🌱',title:'Roth Foundation in Place — Protect It',body:`Your Roth balance of $${roth.toLocaleString()} represents tax-free wealth that will compound indefinitely. In the sequencing tab, Roth accounts are the last to draw. During your Roth conversion window (age ${rage}→73), consider additional conversions to grow this balance further before RMDs begin forcing Traditional withdrawals.`,cta:''});} return items; } function updActions(){ const items=genActions(); const list=document.getElementById('action-list'); if(list){ list.innerHTML=items.map(a=>`
${a.icon}
${a.title}
${a.body}
${a.cta?`
${a.cta}
`:''}
`).join(''); } // Banner (top 3 urgent/warn) const banner=document.getElementById('ab-items'); if(banner){ const top=items.filter(a=>a.type!=='ok').slice(0,3); banner.innerHTML=top.map(a=>`
${a.title}
`).join(''); if(top.length===0)banner.innerHTML='✓ All action items addressed'; // bind clicks via delegation banner.querySelectorAll('[data-gotab]').forEach(el=>{ el.addEventListener('click',()=>{ const t=document.querySelector('[data-tab="'+el.dataset.gotab+'"]');if(t)t.click(); }); }); } } // ── STATE ── const S={ p:{cage:35,rage:60,lon:95,inc:120000,sinc:80000,srage:58,sav:48000}, port:{total:753000,eq:80,bond:15,eqret:7,bondret:3.5, llc:{on:true,mode:'income',inc:85000,stop:2040,sale:400000,saleyr:2038,prob:75}, ip:{on:false,val:1500000,yr:2030,prob:25}}, cats:[ {id:'hous',name:'Housing / Rent',on:true,mo:2500,yr:30000,t:'$'}, {id:'hlth',name:'Healthcare & Insurance',on:true,mo:1200,yr:14400,t:'$'}, {id:'food',name:'Food & Groceries',on:true,mo:900,yr:10800,t:'$'}, {id:'tran',name:'Transportation',on:true,mo:500,yr:6000,t:'$'}, {id:'trav',name:'Travel & Outdoor Recreation',on:true,mo:700,yr:8400,t:'$'}, {id:'util',name:'Utilities',on:true,mo:280,yr:3360,t:'$'}, {id:'clth',name:'Clothing',on:true,mo:150,yr:1800,t:'$'}, {id:'ent',name:'Entertainment & Dining',on:true,mo:350,yr:4200,t:'$'}, {id:'pers',name:'Personal Care',on:true,mo:100,yr:1200,t:'$'}, {id:'misc',name:'Miscellaneous',on:true,mo:300,yr:3600,t:'$'}, ], strat:{method:'guardrails',ghi:5.5,glo:3.5,floor:48000,ceil:120000}, inf:{cpi:3.2,ppi:2.8,pers:3.8,cpiw:60,ppiw:20}, ss:{on:true,prob:70,age:67,ben:2800,spon:false,spben:1800}, geo:{ a:{zip:'83340',metro:'Sun Valley / Ketchum, ID',county:'Blaine County',col:148,home:1240000,appr:8.2,tax:0}, b:{zip:'83702',metro:'Boise, ID',county:'Ada County',col:108,home:465000,appr:6.1,tax:0} } }; // ── ZIP DB ── const ZDB={'833':{metro:'Sun Valley / Ketchum, ID',county:'Blaine County',col:148,home:1240000,appr:8.2,tax:0},'837':{metro:'Boise, ID',county:'Ada County',col:108,home:465000,appr:6.1,tax:0},'832':{metro:'Nampa / Caldwell, ID',county:'Canyon County',col:96,home:345000,appr:5.6,tax:0},'834':{metro:'Twin Falls, ID',county:'Twin Falls County',col:91,home:285000,appr:4.9,tax:0},'836':{metro:'Coeur d\'Alene, ID',county:'Kootenai County',col:108,home:480000,appr:7.1,tax:0},'598':{metro:'Bozeman, MT',county:'Gallatin County',col:132,home:790000,appr:9.3,tax:5.9},'595':{metro:'Great Falls, MT',county:'Cascade County',col:87,home:215000,appr:3.2,tax:5.9},'820':{metro:'Cheyenne, WY',county:'Laramie County',col:94,home:305000,appr:4.1,tax:0},'830':{metro:'Jackson, WY',county:'Teton County',col:198,home:2900000,appr:10.8,tax:0},'804':{metro:'Denver, CO',county:'Denver County',col:120,home:590000,appr:5.5,tax:4.4},'816':{metro:'Aspen, CO',county:'Pitkin County',col:215,home:3400000,appr:10.1,tax:4.4},'805':{metro:'Boulder, CO',county:'Boulder County',col:140,home:830000,appr:5.8,tax:4.4},'841':{metro:'Salt Lake City, UT',county:'Salt Lake County',col:111,home:495000,appr:6.4,tax:4.85},'840':{metro:'Provo, UT',county:'Utah County',col:106,home:415000,appr:6.0,tax:4.85},'891':{metro:'Las Vegas, NV',county:'Clark County',col:108,home:425000,appr:6.9,tax:0},'895':{metro:'Reno, NV',county:'Washoe County',col:119,home:495000,appr:6.3,tax:0},'852':{metro:'Phoenix / Scottsdale, AZ',county:'Maricopa County',col:105,home:420000,appr:6.1,tax:2.5},'857':{metro:'Tucson, AZ',county:'Pima County',col:95,home:290000,appr:5.3,tax:2.5},'941':{metro:'San Francisco, CA',county:'SF County',col:196,home:1400000,appr:4.0,tax:9.3},'900':{metro:'Los Angeles, CA',county:'LA County',col:165,home:930000,appr:4.7,tax:9.3},'921':{metro:'San Diego, CA',county:'SD County',col:158,home:860000,appr:5.1,tax:9.3},'981':{metro:'Seattle, WA',county:'King County',col:150,home:790000,appr:5.0,tax:0},'972':{metro:'Portland, OR',county:'Multnomah County',col:132,home:535000,appr:4.5,tax:9.9},'787':{metro:'Austin, TX',county:'Travis County',col:120,home:565000,appr:4.9,tax:0},'752':{metro:'Dallas, TX',county:'Dallas County',col:109,home:385000,appr:4.1,tax:0},'770':{metro:'Houston, TX',county:'Harris County',col:101,home:318000,appr:3.7,tax:0},'303':{metro:'Atlanta, GA',county:'Fulton County',col:109,home:395000,appr:5.5,tax:5.49},'331':{metro:'Miami, FL',county:'Miami-Dade County',col:132,home:630000,appr:7.2,tax:0},'328':{metro:'Orlando, FL',county:'Orange County',col:108,home:390000,appr:6.3,tax:0},'100':{metro:'New York City, NY',county:'Manhattan',col:222,home:1680000,appr:2.7,tax:6.85},'021':{metro:'Boston, MA',county:'Suffolk County',col:165,home:790000,appr:4.0,tax:5.0},'606':{metro:'Chicago, IL',county:'Cook County',col:119,home:335000,appr:2.8,tax:4.95},'def':{metro:'US Average',county:'National Avg',col:100,home:385000,appr:4.0,tax:4.5}}; function zL(z){return ZDB[z&&z.length>=3?z.slice(0,3):'def']||ZDB['def'];} // ── MATH ── function bInf(){const pw=Math.max(0,100-S.inf.cpiw-S.inf.ppiw);return((S.inf.cpi*S.inf.cpiw)+(S.inf.ppi*S.inf.ppiw)+(S.inf.pers*pw))/100;} function aSpend(){return S.cats.filter(c=>c.on).reduce((t,c)=>t+c.yr,0);} function bRet(){const eq=S.port.eq/100,bond=S.port.bond/100,cash=Math.max(0,1-eq-bond);return eq*S.port.eqret+bond*S.port.bondret+cash*1.5;} function vpwR(age){const r=bRet()/100,yrs=Math.max(1,S.p.lon-age),pv=r>0?(1-Math.pow(1+r,-yrs))/r:yrs;return 1/pv;} function getWD(port,age,ifact){ const base=aSpend()*(S.geo.a.col/100); switch(S.strat.method){ case '4pct':return port*0.04; case 'vpw':return port*vpwR(age); case 'floor_ceiling':return Math.min(Math.max(base*ifact,S.strat.floor),S.strat.ceil); default:return base*ifact; } } function project(rets){ const CY=new Date().getFullYear(),retYr=CY+(S.p.rage-S.p.cage),endYr=CY+(S.p.lon-S.p.cage); const inf=bInf()/100,br=bRet()/100; let port=S.port.total;const rows=[]; for(let yr=CY;yr<=endYr;yr++){ const age=S.p.cage+(yr-CY),ret=rets?rets[yr-CY]||br:br; const ifact=Math.max(1,Math.pow(1+inf,yr-retYr)),prev=port; let wd=0,inc=0; if(age0){const r=(wd/port)*100;if(r>S.strat.ghi)wd*=0.9;else if(r=S.ss.age)inc+=S.ss.ben*(S.ss.prob/100)*12; if(S.ss.spon&&age>=65)inc+=S.ss.spben*12; if(S.port.llc.on&&(S.port.llc.mode==='income'||S.port.llc.mode==='both')&&yr{const u1=Math.random(),u2=Math.random(),z=Math.sqrt(-2*Math.log(u1+1e-10))*Math.cos(2*Math.PI*u2);return br+z*std;}); const proj=project(rets);all.push({end:proj[proj.length-1]?.port||0,proj}); } all.sort((a,b)=>a.end-b.end); return{succ:(all.filter(r=>r.end>0).length/runs*100).toFixed(1),p10:all[Math.floor(runs*.10)].end,p50:all[Math.floor(runs*.50)].end,p90:all[Math.floor(runs*.90)].end,all}; } // ── INLINE CANVAS CHART (no CDN) ── function drawLineChart(canvasId, mc, showLegend){ const el=document.getElementById(canvasId); if(!el)return; const ctx=el.getContext('2d'); const W=el.offsetWidth||el.width||300; const H=el.offsetHeight||el.height||240; el.width=W; el.height=H; const pad={l:52,r:16,t:showLegend?28:12,b:32}; const cw=W-pad.l-pad.r, ch=H-pad.t-pad.b; ctx.clearRect(0,0,W,H); ctx.fillStyle='#0f1623'; ctx.fillRect(0,0,W,H); const n=mc.all.length; const p10p=mc.all[Math.floor(n*.10)].proj; const p50p=mc.all[Math.floor(n*.50)].proj; const p90p=mc.all[Math.floor(n*.90)].proj; const CY=new Date().getFullYear(); const endYr=CY+(S.p.lon-S.p.cage); const retYr=CY+(S.p.rage-S.p.cage); // Build year array and values const yrs=[]; for(let y=CY;y<=endYr;y++)yrs.push(y); const getVal=(proj,yr)=>{const r=proj.find(x=>x.yr===yr);return r?r.port:0;}; const series=[ {proj:p10p,color:'#9a3030',dash:[4,3],w:1.5,label:'Bear P10'}, {proj:p50p,color:'#c8912a',dash:[],w:2.5,label:'Base P50'}, {proj:p90p,color:'#2e7d52',dash:[4,3],w:1.5,label:'Bull P90'} ]; // Find max value for scale const allVals=yrs.flatMap(y=>series.map(s=>getVal(s.proj,y))).filter(v=>v>0); const maxV=Math.max(...allVals)*1.05||1; const xPos=i=>pad.l+(i/(yrs.length-1||1))*cw; const yPos=v=>pad.t+ch-(v/maxV)*ch; // Grid lines ctx.strokeStyle='#1e1c19'; ctx.lineWidth=1; for(let i=0;i<=4;i++){ const y=pad.t+(ch/4)*i; ctx.beginPath();ctx.moveTo(pad.l,y);ctx.lineTo(pad.l+cw,y);ctx.stroke(); const val=maxV*(1-i/4); ctx.fillStyle='#555250'; ctx.font='9px JetBrains Mono'; ctx.textAlign='right'; ctx.fillText(fS(val),pad.l-4,y+3); } // X axis labels ctx.textAlign='center'; ctx.fillStyle='#555250'; ctx.font='8px JetBrains Mono'; const step=Math.ceil(yrs.length/6); yrs.forEach((yr,i)=>{ if(i%step===0||i===yrs.length-1){ ctx.fillText(yr,xPos(i),H-8); } }); // Retirement line if(retYr>=CY&&retYr<=endYr){ const ri=yrs.indexOf(retYr); if(ri>=0){ ctx.strokeStyle='rgba(200,145,42,0.25)'; ctx.lineWidth=1; ctx.setLineDash([3,3]); ctx.beginPath();ctx.moveTo(xPos(ri),pad.t);ctx.lineTo(xPos(ri),pad.t+ch);ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle='rgba(200,145,42,0.5)'; ctx.font='8px Barlow Condensed'; ctx.textAlign='center'; ctx.fillText('RET',xPos(ri),pad.t-2); } } // Draw series series.forEach(s=>{ ctx.strokeStyle=s.color; ctx.lineWidth=s.w; ctx.setLineDash(s.dash); ctx.beginPath(); let first=true; yrs.forEach((yr,i)=>{ const v=getVal(s.proj,yr); if(v>0){ if(first){ctx.moveTo(xPos(i),yPos(v));first=false;} else ctx.lineTo(xPos(i),yPos(v)); } }); ctx.stroke(); }); ctx.setLineDash([]); // Legend if(showLegend){ let lx=pad.l; series.forEach(s=>{ ctx.fillStyle=s.color; ctx.fillRect(lx,6,16,3); ctx.fillStyle='#888480'; ctx.font='9px Barlow Condensed'; ctx.textAlign='left'; ctx.fillText(s.label,lx+20,12); lx+=80; }); } } function renderChart(mc){ try{drawLineChart('projChart',mc,true);}catch(e){console.warn('chart:',e);} } function renderMiniChart(mc){ try{drawLineChart('miniChart',mc,false);}catch(e){console.warn('minichart:',e);} } function renderTable(proj){ document.getElementById('p-tbody').innerHTML=proj.map(r=>`${r.yr}${r.age}${r.age===S.p.rage?' Retire':''}${fC(r.port)}${r.age>=S.p.rage?fC(r.wd):'—'}${r.inc>0?fC(r.inc):'—'}${fC(r.net)}`).join(''); } // ── RUN PROJECTION ── function runProjection(){ const mc=monteCarlo(300),base=project(null); const sc=parseFloat(mc.succ); const psucc=document.getElementById('p-succ'); if(psucc){psucc.textContent=mc.succ+'%';psucc.style.color=sc>=85?'var(--green2)':sc>=70?'var(--amber)':'var(--red2)';} setText('p-med',fC(mc.p50));setText('p-p10',fC(mc.p10));setText('p-p90',fC(mc.p90)); const rr=base.find(r=>r.age>=S.p.rage); setText('p-wd',rr?fC(getWD(rr.port,S.p.rage,1)):'—'); setText('b-bear',fS(mc.p10));setText('b-base',fS(mc.p50));setText('b-bull',fS(mc.p90)); // header setText('hh-port',fS(S.port.total));setText('hh-spend',fS(aSpend())); const hs=document.getElementById('hh-succ'); if(hs){hs.textContent=mc.succ+'%';hs.className='hm-v '+(sc>=85?'g':sc>=70?'w':'r');} setText('hh-dur',(S.p.lon-S.p.rage)+' yrs'); // mini header stats setText('mini-succ',mc.succ+'%'); const msel=document.getElementById('mini-succ');if(msel)msel.style.color=sc>=85?'var(--green2)':sc>=70?'var(--amber)':'var(--red2)'; setText('mini-med',fS(mc.p50));setText('mini-p10',fS(mc.p10));setText('mini-p90',fS(mc.p90)); renderChart(mc);renderMiniChart(mc);renderTable(base); try{updSpeed();}catch(e){} scheduleSave(); } // ── SPEND CATEGORIES ── function renderCats(){ document.getElementById('cat-list').innerHTML=S.cats.map(c=>`
${c.name}
MO
YR
`).join(''); updSpendSum();renderCatInf(); } function togCat(id){const c=S.cats.find(x=>x.id===id);if(c){c.on=!c.on;renderCats();runProjection();}} function updCatMo(id,v){const c=S.cats.find(x=>x.id===id);if(c){c.mo=parseFloat(v)||0;c.yr=Math.round(c.mo*12);renderCats();runProjection();}} function updCatYr(id,v){const c=S.cats.find(x=>x.id===id);if(c){c.yr=parseFloat(v)||0;c.mo=Math.round(c.yr/12*100)/100;renderCats();runProjection();}} function rmCat(id){S.cats=S.cats.filter(c=>c.id!==id);renderCats();runProjection();} let cc=200; function addCat(){const n=prompt('Category name:')||'Custom';S.cats.push({id:'c'+cc++,name:n,on:true,mo:200,yr:2400,t:'$'});renderCats();} function updSpendSum(){ const yr=aSpend(),mo=yr/12,inf10=yr*Math.pow(1+bInf()/100,10),act=S.cats.filter(c=>c.on).length; setText('s-mo',fC(mo));setText('s-yr',fC(yr));setText('s-inf',fC(inf10));setText('s-cnt',act+'/'+S.cats.length); } function renderCatInf(){ const el=document.getElementById('cat-inf-ov');if(!el)return; const defs={hous:4.2,hlth:5.5,food:3.4,trav:4.0}; el.innerHTML=S.cats.filter(c=>c.on).slice(0,6).map(c=>`
${c.name}
%
`).join(''); } // ── GEO ── function updGeo(which){ const z=document.getElementById(which==='a'?'i-zipa':'i-zipb').value; const d=zL(z);S.geo[which]={...d,zip:z}; const w=which; setText(`z-${w}m`,d.metro); setText(`z-${w}s`,d.county+(d.tax===0?' · No State Income Tax':' · State Tax: '+d.tax+'%')); setText(`z-${w}col`,d.col);setText(`z-${w}home`,fC(d.home)); setText(`z-${w}appr`,d.appr.toFixed(1)+'%');setText(`z-${w}tax`,d.tax===0?'0%':d.tax+'%'); setText(`z-${w}mult`,(d.col/100).toFixed(2)+'×'); const sp=aSpend()*(d.col/100); setText(`z-${w}eff`,fC(sp));setText(`z-${w}30`,fC(d.home*Math.pow(1+d.appr/100,30))); const sa=aSpend()*(S.geo.a.col/100),sb=aSpend()*(S.geo.b.col/100),delta=sa-sb; setText('g-delta',fC(Math.abs(delta))); setText('g-delta-s',delta>0?'more per year in Location A':delta<0?'more per year in Location B':'identical cost'); const inf=bInf()/100,sum30=Array.from({length:30},(_,i)=>Math.abs(delta)*Math.pow(1+inf,i)).reduce((a,b)=>a+b,0); setText('g-30d',fC(sum30)); const el_acol=document.getElementById('z-acol'),el_bcol=document.getElementById('z-bcol'); if(el_acol)el_acol.className='zsv '+(S.geo.a.col<=S.geo.b.col?'bt':'wr'); if(el_bcol)el_bcol.className='zsv '+(S.geo.b.col<=S.geo.a.col?'bt':'wr'); } // ── STRATEGY ── function setSt(m){S.strat.method=m;document.querySelectorAll('.strat-card').forEach(c=>c.classList.toggle('sel',c.dataset.s===m));runProjection();} // ── LLC MODE ── function setLLC(m){ S.port.llc.mode=m; ['income','sell','both'].forEach(x=>document.getElementById('llc-'+x).classList.toggle('a',x===m)); document.getElementById('llc-inc-w').style.display=(m==='income'||m==='both')?'block':'none'; document.getElementById('llc-sell-w').style.display=(m==='sell'||m==='both')?'block':'none'; } // ── FORMATTERS ── function bSl(id,dispId,sfx,cb){ const sl=document.getElementById(id);if(!sl)return; sl.addEventListener('input',()=>{const v=parseFloat(sl.value);if(dispId)setText(dispId,v+(sfx||''));if(cb)cb(v);}); } function updPortSum(){ setText('m-blend',bRet().toFixed(2)+'%'); const yrs=S.p.rage-S.p.cage;let port=S.port.total; for(let i=0;i
Portfolio Trajectory — Live Preview Updates as you configure inputs
Success Rate
Median End Value
Bear (P10)
Bull (P90)
Bear · Base · Bull — 300-path Monte Carlo · Navigate to Projections for full detail
`; profileTab.appendChild(miniWrap); } document.addEventListener('DOMContentLoaded', function() { // ── SLIDERS ── bSl('sl-cage','d-cage','',v=>{S.p.cage=v;setText('m-ytr',Math.max(0,S.p.rage-v));setText('m-rdur',Math.max(0,S.p.lon-S.p.rage)+' yrs');updPortSum();updSequencing();runProjection();}); bSl('sl-rage','d-rage','',v=>{S.p.rage=v;setText('m-ytr',Math.max(0,v-S.p.cage));setText('m-rdur',Math.max(0,S.p.lon-v)+' yrs');updPortSum();updSequencing();runProjection();}); bSl('sl-lon','d-lon','',v=>{S.p.lon=v;setText('m-rdur',Math.max(0,v-S.p.rage)+' yrs');updSequencing();runProjection();}); bSl('sl-ssprob','d-ssprob','%',v=>{S.ss.prob=v;const el=document.getElementById('ss-pm');if(el)el.style.width=v+'%';updSequencing();runProjection();}); bSl('sl-eq',null,null,v=>{S.port.eq=v;setText('d-eq',v+'%');setText('d-cash',Math.max(0,100-v-S.port.bond)+'%');updPortSum();}); bSl('sl-bond',null,null,v=>{S.port.bond=v;setText('d-bond',v+'%');setText('d-cash',Math.max(0,100-S.port.eq-v)+'%');updPortSum();}); bSl('sl-eqret','d-eqret','%',v=>{S.port.eqret=v;updPortSum();}); bSl('sl-bondret','d-bondret','%',v=>{S.port.bondret=v;updPortSum();}); bSl('sl-llcprob','d-llcprob','%',v=>{S.port.llc.prob=v;}); bSl('sl-ipprob','d-ipprob','%',v=>{S.port.ip.prob=v;const ev=pN(document.getElementById('i-ipval').value)*(v/100);setText('ip-ev',fC(ev));}); bSl('sl-cpiw','d-cpiw','%',v=>{S.inf.cpiw=v;setText('d-persw',Math.max(0,100-v-S.inf.ppiw)+'%');updInf();}); bSl('sl-ppiw','d-ppiw','%',v=>{S.inf.ppiw=v;setText('d-persw',Math.max(0,100-S.inf.cpiw-v)+'%');updInf();}); // Inflation raw inputs ['i-cpi','i-ppi','i-persinf'].forEach(id=>{ const el=document.getElementById(id);if(!el)return; el.addEventListener('input',()=>{ const v=parseFloat(el.value)||0; if(id==='i-cpi'){S.inf.cpi=v;setText('d-cpi',v.toFixed(1)+'%');const b=document.getElementById('bar-cpi');if(b)b.style.width=Math.min(v*7,100)+'%';} if(id==='i-ppi'){S.inf.ppi=v;setText('d-ppi',v.toFixed(1)+'%');const b=document.getElementById('bar-ppi');if(b)b.style.width=Math.min(v*7,100)+'%';} if(id==='i-persinf'){S.inf.pers=v;setText('d-persinf',v.toFixed(1)+'%');const b=document.getElementById('bar-pers');if(b)b.style.width=Math.min(v*7,100)+'%';} updInf(); }); }); // Toggles bTog('tog-ss',v=>{const s=document.getElementById('ss-sec');if(s){s.style.opacity=v?'1':'.3';s.style.pointerEvents=v?'all':'none';}S.ss.on=v;}); bTog('tog-spss',v=>{const s=document.getElementById('sp-ss');if(s)s.style.display=v?'block':'none';S.ss.spon=v;}); bTog('tog-llc',v=>{const s=document.getElementById('llc-sec');if(s){s.style.opacity=v?'1':'.3';s.style.pointerEvents=v?'all':'none';}S.port.llc.on=v;}); bTog('tog-ip',v=>{const s=document.getElementById('ip-sec');if(s){s.style.opacity=v?'1':'.3';s.style.pointerEvents=v?'all':'none';}S.port.ip.on=v;}); // ZIP ['i-zipa','i-zipb'].forEach(id=>{document.getElementById(id).addEventListener('input',function(){if(this.value.length>=3)updGeo(id==='i-zipa'?'a':'b');});}); // TAB NAV document.querySelectorAll('.tab-btn').forEach(btn=>{ btn.addEventListener('click',()=>{ const tab=btn.dataset.tab; document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active')); document.querySelectorAll('.tab-pane').forEach(p=>p.classList.remove('active')); btn.classList.add('active'); document.getElementById('tab-'+tab).classList.add('active'); if(tab==='projections')runProjection(); if(tab==='speed')updSpeed(); }); }); // ── INTELLIGENCE ── async function runIntel(){ const out=document.getElementById('i-out'); const pulse=document.getElementById('i-pulse'); const status=document.getElementById('i-status'); const keyEl=document.getElementById('gemini-key'); const apiKey=keyEl?keyEl.value.trim():''; if(!apiKey){ out.textContent='Enter your Gemini API key above to generate an analysis.\n\nGet a free key at aistudio.google.com — it takes about 30 seconds.'; return; } out.textContent='Analyzing your retirement scenario...'; if(pulse)pulse.style.opacity='1'; if(status)status.textContent='Gemini is thinking...'; const mc=monteCarlo(200),base=project(null),rr=base.find(r=>r.age>=S.p.rage); const expActive=PA.expenditures.filter(e=>e.on); const expTotal=expActive.reduce((s,e)=>s+e.amt,0); const prompt=`You are a retirement intelligence analyst. Analyze this scenario with thorough, specific, actionable prose. No bullet points. No markdown headers. Organized paragraphs only. Be quantitative throughout. SCENARIO: - Age: ${S.p.cage} | Retire: ${S.p.rage} | Longevity: ${S.p.lon} | Duration: ${S.p.lon-S.p.rage} years - Portfolio total: $${S.port.total.toLocaleString()} | Est. at retirement: $${rr?rr.port.toLocaleString():'N/A'} - Annual savings/contributions: $${S.port.sav.toLocaleString()} | Blended return: ${bRet().toFixed(2)}% - Allocation: ${S.port.eq}% equity / ${S.port.bond}% bonds / ${Math.max(0,100-S.port.eq-S.port.bond)}% cash/alts - Annual spending: $${Math.round(aSpend()).toLocaleString()} ($${Math.round(aSpend()/12).toLocaleString()}/mo) - Withdrawal strategy: ${S.strat.method} - Blended inflation: ${bInf().toFixed(2)}% (CPI: ${S.inf.cpi}%, PPI: ${S.inf.ppi}%, Personal: ${S.inf.pers}%) - Monte Carlo success rate: ${mc.succ}% (300 simulations) | P10: $${Math.round(mc.p10).toLocaleString()} | P50: $${Math.round(mc.p50).toLocaleString()} | P90: $${Math.round(mc.p90).toLocaleString()} - SS: ${S.ss.on?`$${S.ss.ben.toLocaleString()}/mo at age ${S.ss.age}, ${S.ss.prob}% confidence`:'Excluded'} - LLC: ${S.port.llc.on?`${S.port.llc.mode} mode, $${S.port.llc.inc.toLocaleString()}/yr until ${S.port.llc.stop}`:'Off'} - IP event: ${S.port.ip.on?`$${S.port.ip.val.toLocaleString()} at ${S.port.ip.yr}, ${S.port.ip.prob}% probability`:'Not modeled'} - Planned large expenditures: ${expActive.length} items totaling $${expTotal.toLocaleString()} - Location A: ${S.geo.a.metro} (COL ${S.geo.a.col}) — $${Math.round(aSpend()*(S.geo.a.col/100)).toLocaleString()}/yr effective spend - Location B: ${S.geo.b.metro} (COL ${S.geo.b.col}) — $${Math.round(aSpend()*(S.geo.b.col/100)).toLocaleString()}/yr effective spend Analyze five areas in organized prose paragraphs: 1. Overall retirement readiness — what the numbers actually say. 2. Key risks — sequence-of-returns given this retirement duration, and what the PPI-CPI spread means for real purchasing power erosion. 3. The single highest-leverage action to improve the success rate. 4. Assessment of the selected withdrawal strategy (${S.strat.method}) given this specific retirement duration. 5. What the geographic choice means in compounded dollar terms over the full retirement period. Be direct, specific, and quantitative throughout. Do not use bullet points or markdown headers.`; try{ const resp = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }], generationConfig: { maxOutputTokens: 1200, temperature: 0.7 } }) } ); const data = await resp.json(); if(data.error){ out.textContent = 'Gemini API error: ' + (data.error.message || JSON.stringify(data.error)); } else { const text = data.candidates?.[0]?.content?.parts?.[0]?.text || 'No response received.'; out.textContent = text; } } catch(e) { out.textContent = 'Network error: ' + e.message + '\n\nCheck your API key and internet connection.'; } if(pulse)pulse.style.opacity='0'; if(status)status.textContent='Analysis complete — powered by Gemini 2.0 Flash'; } // ── INJECT MINI CHART + STATS INTO PROFILE TAB ── // ── SPEED CALCULATOR ── function updSpeed(){ const grossIncome = pN(document.getElementById('i-income')?.value||0) + pN(document.getElementById('i-sincome')?.value||0); const totalContrib = getTotalContrib(); const portfolio = S.port.total; const spend = aSpend(); const ret = bRet()/100; const inf = bInf()/100; // Savings rate const sr = grossIncome > 0 ? (totalContrib/grossIncome)*100 : 0; const srFmt = sr.toFixed(1)+'%'; setText('spd-sr', srFmt); setText('spd-sr-big', srFmt); // Color the savings rate metric box const box = document.getElementById('spd-sr-box'); if(box) box.style.borderColor = sr>=40?'var(--green2)':sr>=25?'var(--gold)':sr>=15?'var(--amber)':'var(--red2)'; // Rate bar const bar = document.getElementById('spd-rate-bar'); if(bar) bar.style.width = Math.min(sr/50*100, 100)+'%'; // Grade and note let grade='', note=''; if(sr>=50){grade='Exceptional — FIRE trajectory';note='A 50%+ savings rate puts you on a trajectory to financial independence in under 17 years from zero, regardless of starting point. Every additional point of savings rate shaves months off your timeline.';} else if(sr>=35){grade='Excellent — Accelerated path';note='A 35%+ savings rate meaningfully shortens your accumulation phase. Most people in this range reach FI 8-12 years ahead of the median American worker.';} else if(sr>=25){grade='Strong — Above average';note='25%+ puts you well ahead of the US median savings rate of 4-6%. At this rate you are building real long-term wealth and compressing your retirement timeline.';} else if(sr>=15){grade='Moderate — Room to accelerate';note='15% meets conventional retirement planning thresholds but results in a traditional 30-40 year working career. Increasing to 25%+ meaningfully changes your trajectory.';} else if(sr>0){grade='Below target — Action needed';note='A sub-15% savings rate means your wealth-building speed is slower than needed for early retirement. Even a 5% increase in savings rate can shave years off your timeline.';} else{grade='Enter your income and contributions above';note='Add your gross income in the Profile tab and contribution amounts in the Portfolio tab to calculate your savings rate.';} setText('spd-sr-grade', grade); setText('spd-rate-note', note); // Benchmarks const benchmarks=[ {label:'US Median Savings Rate', val:5, note:'BLS 2024'}, {label:'Traditional Planner Target', val:15, note:'conventional advice'}, {label:'Early Retirement Threshold', val:30, note:'retire in ~28 years'}, {label:'Accelerated FI (retire in 17 yrs)', val:50, note:'from zero'}, {label:'Your Rate', val:sr, note:'current', highlight:true}, ]; const bEl=document.getElementById('spd-benchmarks'); if(bEl){ bEl.innerHTML=benchmarks.map(b=>`
${b.val.toFixed(1)}% ${b.label}
`).join(''); } // Wealth velocity const growthVelocity = portfolio * ret; const totalVelocity = totalContrib + growthVelocity; setText('spd-wa', fC(totalVelocity)); setText('spd-wm', fC(totalVelocity/12)); setText('spd-cv', fC(totalContrib)+'/yr'); setText('spd-gv', fC(growthVelocity)+'/yr'); // Crossover point — when growth > contributions if(ret > 0 && totalContrib > 0){ const crossoverPortfolio = totalContrib / ret; const crossEl = document.getElementById('spd-cross'); if(crossEl){ if(portfolio >= crossoverPortfolio){ crossEl.textContent = 'Already crossed — your portfolio earns '+fC(growthVelocity)+'/yr, more than your '+fC(totalContrib)+'/yr contributions.'; crossEl.style.color='var(--green2)'; } else { const deficit = crossoverPortfolio - portfolio; const yrs = Math.log(crossoverPortfolio/portfolio) / Math.log(1+ret+totalContrib/portfolio); const crossYear = new Date().getFullYear() + Math.ceil(Math.abs(yrs)); crossEl.textContent = 'Est. '+crossYear+' ('+Math.ceil(Math.abs(yrs))+' years) — when portfolio reaches '+fC(crossoverPortfolio); crossEl.style.color='var(--text)'; } } } // 5-year projection table const tbody=document.getElementById('spd-5yr'); if(tbody){ let p=portfolio, rows=''; const CY=new Date().getFullYear(); for(let i=1;i<=5;i++){ const growth=p*ret; p=p*(1+ret)+totalContrib; rows+=` ${CY+i} ${fC(p)} +${fC(totalContrib)} +${fC(growth)} `; } tbody.innerHTML=rows; } // FI Numbers const fiSpend = spend>0 ? spend : 60000; const fi4 = fiSpend*25; const fi33 = fiSpend*30; const fi5 = fiSpend*20; setText('fi-4pct', fC(fi4)); setText('fi-33pct', fC(fi33)); setText('fi-5pct', fC(fi5)); // Years to FI at 3.3% let yrsToFI = 0; if(portfolio < fi33 && totalContrib > 0){ // Solve: fi33 = portfolio*(1+ret)^n + contrib*((1+ret)^n - 1)/ret let p2=portfolio; while(p2= fi33){ yrsToFI = 0; } setText('spd-fi', yrsToFI===0?'Now!':yrsToFI+' yrs'); setText('spd-fin', fC(fi33)); // Progress bar const pctFI = Math.min(portfolio/fi33*100, 100); setText('spd-pct', pctFI.toFixed(1)+'%'); setText('fi-pct-label', pctFI.toFixed(1)+'%'); const progressBar=document.getElementById('fi-progress-bar'); if(progressBar) progressBar.style.width=pctFI.toFixed(1)+'%'; setText('fi-dur', Math.max(0,S.p.lon-S.p.rage)); } // ── PERSISTENCE — localStorage ── const STORAGE_KEY = 'symphony_retirement_v1'; function saveState() { try { // Pull live DOM values into state before saving const syncFields = [ ['i-income', v => S.port.sav = pN(v)], // handled via savings ['i-savings', v => S.port.sav = pN(v)], ['i-ssage', v => S.ss.age = parseInt(v)||67], ['i-ssbenefit', v => S.ss.ben = pN(v)||2800], ['i-ghi', v => S.strat.ghi = parseFloat(v)||5.5], ['i-glo', v => S.strat.glo = parseFloat(v)||3.5], ['i-floor', v => S.strat.floor = pN(v)||48000], ['i-ceil', v => S.strat.ceil = pN(v)||120000], ['p-stocks', v => PA.accounts.stocks = pN(v)], ['p-etfs', v => PA.accounts.etfs = pN(v)], ['p-cash-tax', v => PA.accounts.cashTax = pN(v)], ['p-tax-contrib',v=> PA.accounts.taxContrib = pN(v)], ['p-tax-ret', v => PA.accounts.taxRet = parseFloat(v)||7.2], ['p-trad-bal', v => PA.accounts.tradBal = pN(v)], ['p-trad-contrib',v=>PA.accounts.tradContrib = pN(v)], ['p-trad-match',v => PA.accounts.tradMatch = pN(v)], ['p-trad-ret', v => PA.accounts.tradRet = parseFloat(v)||7.0], ['p-roth-bal', v => PA.accounts.rothBal = pN(v)], ['p-roth-contrib',v=>PA.accounts.rothContrib = pN(v)], ['p-roth-ret', v => PA.accounts.rothRet = parseFloat(v)||7.5], ['p-hsa-bal', v => PA.accounts.hsaBal = pN(v)], ['p-hsa-contrib',v=>PA.accounts.hsaContrib = pN(v)], ['p-hsa-ret', v => PA.accounts.hsaRet = parseFloat(v)||7.0], ['p-pension', v => PA.accounts.pension = pN(v)], ['p-realestate',v => PA.accounts.realEstate = pN(v)], ['p-other', v => PA.accounts.other = pN(v)], ['i-llcinc', v => S.port.llc.inc = pN(v)], ['i-llcstop', v => S.port.llc.stop = parseInt(v)||2040], ['i-llcsale', v => S.port.llc.sale = pN(v)], ['i-llcsaleyr', v => S.port.llc.saleyr = parseInt(v)||2038], ['i-ipval', v => S.port.ip.val = pN(v)], ['i-ipyr', v => S.port.ip.yr = parseInt(v)||2030], ['i-zipa', v => S.geo.a.zip = v], ['i-zipb', v => S.geo.b.zip = v], ['i-hcprem', v => {}], ['i-hcded', v => {}], ['i-income', v => {}], ['i-sincome', v => {}], ['i-srage', v => {}], ]; syncFields.forEach(([id]) => { const el = document.getElementById(id); if(el) {/* value already in S/PA from event listeners */} }); // Read slider values into state const sliders = [ ['sl-cage', v => S.p.cage = v], ['sl-rage', v => S.p.rage = v], ['sl-lon', v => S.p.lon = v], ['sl-ssprob', v => S.ss.prob = v], ['sl-eq', v => S.port.eq = v], ['sl-bond', v => S.port.bond = v], ['sl-eqret',v => S.port.eqret = v], ['sl-bondret',v=>S.port.bondret = v], ['sl-llcprob', v => S.port.llc.prob = v], ['sl-ipprob', v => S.port.ip.prob = v], ['sl-cpiw', v => S.inf.cpiw = v], ['sl-ppiw', v => S.inf.ppiw = v], ]; sliders.forEach(([id, setter]) => { const el = document.getElementById(id); if(el) setter(parseFloat(el.value)); }); // Read raw inflation inputs ['i-cpi','i-ppi','i-persinf'].forEach((id,i) => { const el = document.getElementById(id); if(!el) return; const v = parseFloat(el.value)||0; if(id==='i-cpi') S.inf.cpi=v; if(id==='i-ppi') S.inf.ppi=v; if(id==='i-persinf') S.inf.pers=v; }); // Snapshot all text inputs by ID for exact DOM restoration const domSnapshot = {}; const inputIds = [ 'i-income','i-sincome','i-savings','i-srage', 'i-ssage','i-ssbenefit','i-hcprem','i-hcded','i-spbenefit', 'p-stocks','p-etfs','p-cash-tax','p-tax-contrib','p-tax-ret', 'p-trad-bal','p-trad-contrib','p-trad-match','p-trad-ret', 'p-roth-bal','p-roth-contrib','p-roth-ret', 'p-hsa-bal','p-hsa-contrib','p-hsa-ret', 'p-pension','p-realestate','p-other', 'i-llcinc','i-llcstop','i-llcsale','i-llcsaleyr', 'i-ipval','i-ipyr', 'i-ghi','i-glo','i-floor','i-ceil', 'i-cpi','i-ppi','i-persinf', 'i-zipa','i-zipb', 'i-hcprem','i-hcded', ]; inputIds.forEach(id => { const el = document.getElementById(id); if(el) domSnapshot[id] = el.value; }); const payload = { S, PA: { accounts: PA.accounts, expenditures: PA.expenditures, convBkt: PA.convBkt }, bondsOn, llcMode: S.port.llc.mode, domSnapshot, savedAt: Date.now() }; localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); } catch(e) { console.warn('Save failed:', e); } } function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if(!raw) return false; const payload = JSON.parse(raw); if(!payload || !payload.S) return false; // Restore state objects Object.assign(S.p, payload.S.p || {}); Object.assign(S.port, payload.S.port || {}); if(payload.S.port && payload.S.port.llc) Object.assign(S.port.llc, payload.S.port.llc); if(payload.S.port && payload.S.port.ip) Object.assign(S.port.ip, payload.S.port.ip); Object.assign(S.strat, payload.S.strat || {}); Object.assign(S.inf, payload.S.inf || {}); Object.assign(S.ss, payload.S.ss || {}); Object.assign(S.geo.a, payload.S.geo && payload.S.geo.a ? payload.S.geo.a : {}); Object.assign(S.geo.b, payload.S.geo && payload.S.geo.b ? payload.S.geo.b : {}); if(payload.S.cats && Array.isArray(payload.S.cats)) S.cats = payload.S.cats; // Restore PA if(payload.PA) { if(payload.PA.accounts) Object.assign(PA.accounts, payload.PA.accounts); if(payload.PA.expenditures) PA.expenditures = payload.PA.expenditures; if(payload.PA.convBkt) PA.convBkt = payload.PA.convBkt; } // Restore bondsOn if(typeof payload.bondsOn === 'boolean') bondsOn = payload.bondsOn; // Restore DOM inputs if(payload.domSnapshot) { Object.entries(payload.domSnapshot).forEach(([id, val]) => { const el = document.getElementById(id); if(el) el.value = val; }); } // Restore sliders const sliderMap = { 'sl-cage': S.p.cage, 'sl-rage': S.p.rage, 'sl-lon': S.p.lon, 'sl-ssprob': S.ss.prob, 'sl-eq': S.port.eq, 'sl-bond': S.port.bond, 'sl-eqret': S.port.eqret, 'sl-bondret': S.port.bondret, 'sl-llcprob': S.port.llc.prob, 'sl-ipprob': S.port.ip.prob, 'sl-cpiw': S.inf.cpiw, 'sl-ppiw': S.inf.ppiw, }; Object.entries(sliderMap).forEach(([id, val]) => { const el = document.getElementById(id); if(el) el.value = val; }); // Restore slider display labels setText('d-cage', S.p.cage); setText('d-rage', S.p.rage); setText('d-lon', S.p.lon); setText('d-ssprob', S.ss.prob + '%'); setText('d-eq', S.port.eq + '%'); setText('d-bond', S.port.bond + '%'); setText('d-cash', Math.max(0, 100 - S.port.eq - S.port.bond) + '%'); setText('d-eqret', S.port.eqret + '%'); setText('d-bondret', S.port.bondret + '%'); setText('d-llcprob', S.port.llc.prob + '%'); setText('d-ipprob', S.port.ip.prob + '%'); setText('d-cpiw', S.inf.cpiw + '%'); setText('d-ppiw', S.inf.ppiw + '%'); setText('d-persw', Math.max(0, 100 - S.inf.cpiw - S.inf.ppiw) + '%'); setText('m-ytr', Math.max(0, S.p.rage - S.p.cage)); setText('m-rdur', Math.max(0, S.p.lon - S.p.rage) + ' yrs'); // Restore SS prob bar const ssBar = document.getElementById('ss-pm'); if(ssBar) ssBar.style.width = S.ss.prob + '%'; // Restore bond toggle UI const bondTog = document.getElementById('tog-bonds'); if(bondTog) { bondTog.classList.toggle('on', bondsOn); setText('bond-tog-label', bondsOn ? 'ON' : 'OFF'); const ctrl = document.getElementById('bond-controls'); const note = document.getElementById('bond-off-note'); if(ctrl) ctrl.style.display = bondsOn ? 'block' : 'none'; if(note) note.style.display = bondsOn ? 'none' : 'block'; } // Restore LLC mode setLLC(S.port.llc.mode); // Restore toggle states const toggles = [ ['tog-ss', S.ss.on, 'ss-sec'], ['tog-llc', S.port.llc.on, 'llc-sec'], ]; toggles.forEach(([id, val, secId]) => { const el = document.getElementById(id); if(el) el.classList.toggle('on', val); const sec = document.getElementById(secId); if(sec) { sec.style.opacity = val ? '1' : '.3'; sec.style.pointerEvents = val ? 'all' : 'none'; } }); const ipTog = document.getElementById('tog-ip'); if(ipTog) ipTog.classList.toggle('on', S.port.ip.on); const ipSec = document.getElementById('ip-sec'); if(ipSec) { ipSec.style.opacity = S.port.ip.on ? '1' : '.3'; ipSec.style.pointerEvents = S.port.ip.on ? 'all' : 'none'; } console.log('State restored from', new Date(payload.savedAt).toLocaleString()); return true; } catch(e) { console.warn('Load failed:', e); return false; } } function clearState() { localStorage.removeItem(STORAGE_KEY); location.reload(); } // Auto-save on any state change — debounced 800ms let saveTimer = null; function scheduleSave() { clearTimeout(saveTimer); saveTimer = setTimeout(saveState, 800); } // ── INIT ── injectProfileChart(); // ── TEXT INPUT LIVE LISTENERS ── // Profile inputs ['i-income','i-sincome','i-savings','i-srage'].forEach(id=>{ const el=document.getElementById(id);if(!el)return; el.addEventListener('input',()=>{ S.port.sav=pN(document.getElementById('i-savings')?.value||S.port.sav); updPortSum();runProjection(); }); }); ['i-ssage','i-ssbenefit','i-hcprem','i-hcded','i-spbenefit'].forEach(id=>{ const el=document.getElementById(id);if(!el)return; el.addEventListener('input',()=>{ const age=document.getElementById('i-ssage'); const ben=document.getElementById('i-ssbenefit'); if(age)S.ss.age=parseInt(age.value)||67; if(ben)S.ss.ben=pN(ben.value)||2800; updSequencing();runProjection(); }); }); // Portfolio account inputs — all wired to updPortTotals ['p-stocks','p-etfs','p-cash-tax','p-tax-contrib','p-tax-ret', 'p-trad-bal','p-trad-contrib','p-trad-match','p-trad-ret', 'p-roth-bal','p-roth-contrib','p-roth-ret', 'p-hsa-bal','p-hsa-contrib','p-hsa-ret', 'p-pension','p-realestate','p-other'].forEach(id=>{ const el=document.getElementById(id);if(!el)return; el.addEventListener('input',()=>updPortTotals()); }); // LLC inputs ['i-llcinc','i-llcstop','i-llcsale','i-llcsaleyr'].forEach(id=>{ const el=document.getElementById(id);if(!el)return; el.addEventListener('input',()=>{ const inc=document.getElementById('i-llcinc'); const stop=document.getElementById('i-llcstop'); const sale=document.getElementById('i-llcsale'); const saleyr=document.getElementById('i-llcsaleyr'); if(inc)S.port.llc.inc=pN(inc.value); if(stop)S.port.llc.stop=parseInt(stop.value)||2040; if(sale)S.port.llc.sale=pN(sale.value); if(saleyr)S.port.llc.saleyr=parseInt(saleyr.value)||2038; runProjection(); }); }); // IP event inputs ['i-ipval','i-ipyr'].forEach(id=>{ const el=document.getElementById(id);if(!el)return; el.addEventListener('input',()=>{ const val=document.getElementById('i-ipval'); const yr=document.getElementById('i-ipyr'); if(val)S.port.ip.val=pN(val.value); if(yr)S.port.ip.yr=parseInt(yr.value)||2030; const prob=parseFloat(document.getElementById('sl-ipprob')?.value||25); setText('ip-ev',fC(S.port.ip.val*(prob/100))); runProjection(); }); }); // Strategy inputs ['i-ghi','i-glo','i-floor','i-ceil'].forEach(id=>{ const el=document.getElementById(id);if(!el)return; el.addEventListener('input',()=>{ const ghi=document.getElementById('i-ghi'); const glo=document.getElementById('i-glo'); const fl=document.getElementById('i-floor'); const ce=document.getElementById('i-ceil'); if(ghi)S.strat.ghi=parseFloat(ghi.value)||5.5; if(glo)S.strat.glo=parseFloat(glo.value)||3.5; if(fl)S.strat.floor=pN(fl.value)||48000; if(ce)S.strat.ceil=pN(ce.value)||120000; runProjection(); }); }); // Restore persisted state if available const stateRestored = loadState(); try{renderCats();}catch(e){console.warn('renderCats:',e);} try{updPortSum();}catch(e){console.warn('updPortSum:',e);} try{updInf();}catch(e){console.warn('updInf:',e);} try{updGeo('a');updGeo('b');}catch(e){console.warn('updGeo:',e);} try{updPortTotals();}catch(e){console.warn('updPortTotals:',e);} try{updSequencing();}catch(e){console.warn('updSequencing:',e);} try{renderExps();}catch(e){console.warn('renderExps:',e);} try{updActions();}catch(e){console.warn('updActions:',e);} try{runProjection();}catch(e){console.warn('runProjection:',e);} try{updSpeed();}catch(e){console.warn('updSpeed:',e);} }); 'renderCats:',e);} try{updPortSum();}catch(e){console.warn('updPortSum:',e);} try{updInf();}catch(e){console.warn('updInf:',e);} try{updGeo('a');updGeo('b');}catch(e){console.warn('updGeo:',e);} try{updPortTotals();}catch(e){console.warn('updPortTotals:',e);} try{updSequencing();}catch(e){console.warn('updSequencing:',e);} try{renderExps();}catch(e){console.warn('renderExps:',e);} try{updActions();}catch(e){console.warn('updActions:',e);} try{runProjection();}catch(e){console.warn('runProjection:',e);} try{updSpeed();}catch(e){console.warn('updSpeed:',e);} });
Smart Calculator
No field selected
0
Click any input field to link it · Set Field replaces · Add/Sub adjusts