Skip to content

Instantly share code, notes, and snippets.

@StephenBrown2
Last active May 24, 2026 14:37
Show Gist options
  • Select an option

  • Save StephenBrown2/6cf278ac3e109ec20489c385f6c8e562 to your computer and use it in GitHub Desktop.

Select an option

Save StephenBrown2/6cf278ac3e109ec20489c385f6c8e562 to your computer and use it in GitHub Desktop.
Budget Calc
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<title>2026 Household Budget · Delaware, Ohio</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500;600&display=swap');
:root {
--bg:#f7f5f0;--surface:#fff;--s2:#f0ede6;
--border:#e2ddd4;--border2:#c8c0b0;
--text:#1a1814;--t2:#6b6257;--t3:#9e9589;
--accent:#2d5a3d;--abg:#e8f2ec;
--amber:#b85c00;--ambg:#fef3e6;
--blue:#1a3a6b;--bbg:#e8eef8;
--red:#8b2020;
--r:6px;--r2:10px;
--sh:0 1px 3px rgba(0,0,0,.06),0 1px 2px rgba(0,0,0,.04);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{overflow-x:hidden;width:100%}
html{font-size:20px}
body{font-family:'DM Sans',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}
.hdr{background:var(--text);color:#f7f5f0;padding:18px 24px 14px;display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;position:fixed;top:0;left:0;right:0;z-index:100;box-shadow:0 2px 10px rgba(0,0,0,.25)}
.hdr h1{font-family:'DM Serif Display',serif;font-size:1.45rem;font-weight:400;letter-spacing:-.02em}
.hdr-sub{font-size:.7rem;color:#a09880;font-family:'DM Mono',monospace}
.hdr-badge{margin-left:auto;font-size:.63rem;font-family:'DM Mono',monospace;color:#a09880;border:1px solid #3a3630;padding:3px 10px;border-radius:20px;white-space:nowrap}
.ctrl-bar{background:var(--surface);border-bottom:1px solid var(--border);padding:14px 24px;display:flex;flex-direction:column;gap:12px;box-shadow:var(--sh)}
.ctrl-row{display:flex;flex-wrap:wrap;gap:14px;align-items:flex-end}
.ctrl-grp{display:flex;flex-direction:column;gap:5px;min-width:0}
.ctrl-lbl{font-size:.63rem;font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:.08em;color:var(--t2)}
.sal-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
input.sal-num{font-family:'DM Serif Display',serif;font-size:1.75rem;color:var(--text);border:none;border-bottom:2px solid transparent;background:transparent;outline:none;width:150px;padding:1px 0;cursor:text;-moz-appearance:textfield}
input.sal-num:focus{border-bottom-color:var(--text)}
input.sal-num::-webkit-outer-spin-button,input.sal-num::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.sal-hint{font-size:.68rem;color:var(--t3);font-family:'DM Mono',monospace;white-space:nowrap}
input[type=range]{-webkit-appearance:none;appearance:none;width:200px;max-width:100%;height:4px;background:var(--border);border-radius:2px;outline:none;cursor:pointer}
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:var(--text);cursor:pointer;border:3px solid var(--surface);box-shadow:0 0 0 1px var(--text)}
input[type=range].slim{width:90px;height:3px}
input[type=range].slim::-webkit-slider-thumb{width:14px;height:14px;border:2px solid var(--surface);box-shadow:0 0 0 1px var(--t2);background:var(--t2)}
.pill{display:flex;border:1px solid var(--border);border-radius:var(--r);overflow:hidden;font-size:.7rem;flex-wrap:wrap}
.pill input[type=radio]{display:none}
.pill label{padding:6px 10px;cursor:pointer;color:var(--t2);white-space:nowrap;border-right:1px solid var(--border);user-select:none;transition:background .12s,color .12s}
.pill label:last-of-type{border-right:none}
.pill input[type=radio]:checked+label{background:var(--text);color:#f7f5f0;font-weight:500}
.num-inp{display:flex;align-items:center;gap:5px}
.num-inp span{font-size:.8rem;color:var(--t2)}
input.hlc-num{width:88px;font-family:'DM Mono',monospace;font-size:.8rem;padding:5px 7px;border:1px solid var(--border);border-radius:var(--r);background:var(--bg);color:var(--text);outline:none;-moz-appearance:textfield}
input.hlc-num:focus{border-color:var(--text)}
input.hlc-num::-webkit-outer-spin-button,input.hlc-num::-webkit-inner-spin-button{-webkit-appearance:none}
.metrics{padding:12px 24px;display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px}
.metric{background:var(--surface);border:1px solid var(--border);border-radius:var(--r2);padding:10px 12px;box-shadow:var(--sh)}
.m-l{font-size:.6rem;font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:.07em;color:var(--t3);margin-bottom:3px}
.m-v{font-family:'DM Serif Display',serif;font-size:1.15rem;color:var(--text);line-height:1.2}
.m-s{font-size:.6rem;color:var(--t3);margin-top:2px;font-family:'DM Mono',monospace}
.metric.grn .m-v{color:var(--accent)}
.metric.amb .m-v{color:var(--amber)}
.metric.red .m-v{color:var(--red)}
.warns{padding:0 24px 8px;display:flex;flex-direction:column;gap:5px}
.warn-box{background:var(--ambg);border:1px solid #e8c080;border-radius:var(--r2);padding:8px 12px;font-size:.73rem;color:var(--amber);line-height:1.5}
.warn-box strong{font-weight:500}
.info-box{background:var(--bbg);border:1px solid #b0c0de;border-radius:var(--r2);padding:8px 12px;font-size:.73rem;color:var(--blue);line-height:1.5}
.info-box strong{font-weight:500}
.main{padding:0 24px 32px;display:grid;grid-template-columns:1fr 360px;gap:12px;align-items:start}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r2);overflow:hidden;box-shadow:var(--sh)}
.card-hd{padding:9px 13px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;background:var(--s2)}
.card-hd h2{font-family:'DM Mono',monospace;font-size:.65rem;font-weight:500;text-transform:uppercase;letter-spacing:.08em;color:var(--t2)}
.hd-r{font-family:'DM Mono',monospace;font-size:.7rem;color:var(--t3)}
.tbl-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;width:100%;max-width:100%}
table{width:100%;border-collapse:collapse;min-width:400px}
thead th{padding:6px 11px;text-align:right;font-size:.6rem;font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:.06em;color:var(--t3);border-bottom:1px solid var(--border);background:var(--s2);white-space:nowrap}
.th-short{display:none}
thead th:first-child{text-align:left}
tbody tr{border-bottom:1px solid var(--border)}
tbody tr:last-child{border-bottom:none}
tbody tr:hover{background:var(--bg)}
td{padding:5px 11px;font-size:.77rem;text-align:right;color:var(--text)}
td:first-child{text-align:left;color:var(--t2)}
tr.g-row td{background:var(--s2);padding:5px 11px;font-size:.6rem;font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:.07em;color:var(--t3)}
tr.g-row td:not(:first-child){text-align:right;color:var(--text);font-size:.72rem;letter-spacing:0;text-transform:none;font-weight:500}
tr.g-row td .dot{display:inline-block;width:7px;height:7px;border-radius:1px;margin-right:5px;vertical-align:middle}
tr.sub-row td{font-weight:500;font-size:.77rem;background:var(--bg);font-family:'DM Mono',monospace}
tr.sub-row td:first-child{font-weight:400;color:var(--t2)}
tr.ttl-row td{font-weight:600;border-top:2px solid var(--text);padding:9px 11px;font-family:'DM Serif Display',serif;font-size:.88rem}
tr.ttl-row td:first-child{font-family:'DM Mono',monospace;font-weight:500;font-size:.63rem;letter-spacing:.05em;text-transform:uppercase;color:var(--t2)}
tr.ttl-row.ok td{color:var(--accent)}
tr.ttl-row.over td{color:var(--red)}
tr.fl-line td{font-style:italic}
tr.fl-line td:first-child{font-style:normal}
tr.life-auto td:first-child::after{content:' ↑↑';font-size:.58rem;color:var(--accent);vertical-align:super}
tr.life-none td{color:var(--t3)}
.note{font-size:.61rem;color:var(--t3);display:block}
.mono{font-family:'DM Mono',monospace}
.life-badge{display:inline-block;font-size:.57rem;padding:1px 5px;border-radius:3px;background:var(--abg);color:var(--accent);font-family:'DM Mono',monospace;margin-left:4px;vertical-align:middle}
.life-none-badge{background:var(--s2);color:var(--t3)}
.rcol{display:flex;flex-direction:column;gap:9px}
.sc-hd{padding:9px 13px;border-bottom:1px solid var(--border);background:var(--s2)}
.sc-hd h3{font-family:'DM Mono',monospace;font-size:.65rem;font-weight:500;text-transform:uppercase;letter-spacing:.07em;color:var(--t2)}
.t-row{display:flex;justify-content:space-between;align-items:baseline;padding:6px 13px;font-size:.76rem;border-bottom:1px solid var(--border);gap:8px}
.t-row:last-child{border-bottom:none}
.t-row .tl{color:var(--t2);flex:1}
.t-row .tv{font-family:'DM Mono',monospace;font-weight:500;color:var(--text);white-space:nowrap}
.tn{font-size:.6rem;color:var(--t3);display:block}
.t-total{display:flex;justify-content:space-between;align-items:center;padding:8px 13px;font-size:.8rem;font-weight:500;background:var(--bg);border-top:1px solid var(--border2);gap:8px}
.t-total .tl{color:var(--t2)}
.t-total .tv{font-family:'DM Mono',monospace;color:var(--text)}
.a-bar{display:flex;height:16px;border-radius:3px;overflow:hidden;margin:10px 13px 7px}
.a-seg{transition:width .3s}
.a-leg{display:flex;flex-wrap:wrap;gap:7px;padding:0 13px 10px}
.leg-i{display:flex;align-items:center;gap:3px;font-size:.6rem;color:var(--t2);font-family:'DM Mono',monospace}
.leg-dot{width:7px;height:7px;border-radius:1px;flex-shrink:0}
.fx-row{display:flex;align-items:center;padding:8px 13px;gap:8px;border-bottom:1px solid var(--border)}
.fx-row:last-child{border-bottom:none}
.fx-dot{width:9px;height:9px;border-radius:2px;flex-shrink:0}
.fx-lbl{flex:1;font-size:.76rem;color:var(--t2);min-width:0}
.fx-sub{font-size:.6rem;color:var(--t3);display:block;margin-top:1px}
.fx-val{font-family:'DM Mono',monospace;font-size:.8rem;font-weight:500;color:var(--text);min-width:54px;text-align:right;white-space:nowrap}
.fx-row.hl{background:var(--abg)}
.fx-row.hl .fx-val{color:var(--accent)}
.fx-row.bl{background:var(--bbg)}
.fx-row.bl .fx-val{color:var(--blue)}
.fx-row.wn{background:var(--ambg)}
.fx-row.wn .fx-val{color:var(--amber)}
.flow-wrap{background:var(--bg);border-top:1px solid var(--border);padding:10px 13px}
.flow-title{font-size:.6rem;font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:.07em;color:var(--t3);margin-bottom:7px}
.flow-steps{display:flex;flex-direction:column}
.flow-step{display:flex;align-items:stretch}
.flow-spine{display:flex;flex-direction:column;align-items:center;width:20px;flex-shrink:0}
.flow-circle{width:14px;height:14px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.55rem;font-weight:600;color:#fff;flex-shrink:0;margin-top:2px}
.flow-line{flex:1;width:2px;background:var(--border);margin:0 auto}
.flow-content{flex:1;padding:2px 0 8px 8px;display:flex;justify-content:space-between;align-items:baseline;gap:4px;font-size:.73rem}
.flow-name{color:var(--t2)}
.flow-tag{font-size:.58rem;color:var(--t3);font-family:'DM Mono',monospace}
.flow-step:last-child .flow-line{background:transparent}
.pay-area{padding:10px 13px;background:var(--bg);border-top:1px solid var(--border)}
.pay-date{font-family:'DM Serif Display',serif;font-size:1.05rem;color:var(--accent);text-align:center;padding:3px 0}
.pay-int{font-size:.65rem;font-family:'DM Mono',monospace;color:var(--t3);text-align:center;margin-bottom:7px}
.prog{height:6px;background:var(--border);border-radius:3px;overflow:hidden;margin-bottom:5px}
.prog-fill{height:100%;border-radius:3px;background:var(--accent);transition:width .4s}
.prog-lbls{display:flex;justify-content:space-between;font-size:.6rem;font-family:'DM Mono',monospace;color:var(--t3)}
.pay-note{font-size:.7rem;color:var(--t2);padding-top:7px;line-height:1.5}
.pay-note strong{color:var(--text);font-weight:500}
.pay-stat{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px}
.pay-stat-item{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);padding:6px 8px}
.pay-stat-l{font-size:.58rem;font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:.06em;color:var(--t3);margin-bottom:2px}
.pay-stat-v{font-family:'DM Mono',monospace;font-size:.78rem;font-weight:500;color:var(--text)}
.ded-tag{display:inline-block;font-size:.57rem;padding:1px 5px;border-radius:3px;font-family:'DM Mono',monospace;margin-left:4px;vertical-align:middle}
.ded-tag.wn{background:var(--ambg);color:var(--amber)}
.ded-tag.ok{background:var(--abg);color:var(--accent)}
/* Life insurance tier table in side panel */
.life-tiers{border:0.5px solid var(--border);border-radius:var(--r);overflow:hidden;margin:10px 13px}
.life-tier-row{display:grid;grid-template-columns:1fr auto auto;padding:5px 9px;font-size:.7rem;border-bottom:0.5px solid var(--border);align-items:center;gap:8px}
.life-tier-row:last-child{border-bottom:none}
.life-tier-row.selected{background:var(--abg)}
.life-tier-row .lt-lbl{color:var(--t2)}
.life-tier-row .lt-mo{font-family:'DM Mono',monospace;font-weight:500;color:var(--text)}
.life-tier-row .lt-tag{font-size:.58rem;padding:1px 5px;border-radius:3px;font-family:'DM Mono',monospace;white-space:nowrap}
.lt-tag.auto{background:var(--abg);color:var(--accent)}
.lt-tag.manual{background:var(--bbg);color:var(--blue);font-weight:500}
.lt-tag.would-auto{background:var(--s2);color:var(--t3)}
.lt-tag.avail{background:var(--s2);color:var(--t3)}
.lt-tag.unafford{background:var(--redbg,#fdeaea);color:var(--red)}
.life-tier-row{cursor:pointer;transition:background .1s}
.life-tier-row:hover:not(.selected){background:var(--s2)}
.life-reset{display:flex;align-items:center;justify-content:space-between;padding:7px 13px 5px;font-size:.7rem;color:var(--blue);border-bottom:1px solid var(--border)}
.life-reset button{font-size:.68rem;font-family:'DM Mono',monospace;padding:3px 9px;border:1px solid var(--blue);border-radius:var(--r);background:var(--bbg);color:var(--blue);cursor:pointer}
.life-reset button:hover{background:var(--blue);color:#fff}
footer{padding:12px 24px;border-top:1px solid var(--border);font-size:.6rem;font-family:'DM Mono',monospace;color:var(--t3);line-height:1.7;background:var(--surface)}
.foot-list{list-style:none;padding:0;margin:0}
.foot-list li{display:inline}
.foot-list li:not(:last-child)::after{content:' · '}
/* ── W-4 grid layout (33/66) ── */
.w4-grid{display:grid;grid-template-columns:1fr 2fr}
.w4-lbl,.w4-val{padding:7px 13px;border-bottom:1px solid var(--border);font-size:.76rem;line-height:1.5}
.w4-lbl{color:var(--t2)}
.w4-val{font-family:'DM Mono',monospace;font-weight:500;text-align:right;overflow-wrap:break-word}
.w4-note{font-size:.6rem;color:var(--t3);display:block;text-align:right;margin-top:2px;overflow-wrap:break-word;white-space:normal}
/* Remove border on the last row so it doesn't double up with the footer border-top */
.w4-grid>div:nth-last-child(-n+2){border-bottom:none}
.w4-footer{padding:10px 13px;border-top:1px solid var(--border);background:var(--bg)}
.w4-link{display:inline-flex;align-items:center;gap:5px;font-size:.75rem;font-family:'DM Mono',monospace;color:var(--blue);text-decoration:none;font-weight:500}
.w4-footer-note{font-size:.65rem;color:var(--t3);margin-top:5px;line-height:1.6}
/* ── Content width — centered on wide viewports (~1250px) ── */
/* Metrics/warns/main: constrain the element directly (no full-bleed background) */
.metrics,.warns,.alloc-section,.main{max-width:1250px;margin-left:auto;margin-right:auto}
.alloc-section{padding:0 24px 8px}
/* Ctrl-bar rows: inner content aligns with the 1250px grid below */
.ctrl-row{max-width:calc(1250px - 48px);margin-left:auto;margin-right:auto;width:100%}
/* Header: background bleeds to edges; padding dynamically centers content at 1250px.
max() ensures it never shrinks below the normal 24px on narrow viewports. */
.hdr{
padding-left:max(24px, calc((100% - 1250px) / 2));
padding-right:max(24px, calc((100% - 1250px) / 2));
}
@media(max-width:800px){.metrics{grid-template-columns:repeat(3,1fr);padding:10px 16px}.main{grid-template-columns:1fr;padding:0 16px 24px}.ctrl-bar{padding:12px 16px}.warns{padding:0 16px 8px}footer{padding:10px 16px}.hdr{padding:12px 16px 10px}}
@media(max-width:540px){.metrics{grid-template-columns:repeat(2,1fr);gap:6px;padding:8px 12px}.m-v{font-size:1rem}.ctrl-bar{padding:10px 12px}.hdr{padding:10px 12px}.hdr h1{font-size:1.2rem}.hdr-badge{margin-left:0}.main{padding:0 12px 20px}.warns{padding:0 12px 6px}footer{padding:8px 12px}input[type=range]{width:160px}input.sal-num{font-size:1.5rem;width:130px}}
@media(max-width:480px){
/* 2-col metrics fits Pixel 9 (412px) and similar phones */
.metrics{grid-template-columns:repeat(2,1fr)}
.m-v{font-size:.95rem}
/* Hide Annual (col 3) only — keep % take-home (col 4) */
table thead th:nth-child(3),table tbody td:nth-child(3){display:none}
table{min-width:0}
/* Show short % header, hide long one */
.th-short{display:inline}
.th-full{display:none}
/* Side-by-side .t-row: label takes remaining space, value wraps within 50% */
.t-row{flex-wrap:nowrap;align-items:flex-start;gap:4px 6px}
.t-row .tl{flex:1 1 0;min-width:0}
.t-row .tv{flex:0 0 50%;max-width:50%;white-space:normal;overflow-wrap:break-word;text-align:right}
.t-row .tn{white-space:normal;overflow-wrap:break-word}
/* W-4 grid narrows to 40/60 on mobile */
.w4-grid{grid-template-columns:2fr 3fr}
/* Footer items stack as bullets on narrow screens */
.foot-list li{display:list-item;list-style:disc;margin-left:18px;line-height:1.8}
.foot-list li:not(:last-child)::after{content:none}
}
@media(max-width:400px){.pill label{font-size:.66rem;padding:5px 7px}input[type=range]{width:130px}}
</style>
</head>
<body>
<div class="hdr">
<h1>2026 Household Budget</h1>
<span class="hdr-sub">Delaware, Ohio · MFJ · 2 children</span>
<span class="hdr-badge">Tax Year 2026</span>
</div>
<div class="ctrl-bar">
<div class="ctrl-row">
<div class="ctrl-grp" style="flex:1;min-width:200px">
<span class="ctrl-lbl">Gross annual salary</span>
<div class="sal-row">
<input class="sal-num" type="text" inputmode="numeric" id="salNum" value="$150,000"
onfocus="onSalFocus()" oninput="onSalInput(this.value)" onblur="onSalBlur(this.value)"
autocomplete="off" title="Type a salary between $100,000 and $250,000">
<input type="range" id="salSlider" min="100000" max="250000" step="1000" value="150000"
oninput="onSalSlider(this.value)">
<span class="sal-hint">$100k – $250k</span>
</div>
</div>
</div>
<div class="ctrl-row">
<div class="ctrl-grp">
<span class="ctrl-lbl">401k strategy</span>
<div class="pill">
<input type="radio" name="strat" id="s1" value="all-traditional" checked onchange="recalc()"><label for="s1">Max take-home</label>
<input type="radio" name="strat" id="s2" value="hybrid" onchange="recalc()"><label for="s2">Max Roth (12%)</label>
<input type="radio" name="strat" id="s3" value="all-roth" onchange="recalc()"><label for="s3">All Roth</label>
</div>
</div>
<div class="ctrl-grp">
<span class="ctrl-lbl">Budget phase</span>
<div class="pill">
<input type="radio" name="ph" id="p1" value="1" checked onchange="recalc()"><label for="p1">Phase 1 — Car savings</label>
<input type="radio" name="ph" id="p2" value="2" onchange="recalc()"><label for="p2">Phase 2 — HELOC paydown</label>
</div>
</div>
<div class="ctrl-grp">
<span class="ctrl-lbl">HELOC balance</span>
<div class="num-inp"><span>$</span>
<input class="hlc-num" type="number" id="helocBal" value="20000" step="500" min="0" max="100000" oninput="recalc()">
</div>
</div>
</div>
</div>
<div class="metrics" id="metrics"></div>
<div class="warns" id="warns"></div>
<div class="alloc-section">
<div class="card">
<div class="sc-hd"><h3>Take-home allocation</h3></div>
<div class="a-bar" id="aBar"></div>
<div class="a-leg" id="aLeg"></div>
</div>
</div>
<div class="main">
<div>
<div class="card">
<div class="card-hd"><h2>Complete budget</h2><span class="hd-r" id="balChk"></span></div>
<div class="tbl-wrap"><table id="tbl"></table></div>
</div>
</div>
<div class="rcol">
<!-- Flexible allocation (phase slider controls) -->
<div class="card">
<div class="sc-hd"><h3 id="fcTitle">Flexible allocation</h3></div>
<div id="fxCtrls"></div>
<div id="fxFlow"></div>
<div class="pay-area" id="payArea"></div>
</div>
<!-- 3. Term life -->
<div class="card"><div class="sc-hd"><h3>Term life — auto-selected tier</h3></div><div id="lifeCard"></div></div>
<!-- 4. Tax breakdown -->
<div class="card"><div class="sc-hd"><h3>Tax breakdown</h3></div><div id="taxRows"></div></div>
<!-- 5. Itemized deductions -->
<div class="card"><div class="sc-hd"><h3>Itemized deductions</h3></div><div id="dedRows"></div></div>
<!-- 6. W-4 guide -->
<div class="card"><div class="sc-hd"><h3>W-4 withholding guide</h3></div><div id="w4Rows"></div></div>
</div>
</div>
<footer id="foot"></footer>
<script>
// ── Constants ─────────────────────────────────────────────────────────────────
const K={
maxK:24500,hsa:8750,hlth:6500,
stdDed:32200,br10:24800,br12:100800,
ssRate:.062,ssBase:184500,medRate:.0145,
ohEx:4800,ohT1:26050,ohT2:100000,ohR1:.0275,ohR2:.035,
cityR:.0185,ctc:4000,
titheR:.10, charityR:.05, // 10% + 5% = 15% total (unchanged for tax)
mortInt:16258.56,propTax:4923.12,saltCap:10000,hlcR:.0825/12,
// Housing
mort:2496.18,elec:149,gas:128,water:97,net:45,
// Food
groc:750,cow:83.33,pig:62.50,
// Transportation (fuel moved from savings; autoMaint new)
aIns:133.33,reg:25.58,fuel:100,autoMaint:50,
// Communication
cell:57.50,
// Insurance (annual premiums converted to monthly)
umbrella:800/12, jewelry:30/12,
// Financial & subscriptions
fp:250,ira:41.67,subs:90.22,
// Giving add-on
gifts:80,
// Savings goals (fuel/trns removed from here — now under Transportation)
vac:200,spF:40,seF:40,date:40,phones:30,fun:20,
// Flex priority caps
CAR_BASE:500,HG_MAX:500,
// Term life insurance options — sorted most expensive → cheapest
// Affordability criterion: flexNoLife - opt.mo >= hlcMin + CAR_BASE
LIFE:[
{label:'2MM / 20-year',mo:518.64,cov:2000000,term:20},
{label:'2MM / 10-year',mo:355.04,cov:2000000,term:10},
{label:'1MM / 20-year',mo:204.02,cov:1000000,term:20},
{label:'1MM / 10-year',mo:135.62,cov:1000000,term:10},
{label:'500k / 20-year',mo:128.32,cov:500000,term:20},
{label:'500k / 10-year',mo:86.92,cov:500000,term:10},
],
};
let carTarget=500,hgP2=500;
let lifeOverrideIdx=null; // null=auto-select; integer=manual override index into K.LIFE
function setLifeTier(idx){
// Clicking the currently-active manual tier resets back to auto
lifeOverrideIdx=(lifeOverrideIdx===idx)?null:idx;
recalc();
}
// ── Salary sync — formatted display via Intl.NumberFormat ───────────────────
const SAL_FMT = new Intl.NumberFormat('en-US', {
style: 'currency', currency: 'USD',
minimumFractionDigits: 0, maximumFractionDigits: 0,
});
function parseSalary(s){return parseInt(String(s).replace(/[^0-9]/g,''),10)||0}
function fmtSalary(v){return SAL_FMT.format(v)}
function clampSal(v){return Math.round(Math.min(Math.max(100000,isNaN(v)?150000:v),250000)/1000)*1000}
// On focus: strip formatting so user types a plain number
function onSalFocus(){const inp=document.getElementById('salNum');inp.value=parseSalary(inp.value)||150000;inp.select()}
// On input: update slider + recalc when the typed value is in range
function onSalInput(v){const n=parseSalary(v);if(n>=100000&&n<=250000){document.getElementById('salSlider').value=Math.round(n/1000)*1000;recalc()}}
// On blur: clamp, reformat with currency symbol + commas, sync slider
function onSalBlur(v){const val=clampSal(parseSalary(v));document.getElementById('salNum').value=fmtSalary(val);document.getElementById('salSlider').value=val;recalc()}
// Slider moved: reformat display
function onSalSlider(v){const val=clampSal(+v);document.getElementById('salNum').value=fmtSalary(val);recalc()}
// ── Format helpers ────────────────────────────────────────────────────────────
function fmt(n,dec){
n=Number(n);dec=Number.isFinite(Number(dec))?Number(dec):0;
if(!Number.isFinite(n)||Number.isNaN(n))return'—';
const s=dec>0?Math.abs(n).toFixed(dec):Math.round(Math.abs(n)).toLocaleString();
return(n<0?'−$':'$')+s.replace(/\B(?=(\d{3})+(?!\d))/g,',');
}
function fmtM(n){const s=fmt(n);return s==='—'?'—':s+'/mo'}
function fmtP(n){n=Number(n);if(!Number.isFinite(n)||Number.isNaN(n))return'—';return(Math.abs(n)*100).toFixed(1)+'%'}
function setText(id,t){const e=document.getElementById(id);if(e)e.textContent=t}
function setClass(id,c){const e=document.getElementById(id);if(e)e.className=c}
// ── Tax math ──────────────────────────────────────────────────────────────────
function ohTax(t){t=Number(t);if(!(t>0))return 0;if(t<=K.ohT1)return 0;if(t<=K.ohT2)return(t-K.ohT1)*K.ohR1;return(K.ohT2-K.ohT1)*K.ohR1+(t-K.ohT2)*K.ohR2}
function fedTax(t){t=Number(t);if(!(t>0))return 0;return[[0,24800,.10],[24800,100800,.12],[100800,211400,.22],[211400,403550,.24],[403550,512450,.32],[512450,768600,.35],[768600,Infinity,.37]].reduce((a,[lo,hi,r])=>a+(t>lo?(Math.min(t,hi)-lo)*r:0),0)}
function margRate(t){t=Number(t);if(t<=24800)return 10;if(t<=100800)return 12;if(t<=211400)return 22;if(t<=403550)return 24;if(t<=512450)return 32;if(t<=768600)return 35;return 37}
// ── Core calculation ──────────────────────────────────────────────────────────
function calc(gross,strat,hlcBal){
gross=Number(gross)||150000;hlcBal=Number(hlcBal)||0;
const tithe=gross*K.titheR, charity=gross*K.charityR;
const don=tithe+charity; // same 15% total — used for itemized deduction
let trad=0;
if(strat==='all-traditional'){trad=K.maxK}
else if(strat==='all-roth'){trad=0}
else{for(let i=0;i<15;i++){const agi=gross-trad-K.hsa-K.hlth;const oh=ohTax(Math.max(0,agi-K.ohEx));const cy=agi*K.cityR;const sl=Math.min(oh+cy+K.propTax,K.saltCap);trad=Math.min(Math.max(0,gross-K.hsa-K.hlth-Math.max(don+K.mortInt+sl,K.stdDed)-K.br12),K.maxK)}trad=Math.round(trad)}
const roth=K.maxK-trad;
const ficaBase=gross-K.hsa-K.hlth;
const ss=Math.min(ficaBase,K.ssBase)*K.ssRate, med=ficaBase*K.medRate;
const agi=gross-trad-K.hsa-K.hlth;
const ohT=ohTax(Math.max(0,agi-K.ohEx)), cyT=agi*K.cityR;
const rawSalt=ohT+cyT+K.propTax, salt=Math.min(rawSalt,K.saltCap), saltCapped=rawSalt>K.saltCap;
const itm=don+K.mortInt+salt, useItm=itm>K.stdDed, ded=useItm?itm:K.stdDed;
const fedT=Math.max(0,agi-ded), fedTRaw=fedTax(fedT), fedTAmt=Math.max(0,fedTRaw-K.ctc);
const taxes=fedTAmt+ss+med+ohT+cyT;
const takehome=(gross-taxes-K.maxK-K.hsa-K.hlth)/12;
// Fixed base — everything EXCEPT term life (determined dynamically below)
const fixedBase=
tithe/12+charity/12+K.gifts+
K.mort+K.elec+K.gas+K.water+K.net+
K.groc+K.cow+K.pig+
K.aIns+K.reg+K.fuel+K.autoMaint+
K.cell+
K.umbrella+K.jewelry+
K.fp+K.ira+K.subs+
K.vac+K.spF+K.seF+K.date+K.phones+K.fun;
// Term life: auto-select most expensive tier within threshold
const flexNoLife=takehome-fixedBase;
const hlcMin=hlcBal*K.hlcR;
const lifeThreshold=hlcMin+K.CAR_BASE; // must leave room for HELOC floor + car base
let autoLife=null;
for(const opt of K.LIFE){
if(flexNoLife-opt.mo>=lifeThreshold){autoLife=opt;break}
}
// Apply manual override if set (any tier, even if below auto-threshold)
const selectedLife=(lifeOverrideIdx!==null)?K.LIFE[lifeOverrideIdx]:autoLife;
const lifeMo=selectedLife?selectedLife.mo:0;
const fixed=fixedBase+lifeMo;
const flex=takehome-fixed; // =flexNoLife-lifeMo
return{gross,strat,trad,roth,tithe,charity,don,agi,fedT,fedTRaw,fedTAmt,
ss,med,ohT,cyT,taxes,rawSalt,salt,saltCapped,itm,useItm,ded,
effRate:taxes/gross,margR:margRate(fedT),
takehome,fixedBase,lifeMo,autoLife,selectedLife,fixed,flex,flexNoLife,
hlcBal,hlcInt:hlcMin};
}
// ── Flex allocation: Car($500) → H/G($500) → Car(extra) → HELOC(remainder) ──
function flexAllocs(b,phase){
const flex=b.flex, hlcMin=b.hlcBal>0?b.hlcInt:0;
if(phase===1){
// Step 1: Car — up to min(carTarget, $500, flex)
let carBase=Math.min(Math.min(carTarget,K.CAR_BASE),Math.max(0,flex));
let rem=Math.max(0,flex-carBase);
// Step 2: H/G — next $500
let hg=Math.min(K.HG_MAX,rem);rem=Math.max(0,rem-hg);
// Step 3: Car extra — slider beyond $500
let carExtra=Math.min(Math.max(0,carTarget-K.CAR_BASE),rem);rem=Math.max(0,rem-carExtra);
// Step 4: HELOC — remainder
let hlc=rem;
// Enforce HELOC minimum floor: claw from H/G, then carExtra, then carBase
if(hlc<hlcMin){
let d=hlcMin-hlc;
const fH=Math.min(hg,d);hg-=fH;hlc+=fH;d-=fH;
if(d>0.01){const fCX=Math.min(carExtra,d);carExtra-=fCX;hlc+=fCX;d-=fCX}
if(d>0.01){const fCB=Math.min(carBase,d);carBase-=fCB;hlc+=fCB}
}
return{hg,carBase,carExtra,car:carBase+carExtra,hlc,hlcMin};
}else{
const maxHg=Math.min(K.HG_MAX,Math.max(0,flex));
let hg=Math.min(Math.max(0,hgP2),maxHg), hlc=flex-hg;
if(hlc<hlcMin&&b.hlcBal>0){const d=Math.min(hlcMin-hlc,hg);hg-=d;hlc+=d}
return{hg,carBase:0,carExtra:0,car:0,hlc,hlcMin};
}
}
// ── HELOC math ────────────────────────────────────────────────────────────────
function hlcMonths(bal,pmt){const r=K.hlcR,int=Number(bal)*r;if(!Number.isFinite(Number(pmt))||Number(pmt)<=int)return Infinity;if(Number(bal)<=0)return 0;const n=-Math.log(1-int/Number(pmt))/Math.log(1+r);return Number.isFinite(n)?Math.ceil(n):Infinity}
function hlcTotalInt(bal,pmt,mo){if(!Number.isFinite(mo))return Infinity;return Math.max(0,Number(pmt)*mo-Number(bal))}
function addMonths(n){if(!Number.isFinite(n))return'—';const d=new Date(2026,4,1);d.setMonth(d.getMonth()+Math.ceil(n));return d.toLocaleDateString('en-US',{month:'long',year:'numeric'})}
function getInputs(){return{sal:clampSal(+document.getElementById('salSlider').value),strat:document.querySelector('input[name=strat]:checked').value,phase:+document.querySelector('input[name=ph]:checked').value,hlc:+document.getElementById('helocBal').value||0}}
// ── Render: metrics ───────────────────────────────────────────────────────────
function renderMetrics(b,phase){
const life=b.selectedLife?b.selectedLife.label:'—';
const items=[
{l:'Monthly take-home',v:fmtM(b.takehome),s:fmt(b.takehome*12)+'/yr',c:'grn'},
{l:'Total taxes/yr',v:fmt(b.taxes),s:fmtP(b.effRate)+' eff. rate',c:''},
{l:'Federal marginal',v:b.margR+'%',s:fmt(b.fedT)+' taxable',c:b.margR>12?'amb':''},
{l:'Fixed expenses',v:fmtM(b.fixed),s:'incl. '+fmtM(b.lifeMo)+' life ins.',c:''},
{l:'Flex pool',v:fmtM(b.flex),s:phase===1?'car·H/G·car+·HELOC':'H/G·HELOC',c:b.flex<0?'red':b.flex<200?'amb':'grn'},
{l:'Life insurance',v:b.selectedLife?b.selectedLife.label:'Not affordable',s:b.selectedLife?fmtM(b.lifeMo):'increase salary',c:b.selectedLife?'':'amb'},
];
document.getElementById('metrics').innerHTML=items.map(m=>`<div class="metric ${m.c}"><div class="m-l">${m.l}</div><div class="m-v">${m.v}</div><div class="m-s">${m.s}</div></div>`).join('');
}
// ── Render: warnings ──────────────────────────────────────────────────────────
function renderWarns(b,phase){
const fa=flexAllocs(b,phase), ws=[], info=[];
if(b.flex<0)ws.push(`<strong>Over budget by ${fmt(-b.flex)}/mo.</strong> Fixed expenses exceed take-home.`);
if(fa.hlc<fa.hlcMin-0.5&&b.hlcBal>0)ws.push(`<strong>HELOC minimum not met</strong> even after clawback. Balance will grow.`);
if(b.margR>12)ws.push(`<strong>Marginal rate ${b.margR}%.</strong> Switch to "Max Roth (12%)" to reduce taxable income.`);
if(b.saltCapped)ws.push(`<strong>SALT capped.</strong> State/local taxes ${fmt(b.rawSalt)} exceeds the $10,000 deduction limit.`);
if(!b.useItm)ws.push(`<strong>Standard deduction wins</strong> (${fmt(K.stdDed)} vs ${fmt(b.itm)} itemized) at this salary.`);
if(!b.selectedLife)ws.push(`<strong>Term life insurance not yet affordable.</strong> Increase salary to unlock coverage. Threshold: flex pool ≥ ${fmt(b.hlcInt+K.CAR_BASE)}/mo after insurance.`);
else{
const nextIdx=K.LIFE.indexOf(b.selectedLife)-1;
if(nextIdx>=0){const next=K.LIFE[nextIdx];info.push(`<strong>Life insurance auto-selected: ${b.selectedLife.label}</strong> at ${fmtM(b.lifeMo)}. Next tier (${next.label} · ${fmtM(next.mo)}) unlocks when flex pool grows by ${fmt(next.mo-b.lifeMo)}/mo more.`)}
}
document.getElementById('warns').innerHTML=ws.map(w=>`<div class="warn-box">${w}</div>`).join('')+info.map(i=>`<div class="info-box">${i}</div>`).join('');
}
// ── Render: budget table ──────────────────────────────────────────────────────
function renderTable(b,phase){
const fa=flexAllocs(b,phase);
const ti=b.tithe/12, ch=b.charity/12;
const insTotal=K.umbrella+K.jewelry+b.lifeMo;
const flexMo=fa.car+fa.hg+fa.hlc;
// Pre-compute group subtotals (mo and yr) so they appear in header rows
const G={
giv:{mo:ti+ch+K.gifts, yr:(ti+ch+K.gifts)*12},
hou:{mo:K.mort+K.elec+K.gas+K.water+K.net, yr:(K.mort+K.elec+K.gas+K.water+K.net)*12},
foo:{mo:K.groc+K.cow+K.pig, yr:K.groc*12+1000+750},
tra:{mo:K.aIns+K.reg+K.fuel+K.autoMaint, yr:(K.aIns+K.reg+K.fuel+K.autoMaint)*12},
com:{mo:K.cell, yr:K.cell*12},
ins:{mo:insTotal, yr:insTotal*12},
fin:{mo:K.fp+K.ira, yr:K.fp*12+500},
sub:{mo:K.subs, yr:K.subs*12},
sav:{mo:K.vac+K.spF+K.seF+K.date+K.phones+K.fun, yr:(K.vac+K.spF+K.seF+K.date+K.phones+K.fun)*12},
flx:{mo:flexMo, yr:flexMo*12},
};
const rows=[
// Subtotals now live in the group header {g,c,mo,yr} — no separate {sub} rows
{g:'Giving & gifts',c:'#2d5a3d',...G.giv},
{n:'Tithe',nt:'10% of gross · scales with salary',mo:ti,yr:b.tithe,fx:true},
{n:'Charities',nt:'5% of gross · scales with salary',mo:ch,yr:b.charity,fx:true},
{n:'Gifts — birthdays & Christmas',mo:K.gifts,yr:K.gifts*12},
{g:'Housing',c:'#1a3a6b',...G.hou},
{n:'Mortgage',nt:'P&I + escrow (taxes + ins.) · 5.25%',mo:K.mort,yr:K.mort*12},
{n:'Electricity',nt:'12-mo avg',mo:K.elec,yr:K.elec*12},
{n:'Natural gas',mo:K.gas,yr:K.gas*12},
{n:'Water',mo:K.water,yr:K.water*12},
{n:'Internet',mo:K.net,yr:K.net*12},
{g:'Food',c:'#5a7a2d',...G.foo},
{n:'Groceries',mo:K.groc,yr:K.groc*12},
{n:'Half cow sinking fund',nt:'$1,000/yr',mo:K.cow,yr:1000},
{n:'Half pig sinking fund',nt:'$750/yr',mo:K.pig,yr:750},
{g:'Transportation',c:'#7a4a1a',...G.tra},
{n:'Auto insurance',nt:'$1,600/yr',mo:K.aIns,yr:1600},
{n:'Registration sinking fund',nt:'$1,355.55 due Oct 2030',mo:K.reg},
{n:'Fuel, parking & tolls sinking fund',mo:K.fuel,yr:K.fuel*12},
{n:'Auto maintenance sinking fund',nt:'Goal: $1,000',mo:K.autoMaint,yr:K.autoMaint*12},
{g:'Communication',c:'#2d5a6b',...G.com},
{n:'Cell phone',nt:'½ of $115 shared plan',mo:K.cell,yr:K.cell*12},
{g:'Insurance',c:'#4a1a6b',...G.ins},
{n:'Umbrella policy',nt:'$800/yr',mo:K.umbrella,yr:800},
{n:'Jewelry insurance',nt:'$30/yr',mo:K.jewelry,yr:30},
...(b.selectedLife?[
{n:`Term life — ${b.selectedLife.label}`,nt:`$${(b.selectedLife.cov/1000000).toFixed(0)}MM · ${b.selectedLife.term}-year · auto-selected ↑↑`,mo:b.lifeMo,yr:b.lifeMo*12,lifeAuto:true},
]:[
{n:'Term life insurance',nt:'Not affordable at this salary',mo:0,yr:0,lifeNone:true},
]),
{g:'Financial services',c:'#4a4a5a',...G.fin},
{n:'Financial planner',mo:K.fp,yr:K.fp*12},
{n:'IRA management fees',nt:'2 × $250/yr',mo:K.ira,yr:500},
{g:'Subscriptions',c:'#2d5a6b',...G.sub},
{n:'Amazon · Costco · Google One · Tesla · Kagi',nt:'Annual',mo:50.08,yr:601},
{n:'YouTube · YNAB · FastMail · Domains · Nabu · SimpleFIN · Bitwarden',nt:'Mix of billing periods',mo:40.14,yr:482},
{g:'Savings goals',c:'#7a4a1a',...G.sav},
{n:'Vacation',mo:K.vac,yr:K.vac*12},
{n:'Spouse fun money',mo:K.spF,yr:K.spF*12},
{n:'Self fun money',mo:K.seF,yr:K.seF*12},
{n:'Date money',mo:K.date,yr:K.date*12},
{n:'New phones sinking fund',mo:K.phones,yr:K.phones*12},
{n:'General fun money',mo:K.fun,yr:K.fun*12},
// Flex — ①③ for car (priorities 1+3), ④ for HELOC
{g:phase===1?'Flexible — Phase 1':'Flexible — Phase 2',c:'#8b2020',...G.flx},
...(phase===1?[
{n:'①③ Car fund',nt:'$500 base (①) + slider extra (③)',mo:fa.car,fl:true},
{n:'② Home improvement + garden',nt:'Priority 2 · capped at $500',mo:fa.hg,fl:true},
{n:'④ HELOC',nt:'Remainder · min interest enforced',mo:fa.hlc,fl:true},
]:[
{n:'① Home improvement + garden',nt:'Priority 1 · slider up to $500',mo:fa.hg,fl:true},
{n:'② HELOC paydown',nt:'All remainder',mo:fa.hlc,fl:true},
]),
];
const total=b.fixed+fa.car+fa.hg+fa.hlc;
const diff=b.takehome-total, ok=Math.abs(diff)<0.5;
let h=`<thead><tr><th>Line item</th><th>Monthly</th><th>Annual</th><th><span class="th-full">% take-home</span><span class="th-short">%</span></th></tr></thead><tbody>`;
for(const r of rows){
if(r.g!==undefined){
// Group header row — show subtotals in all four columns
const pct=(Number.isFinite(r.mo)&&b.takehome>0)?r.mo/b.takehome:NaN;
h+=`<tr class="g-row">
<td><span class="dot" style="background:${r.c}"></span>${r.g}</td>
<td>${fmt(r.mo)}</td>
<td>${r.yr!=null?fmt(r.yr):''}</td>
<td>${Number.isFinite(pct)&&pct>0?fmtP(pct):''}</td>
</tr>`;
}else{
const pct=(Number.isFinite(r.mo)&&b.takehome>0)?r.mo/b.takehome:NaN;
const cls=r.fl?'fl-line':r.lifeAuto?'life-auto':r.lifeNone?'life-none':'';
h+=`<tr class="${cls}">
<td>${r.n}${r.nt?`<span class="note">${r.nt}</span>`:''}</td>
<td>${r.fl?`<em>${fmt(r.mo)}</em>`:fmt(r.mo)}</td>
<td>${r.yr!=null?fmt(r.yr):'—'}</td>
<td>${Number.isFinite(pct)&&pct>0?fmtP(pct):'—'}</td>
</tr>`;
}
}
const cls=ok?'ok':diff<0?'over':'';
const chk=ok?'✓ balanced':diff>0?fmt(diff)+' unallocated':fmt(-diff)+' over budget';
h+=`<tr class="ttl-row ${cls}"><td>Monthly total</td><td class="mono">${fmt(total)}</td><td class="mono">${fmt(total*12)}</td><td>${chk}</td></tr></tbody>`;
document.getElementById('tbl').innerHTML=h;
document.getElementById('balChk').textContent=chk;
}
// ── Render: tax breakdown ─────────────────────────────────────────────────────
function renderTax(b){
const rows=[['Federal taxable income',fmt(b.fedT),''],['Federal income tax',fmt(b.fedTRaw),''],['Child Tax Credit (2 children)','−$4,000',''],['Federal tax after CTC',fmt(b.fedTAmt),''],['Social Security (6.2%)',fmt(b.ss),'on '+fmt(Math.min(b.gross-K.hsa-K.hlth,K.ssBase))],['Medicare (1.45%)',fmt(b.med),''],['Ohio state tax',fmt(b.ohT),''],['Delaware city (1.85%)',fmt(b.cyT),'']];
let h=rows.map(([l,v,n])=>`<div class="t-row"><span class="tl">${l}${n?`<span class="tn">${n}</span>`:''}</span><span class="tv">${v}</span></div>`).join('');
h+=`<div class="t-total"><span class="tl">Total taxes · effective rate</span><span class="tv">${fmt(b.taxes)} · ${fmtP(b.effRate)}</span></div>`;
document.getElementById('taxRows').innerHTML=h;
}
// ── Render: allocation bar ────────────────────────────────────────────────────
function renderAlloc(b,phase){
const fa=flexAllocs(b,phase),mo=b.takehome;
const givingMo=b.tithe/12+b.charity/12+K.gifts;
const segs=[
{p:givingMo/mo,c:'#2d5a3d',l:'Giving+Gifts'},
{p:(K.mort+K.elec+K.gas+K.water+K.net)/mo,c:'#1a3a6b',l:'Housing'},
{p:(K.groc+K.cow+K.pig)/mo,c:'#5a7a2d',l:'Food'},
{p:(K.aIns+K.reg+K.fuel+K.autoMaint)/mo,c:'#8b6020',l:'Transport'},
{p:(K.umbrella+K.jewelry+b.lifeMo)/mo,c:'#4a1a6b',l:'Insurance'},
{p:(K.fp+K.ira+K.subs+K.cell)/mo,c:'#4a4a5a',l:'Financial+Subs'},
{p:(K.vac+K.spF+K.seF+K.date+K.phones+K.fun)/mo,c:'#7a4a1a',l:'Savings'},
{p:Math.max(0,fa.car+fa.hg+fa.hlc)/mo,c:'#8b2020',l:'Flex'},
];
document.getElementById('aBar').innerHTML=segs.map(s=>`<div class="a-seg" style="width:${Math.max(0,Math.min(100,s.p*100)).toFixed(1)}%;background:${s.c}" title="${s.l}"></div>`).join('');
document.getElementById('aLeg').innerHTML=segs.map(s=>`<div class="leg-i"><div class="leg-dot" style="background:${s.c}"></div>${s.l} ${Number.isFinite(s.p)&&s.p>0?fmtP(s.p):'—'}</div>`).join('');
}
// ── Render: life insurance tier table ─────────────────────────────────────────
function renderLife(b){
const threshold=b.hlcInt+K.CAR_BASE;
const isManual=lifeOverrideIdx!==null;
let h='';
// Header row: show reset button when manually overriding
if(isManual){
h+=`<div class="life-reset"><span>Manual override active</span><button onclick="lifeOverrideIdx=null;recalc()">↺ Reset to auto</button></div>`;
}else{
h+=`<div style="font-size:.65rem;color:var(--t2);padding:7px 13px 4px;border-bottom:1px solid var(--border)">Auto-selects highest affordable tier · click any row to override</div>`;
}
h+=`<div class="life-tiers">`;
K.LIFE.forEach((opt,i)=>{
const remaining=b.flexNoLife-opt.mo;
const isSelected=b.selectedLife&&b.selectedLife.label===opt.label;
const isAutoTier=b.autoLife&&b.autoLife.label===opt.label;
const atThreshold=remaining>=threshold;
const canAfford=remaining>=0;
// Build tag
let tag;
if(isSelected&&isManual){
tag=`<span class="lt-tag manual">selected</span>`;
}else if(isSelected){
tag=`<span class="lt-tag auto">auto-selected</span>`;
}else if(isManual&&isAutoTier){
tag=`<span class="lt-tag would-auto">would be auto</span>`;
}else if(atThreshold){
tag=`<span class="lt-tag avail">available</span>`;
}else if(canAfford){
tag=`<span class="lt-tag avail">affordable</span>`;
}else{
tag=`<span class="lt-tag unafford">+${fmt(-remaining)}/mo over</span>`;
}
// Sub-hint shown under label
let hint='';
if(isSelected&&isManual) hint='<span style="font-size:.58rem;color:var(--t3);display:block">click to reset to auto</span>';
else if(!isSelected&&canAfford) hint='<span style="font-size:.58rem;color:var(--t3);display:block">click to select</span>';
else if(!canAfford) hint='<span style="font-size:.58rem;color:var(--red);display:block">budget goes over — click to see impact</span>';
h+=`<div class="life-tier-row${isSelected?' selected':''}" onclick="setLifeTier(${i})">
<span class="lt-lbl">${opt.label}${hint}</span>
<span class="lt-mo">${fmtM(opt.mo)}</span>
${tag}
</div>`;
});
h+=`</div>`;
if(!b.selectedLife&&!isManual){
h+=`<div style="font-size:.7rem;color:var(--amber);padding:6px 13px 10px">No tier affordable at this salary.</div>`;
}
document.getElementById('lifeCard').innerHTML=h;
}
// ── Render: flex controls (slider-safe) ───────────────────────────────────────
function renderFlex(b,phase){
const fa=flexAllocs(b,phase);
document.getElementById('fcTitle').textContent=phase===1?'Phase 1 — flexible (4-step priority)':'Phase 2 — flexible';
const ctrl=document.getElementById('fxCtrls');
if(ctrl.dataset.phase!=phase){
ctrl.dataset.phase=phase;
if(phase===1){
ctrl.innerHTML=`
<div class="fx-row bl" id="carRowP1">
<div class="fx-dot" style="background:#1a3a6b"></div>
<div class="fx-lbl">Car fund total<span class="fx-sub" id="carSubP1"></span></div>
<input type="range" class="slim" id="carSlider" min="0" max="5000" step="25" value="500"
oninput="carTarget=+this.value;recalc()">
<div class="fx-val" id="carFxValP1"></div>
</div>
<div class="fx-row hl">
<div class="fx-dot" style="background:#2d5a3d"></div>
<div class="fx-lbl">Home improvement + garden<span class="fx-sub" id="hgSubP1"></span></div>
<div class="fx-val" id="hgFxValP1"></div>
</div>
<div class="fx-row" id="hlcRowP1">
<div class="fx-dot" style="background:#b85c00"></div>
<div class="fx-lbl">HELOC<span class="fx-sub" id="hlcSubP1"></span></div>
<div class="fx-val" id="hlcFxValP1"></div>
</div>`;
}else{
ctrl.innerHTML=`
<div class="fx-row hl">
<div class="fx-dot" style="background:#2d5a3d"></div>
<div class="fx-lbl">Home improvement + garden<span class="fx-sub">Priority 1 · slider up to $500</span></div>
<input type="range" class="slim" id="hgSlider" min="0" max="500" step="25" value="500"
oninput="hgP2=+this.value;recalc()">
<div class="fx-val" id="hgFxVal2"></div>
</div>
<div class="fx-row" id="hlcRowP2">
<div class="fx-dot" style="background:#8b2020"></div>
<div class="fx-lbl">HELOC paydown<span class="fx-sub" id="hlcSubP2"></span></div>
<div class="fx-val" id="hlcFxVal2"></div>
</div>`;
}
}
if(phase===1){
const maxCar=Math.max(0,Math.floor(b.flex));
const sl=document.getElementById('carSlider');
if(sl){sl.max=maxCar;const cv=Math.min(Math.max(0,carTarget),maxCar);if(carTarget!==cv){carTarget=cv;sl.value=cv}}
const carExtraLbl=fa.carExtra>0.5?` · +${fmt(fa.carExtra)} extra`:'';
const carBaseLbl=fa.carBase<K.CAR_BASE-0.5?`${fmt(fa.carBase)} (salary-limited)`:fa.carExtra>0.5?`$500 base + slider extra${carExtraLbl}`:`$500 base · slide right for extra`;
setText('carSubP1',carBaseLbl);
setText('carFxValP1',fmtM(fa.car));
setClass('carRowP1','fx-row bl'+(fa.car>0.5?' hl':''));
const hgReduced=fa.hg<K.HG_MAX-0.5&&b.flex>K.HG_MAX;
setText('hgSubP1',`Priority 2 · capped at $500${hgReduced?' · reduced for HELOC min':''}`);
setText('hgFxValP1',fmtM(fa.hg));
const hlcAbove=fa.hlc>fa.hlcMin+0.5, hlcLow=fa.hlc<fa.hlcMin-0.5;
setText('hlcSubP1',hlcAbove?`Priority 4 · int ${fmtM(fa.hlcMin)} + principal ${fmtM(fa.hlc-fa.hlcMin)}`:hlcLow?`⚠ Floor not met · min ${fmtM(fa.hlcMin)}`:`Priority 4 · interest-only floor`);
setText('hlcFxValP1',fmtM(fa.hlc));
setClass('hlcRowP1','fx-row '+(hlcAbove?'hl':hlcLow?'wn':''));
document.getElementById('fxFlow').innerHTML=`<div class="flow-wrap">
<div class="flow-title">Flex pool step-by-step</div>
<div class="flow-steps">
<div class="flow-step"><div class="flow-spine"><div class="flow-circle" style="background:#1a3a6b">1</div><div class="flow-line"></div></div><div class="flow-content"><span class="flow-name">Car base <span class="flow-tag">first $500 (or slider if lower)</span></span><span style="font-family:'DM Mono',monospace;font-size:.73rem;font-weight:500;color:#1a3a6b">${fmtM(fa.carBase)}</span></div></div>
<div class="flow-step"><div class="flow-spine"><div class="flow-circle" style="background:#2d5a3d">2</div><div class="flow-line"></div></div><div class="flow-content"><span class="flow-name">Home/garden <span class="flow-tag">next $500</span></span><span style="font-family:'DM Mono',monospace;font-size:.73rem;font-weight:500;color:#2d5a3d">${fmtM(fa.hg)}</span></div></div>
<div class="flow-step"><div class="flow-spine"><div class="flow-circle" style="background:#1a3a6b">3</div><div class="flow-line"></div></div><div class="flow-content"><span class="flow-name">Car extra <span class="flow-tag">slider beyond $500</span></span><span style="font-family:'DM Mono',monospace;font-size:.73rem;font-weight:500;color:#1a3a6b">${fmtM(fa.carExtra)}</span></div></div>
<div class="flow-step"><div class="flow-spine"><div class="flow-circle" style="background:#b85c00">4</div><div class="flow-line"></div></div><div class="flow-content"><span class="flow-name">HELOC <span class="flow-tag">remainder</span></span><span style="font-family:'DM Mono',monospace;font-size:.73rem;font-weight:500;color:${hlcAbove?'var(--accent)':'var(--amber)'}">${fmtM(fa.hlc)}</span></div></div>
</div></div>`;
const proj=23000+fa.car*14;
const carCol=proj>=30000?'var(--accent)':'var(--amber)';
document.getElementById('payArea').innerHTML=`<div class="pay-stat"><div class="pay-stat-item"><div class="pay-stat-l">Car at mid-2027</div><div class="pay-stat-v" style="color:${carCol}">${fmt(proj)}</div></div><div class="pay-stat-item"><div class="pay-stat-l">HELOC P1 interest</div><div class="pay-stat-v">${fmt(fa.hlcMin*14)}</div></div>${fa.hlc>fa.hlcMin+0.5?`<div class="pay-stat-item"><div class="pay-stat-l">P1 principal paid</div><div class="pay-stat-v" style="color:var(--accent)">${fmt((fa.hlc-fa.hlcMin)*14)}</div></div>`:''}</div><div class="prog"><div class="prog-fill" style="width:${Math.min(100,proj/400).toFixed(1)}%;background:${carCol}"></div></div><div class="prog-lbls"><span>$0</span><span>$30k min</span><span>$40k max</span></div>`;
}else{
const maxHg=Math.min(K.HG_MAX,Math.max(0,Math.floor(b.flex)));
const sl=document.getElementById('hgSlider');
if(sl){sl.max=maxHg;const cvH=Math.min(Math.max(0,hgP2),maxHg);if(hgP2!==cvH){hgP2=cvH;sl.value=cvH}}
const hgC=Math.min(Math.max(0,hgP2),maxHg), hlcPmt=b.flex-hgC;
const paying=hlcPmt>b.hlcInt+0.5, atMin=Math.abs(hlcPmt-b.hlcInt)<=0.5;
setText('hgFxVal2',fmtM(hgC));
setText('hlcFxVal2',fmtM(hlcPmt));
setText('hlcSubP2',paying?`Paying principal · int ${fmtM(b.hlcInt)} + principal ${fmtM(hlcPmt-b.hlcInt)}`:atMin?'Interest only':'⚠ Below minimum');
setClass('hlcRowP2','fx-row '+(paying?'hl':!atMin&&b.hlcBal>0?'wn':''));
document.getElementById('fxFlow').innerHTML='';
const mos=hlcMonths(b.hlcBal,hlcPmt), intPaid=hlcTotalInt(b.hlcBal,hlcPmt,mos), payDate=addMonths(mos);
document.getElementById('payArea').innerHTML=paying?`<div class="pay-date">${payDate}</div><div class="pay-int">${Number.isFinite(mos)?mos+' months · '+fmt(intPaid)+' total interest':'—'}</div><div class="prog" style="margin:7px 0"><div class="prog-fill" style="width:4%"></div></div><div class="prog-lbls"><span>Phase 2 start</span><span>${payDate}</span></div><div class="pay-note">After payoff, <strong>${fmtM(hlcPmt)}</strong> frees up. HELOC line open until Feb 2036.</div>`:`<div style="padding:6px 0;font-size:.73rem;color:var(--t2);text-align:center">${hlcPmt<b.hlcInt&&b.hlcBal>0?'Slide H/G left to cover the '+fmtM(b.hlcInt)+' minimum.':'Slide H/G left to start paying principal.'}</div>`;
}
}
// ── Render: itemized deductions ───────────────────────────────────────────────
function renderDed(b){
const rows=[
['Tithe (10% of gross)',fmt(b.tithe),'Fully deductible as charitable contribution',''],
['Charities (5% of gross)',fmt(b.charity),'Fully deductible',''],
['Total charitable giving',fmt(b.don),'15% of gross — used for itemized deduction',''],
['Mortgage interest (2026)',fmt(K.mortInt),'Exact · Form 1098',''],
['Ohio income tax',fmt(b.ohT),'',''],
['Delaware city tax',fmt(b.cyT),'',''],
['Property tax',fmt(K.propTax),'Escrowed in mortgage',''],
['SALT subtotal',fmt(b.rawSalt),b.saltCapped?'⚠ capped at $10,000':'✓ under $10,000 cap',b.saltCapped?'wn':'ok'],
['Total itemized',fmt(b.itm),'',''],
['Standard deduction',fmt(K.stdDed),'2026 MFJ',''],
];
let h=rows.map(([l,v,n,tc])=>`<div class="t-row"><span class="tl">${l}${n?`<span class="ded-tag ${tc}">${n}</span>`:''}</span><span class="tv">${v}</span></div>`).join('');
const sav=b.useItm?(b.itm-K.stdDed)*b.margR/100:0;
h+=`<div class="t-total"><span class="tl">Using <strong>${b.useItm?'itemized':'standard'}</strong></span><span class="tv" style="color:var(--accent)">${b.useItm?'saves ~'+fmt(sav)+'/yr':''}</span></div>`;
document.getElementById('dedRows').innerHTML=h;
}
function renderW4(b){
const step4b=b.useItm?Math.max(0,Math.round(b.itm-K.stdDed)):0;
const rows=[
{step:'Step 1',lbl:'Filing status',val:'Married Filing Jointly',note:''},
{step:'Step 2',lbl:'Multiple jobs / spouse works',val:'Leave blank',note:'Single income — checking this over-withholds'},
{step:'Step 3',lbl:'Dependents credit',val:fmt(4000),note:'2 children × $2,000 Child Tax Credit'},
{step:'Step 4b',lbl:'Deductions adjustment',val:step4b>0?fmt(step4b):'Leave blank',
note:step4b>0?`Itemized ${fmt(Math.round(b.itm))} − standard ${fmt(K.stdDed)} · scales with salary`:'Taking standard deduction'},
{step:'Step 4c',lbl:'Extra withholding / period',val:'See estimator →',note:'Use for mid-year adjustments or salary changes'},
];
// CSS Grid: 1fr 2fr (33/66). Each row = two consecutive grid cells.
let h='<div class="w4-grid">';
h+=rows.map(r=>`
<div class="w4-lbl">${r.step}<span class="tn">${r.lbl}</span></div>
<div class="w4-val">${r.val}${r.note?`<span class="w4-note">${r.note}</span>`:''}</div>`).join('');
h+='</div>';
h+=`<div class="w4-footer">
<a href="https://apps.irs.gov/app/tax-withholding-estimator/" target="_blank" rel="noopener noreferrer" class="w4-link">
IRS Tax Withholding Estimator ↗
</a>
<div class="w4-footer-note">Takes ~25 min. Have ready: most recent pay stub (YTD gross + federal withheld to date) and your best estimate of H2 salary. The estimator handles mid-year salary changes and outputs a pre-filled W-4.</div>
</div>`;
document.getElementById('w4Rows').innerHTML=h;
}
function recalc(){
const{sal,strat,phase,hlc}=getInputs();
const b=calc(sal,strat,hlc);
renderMetrics(b,phase);renderWarns(b,phase);renderTable(b,phase);
renderTax(b);renderAlloc(b,phase);renderLife(b);renderFlex(b,phase);renderDed(b);renderW4(b);
const footItems=[
'2026 · MFJ · 2 children · Delaware, Ohio',
'Mortgage 5.25% on $315,904 · 2026 interest $16,259 · Property tax $4,923 escrowed',
'HELOC 8.25% variable · draw period ends Feb 2036',
'Child Tax Credit $4,000 · SALT cap $10,000',
'401k $24,500 · HSA $8,750 · Health insurance $6,500 est.',
'Charitable giving: Tithe 10% + Charities 5% = 15% of gross',
'Gifts $80/mo · Auto maintenance $50/mo · Umbrella $67/mo · Jewelry $2.50/mo',
'Term life auto-selects highest affordable tier (threshold: flex pool ≥ HELOC min + $500 car base)',
'Phase 1 flex priority: Car $500 → H/G $500 → Car extra → HELOC remainder',
'All figures estimated — consult your financial planner before filing or making changes.',
];
document.getElementById('foot').innerHTML=`<ul class="foot-list">${footItems.map(i=>`<li>${i}</li>`).join('')}</ul>`;
}
recalc();
// Keep content clear of fixed header — measures actual rendered height and reapplies on resize
(function(){
const hdr=document.querySelector('.hdr');
const ctrl=document.querySelector('.ctrl-bar');
if(!hdr||!ctrl)return;
function adjust(){ctrl.style.marginTop=hdr.offsetHeight+'px'}
adjust();
new ResizeObserver(adjust).observe(hdr);
window.addEventListener('resize',adjust);
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment