<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Beach City Painting — Estimator</title> <style> :root{ --bg:#0b1220; --card:#121a2b; --muted:#8aa0c2; --ink:#f2f6ff; --accent:#3ea7ff; --ok:#27c384; --warn:#ffb020; --br:16px; } *{box-sizing:border-box} html,body{margin:0;background:var(--bg);color:var(--ink);font-family:ui-sans-serif,system-ui,Segoe UI,Roboto,Helvetica,Arial;} .wrap{max-width:1100px;margin:32px auto;padding:0 16px} h1{font-size:28px;margin:0 0 10px} .sub{color:var(--muted);margin:0 0 24px;font-size:14px} .grid{display:grid;gap:16px} @media(min-width:900px){.grid{grid-template-columns:1fr 1fr}} .card{background:var(--card);border-radius:var(--br);padding:16px;border:1px solid #1f2b45} .card h2{margin:0 0 10px;font-size:18px} .row{display:grid;grid-template-columns:1fr 110px;gap:10px;margin:8px 0} .row > label{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px} input[type="number"], input[type="text"], select{width:100%;padding:10px 12px;border-radius:10px;border:1px solid #2a3a5d;background:#0e1526;color:var(--ink)} input[type="checkbox"]{transform:scale(1.1)} .stack{display:grid;gap:8px} .cols{display:grid;grid-template-columns:repeat(3,1fr);gap:8px} .cols4{display:grid;grid-template-columns:repeat(4,1fr);gap:8px} .note{color:var(--muted);font-size:12px} .totals{display:grid;gap:12px} .pill{display:flex;justify-content:space-between;padding:12px;border-radius:12px;background:#0e1526;border:1px solid #1d2840} .pill strong{font-weight:700} .btnbar{display:flex;gap:10px;flex-wrap:wrap} button{padding:10px 14px;border-radius:10px;border:1px solid #25507d;background:#10223a;color:#e7f1ff;cursor:pointer} button.primary{background:linear-gradient(180deg,#2d8fff,#1f7be0);border-color:#1f7be0} button.ghost{background:transparent} table{width:100%;border-collapse:collapse;border:1px solid #1f2b45;border-radius:12px;overflow:hidden} th,td{padding:10px 12px;border-bottom:1px solid #1f2b45;text-align:right} th:first-child, td:first-child{text-align:left} tfoot td{font-weight:700} .tag{display:inline-block;padding:2px 8px;border-radius:999px;background:#0e1526;border:1px solid #1d2840;color:#c6d6f3;font-size:12px} .flex{display:flex;gap:8px;align-items:center;flex-wrap:wrap} .hr{height:1px;background:#1f2b45;margin:14px 0} a.small{color:#a9c8ff;text-decoration:none;border-bottom:1px dotted #2a63b0} </style> </head> <body> <div class="wrap"> <h1>Project Estimator <span class="tag">Beach City Painting</span></h1> <p class="sub">Embeddable calculator based on your spreadsheet logic (primer + 2 coats model, doors/trim, garage, exterior). Values persist locally.</p> <div class="grid"> <section class="card"> <h2>Project & Global Settings</h2> <div class="row"><label>Project Name</label><input id="projectName" type="text" placeholder="e.g., 623 9th St — Interior"/></div> <div class="cols"> <div class="stack"> <label class="note">Markup (%)</label> <input id="markupPct" type="number" step="1" value="0"/> </div> <div class="stack"> <label class="note">Sales Tax (%)</label> <input id="taxPct" type="number" step="0.01" value="0"/> </div> <div class="stack"> <label class="note">Currency</label> <select id="currency"><option value="$">$</option><option value="€">€</option><option value="£">£</option></select> </div> </div> <div class="hr"></div> <div class="btnbar"> <button class="primary" id="calcBtn">Calculate</button> <button id="resetBtn" class="ghost">Reset</button> <button id="exportBtn">Export JSON</button> </div> </section> <section class="card"> <h2>Pricing (per unit)</h2> <p class="note">You can adjust these at any time. Interior walls/ceilings are per square foot; doors & trim are per piece or linear foot.</p> <div class="cols4"> <div class="stack"> <label class="note">Walls — Primer</label> <input id="rateWallsPrimer" type="number" step="0.01" value="0.00"/> </div> <div class="stack"> <label class="note">Walls — Coat 1</label> <input id="rateWallsC1" type="number" step="0.01" value="0.75"/> </div> <div class="stack"> <label class="note">Walls — Coat 2</label> <input id="rateWallsC2" type="number" step="0.01" value="0.65"/> </div> <div class="stack"> <label class="note">Wall Linear Factor (per sqft → LF)</label> <input id="factorWallsLF" type="number" step="0.01" value="0.25"/> </div> </div> <div class="cols4" style="margin-top:8px"> <div class="stack"> <label class="note">Ceilings — Primer</label> <input id="rateCeilPrimer" type="number" step="0.01" value="0.00"/> </div> <div class="stack"> <label class="note">Ceilings — Coat 1</label> <input id="rateCeilC1" type="number" step="0.01" value="0.75"/> </div> <div class="stack"> <label class="note">Ceilings — Coat 2</label> <input id="rateCeilC2" type="number" step="0.01" value="0.50"/> </div> <div class="stack"> <label class="note">Baseboards — per LF (per coat)</label> <input id="rateBaseLF" type="number" step="0.01" value="0.50"/> </div> </div> <div class="cols4" style="margin-top:8px"> <div class="stack"> <label class="note">Interior Door (each) — Primer</label> <input id="rateDoorIntP" type="number" step="0.01" value="0.00"/> </div> <div class="stack"> <label class="note">Interior Door — Coat 1</label> <input id="rateDoorIntC1" type="number" step="0.01" value="50.00"/> </div> <div class="stack"> <label class="note">Interior Door — Coat 2</label> <input id="rateDoorIntC2" type="number" step="0.01" value="50.00"/> </div> <div class="stack"> <label class="note">Baseboard LF Factor (sqft → LF)</label> <input id="factorBaseLF" type="number" step="0.01" value="0.21"/> </div> </div> <div class="cols4" style="margin-top:8px"> <div class="stack"> <label class="note">French Door — per coat (each)</label> <input id="rateDoorFrench" type="number" step="0.01" value="60.00"/> </div> <div class="stack"> <label class="note">Exterior Door — per coat (each)</label> <input id="rateDoorExt" type="number" step="0.01" value="60.00"/> </div> <div class="stack"> <label class="note">Casings — per LF (per coat)</label> <input id="rateCasingLF" type="number" step="0.01" value="0.50"/> </div> <div class="stack"> <label class="note">Casing LF / interior door</label> <input id="casingLFPerDoor" type="number" step="1" value="34"/> </div> </div> <div class="cols4" style="margin-top:8px"> <div class="stack"> <label class="note">Garage — Primer (per sqft)</label> <input id="rateGarageP" type="number" step="0.01" value="0.35"/> </div> <div class="stack"> <label class="note">Garage — Coat 1 (per sqft)</label> <input id="rateGarageC1" type="number" step="0.01" value="0.45"/> </div> <div class="stack"> <label class="note">Garage — Coat 2 (per sqft)</label> <input id="rateGarageC2" type="number" step="0.01" value="0.45"/> </div> <div class="stack"> <label class="note">Garage LF Factor (sqft → LF)</label> <input id="factorGarageLF" type="number" step="0.01" value="0.25"/> </div> </div> <div class="cols4" style="margin-top:8px"> <div class="stack"> <label class="note">Exterior — Primer (per sqft)</label> <input id="rateExtP" type="number" step="0.01" value="0.50"/> </div> <div class="stack"> <label class="note">Exterior — Coat 1 (per sqft)</label> <input id="rateExtC1" type="number" step="0.01" value="0.90"/> </div> <div class="stack"> <label class="note">Exterior — Coat 2 (per sqft)</label> <input id="rateExtC2" type="number" step="0.01" value="0.90"/> </div> <div class="stack"> <label class="note">Currency Decimals</label> <input id="decimals" type="number" step="1" value="2"/> </div> </div> </section> </div> <div class="grid" style="margin-top:16px"> <section class="card"> <h2>Interior — Walls</h2> <div class="cols"> <div class="stack"> <label class="note">Listed Sqft</label> <input id="wallsSqft" type="number" step="1" value="1867"/> </div> <div class="stack"> <label class="note">Height (ft)</label> <select id="wallsHeight"> <option>8</option> <option selected>10</option> <option>12</option> <option value="custom">Custom…</option> </select> <input id="wallsHeightCustom" type="number" step="0.1" placeholder="Enter custom height" style="display:none" /> </div> </div> <div class="cols4" style="margin-top:8px"> <div class="stack"> <label class="note">Use Primer?</label> <select id="wallsUsePrimer"><option value="0">No</option><option value="1">Yes</option></select> </div> <div class="stack"><label class="note">Coat 1?</label><select id="wallsUseC1"><option value="1">Yes</option><option value="0">No</option></select></div> <div class="stack"><label class="note">Coat 2?</label><select id="wallsUseC2"><option value="1">Yes</option><option value="0">No</option></select></div> <div class="stack"><label class="note">Condition Factor</label><input id="wallsCond" type="number" step="0.01" value="1.00"/></div> </div> <p class="note">Formula: <em>Linear Ft = Listed Sqft × Wall Linear Factor</em>; <em>Total Sqft = Linear Ft × Height</em>.</p> <div id="wallsOut" class="note"></div> </section> <section class="card"> <h2>Interior — Ceilings</h2> <div class="cols"> <div class="stack"> <label class="note">Ceiling Sqft</label> <input id="ceilSqft" type="number" step="1" value="1867"/> </div> <div class="stack"> <label class="note">Use Primer?</label> <select id="ceilUsePrimer"><option value="0">No</option><option value="1">Yes</option></select> </div> </div> <div class="cols"> <div class="stack"><label class="note">Coat 1?</label><select id="ceilUseC1"><option value="1">Yes</option><option value="0">No</option></select></div> <div class="stack"><label class="note">Coat 2?</label><select id="ceilUseC2"><option value="1">Yes</option><option value="0">No</option></select></div> </div> <div id="ceilOut" class="note"></div> </section> </div> <div class="grid" style="margin-top:16px"> <section class="card"> <h2>Doors & Trim</h2> <div class="cols4"> <div class="stack"><label class="note">Interior Doors</label><input id="nDoorInt" type="number" step="1" value="12"/></div> <div class="stack"><label class="note">French Doors</label><input id="nDoorFrench" type="number" step="1" value="0"/></div> <div class="stack"><label class="note">Exterior Doors</label><input id="nDoorExt" type="number" step="1" value="0"/></div> <div class="stack"><label class="note">Prime Doors?</label><select id="doorsPrime"><option value="0">No</option><option value="1">Yes</option></select></div> </div> <div class="hr"></div> <div class="cols4"> <div class="stack"><label class="note">House Sqft (for baseboard LF)</label><input id="houseSqftForBase" type="number" step="1" value="2250"/></div> <div class="stack"><label class="note">Baseboard LF (override)</label><input id="baseLFOverride" type="number" step="1" placeholder="optional"/></div> <div class="stack"><label class="note">Casing Doors (count)</label><input id="casingDoors" type="number" step="1" value="14"/></div> <div class="stack"><label class="note">Include Install Costs?</label><select id="includeInstall"><option value="0">No</option><option value="1">Yes</option></select></div> </div> <div id="trimOut" class="note"></div> </section> <section class="card"> <h2>Garage</h2> <div class="cols"> <div class="stack"><label class="note">Garage Sqft (listed)</label><input id="garageSqft" type="number" step="1" value="425"/></div> <div class="stack"><label class="note">Height (ft)</label> <select id="garageHeight"><option>8</option><option selected>10</option><option>12</option></select> </div> </div> <div class="cols"> <div class="stack"><label class="note">Use Primer?</label><select id="garageUseP"><option value="1">Yes</option><option value="0">No</option></select></div> <div class="stack"><label class="note">Coat 1?</label><select id="garageUseC1"><option value="1">Yes</option><option value="0">No</option></select></div> <div class="stack"><label class="note">Coat 2?</label><select id="garageUseC2"><option value="1">Yes</option><option value="0">No</option></select></div> </div> <div id="garageOut" class="note"></div> </section> </div> <section class="card" style="margin-top:16px"> <h2>Exterior</h2> <div class="cols"> <div class="stack"><label class="note">Perimeter (LF)</label><input id="extPerim" type="number" step="1" value="240"/></div> <div class="stack"><label class="note">Height (ft)</label><input id="extHeight" type="number" step="0.1" value="10"/></div> <div class="stack"><label class="note">Second Floor Perimeter (LF)</label><input id="extPerim2" type="number" step="1" value="160"/></div> <div class="stack"><label class="note">Second Floor Height (ft)</label><input id="extHeight2" type="number" step="0.1" value="9"/></div> </div> <div class="cols"> <div class="stack"><label class="note">Use Primer?</label><select id="extUseP"><option value="1">Yes</option><option value="0">No</option></select></div> <div class="stack"><label class="note">Coat 1?</label><select id="extUseC1"><option value="1">Yes</option><option value="0">No</option></select></div> <div class="stack"><label class="note">Coat 2?</label><select id="extUseC2"><option value="1">Yes</option><option value="0">No</option></select></div> </div> <div id="extOut" class="note"></div> </section> <section class="card" style="margin-top:16px"> <h2>Estimate Summary</h2> <div class="totals" id="summary"> <div class="pill"><span>Interior Walls</span><strong id="sumWalls">$0.00</strong></div> <div class="pill"><span>Ceilings</span><strong id="sumCeil">$0.00</strong></div> <div class="pill"><span>Doors & Trim</span><strong id="sumTrim">$0.00</strong></div> <div class="pill"><span>Garage</span><strong id="sumGarage">$0.00</strong></div> <div class="pill"><span>Exterior</span><strong id="sumExt">$0.00</strong></div> <div class="pill" style="border-color:#2b3d63"><span>Subtotal</span><strong id="sumSub">$0.00</strong></div> <div class="pill"><span>Markup</span><strong id="sumMarkup">$0.00</strong></div> <div class="pill"><span>Tax</span><strong id="sumTax">$0.00</strong></div> <div class="pill" style="background:#0b1b2e;border-color:#39527f"><span>Total</span><strong id="sumTotal">$0.00</strong></div> </div> <div style="margin-top:14px"> <table id="detailTable"> <thead> <tr><th>Line</th><th>Qty</th><th>Unit</th><th>Rate</th><th>Cost</th></tr> </thead> <tbody></tbody> <tfoot> <tr><td colspan="4">Subtotal</td><td id="tblSub" style="font-weight:700;text-align:right">$0.00</td></tr> </tfoot> </table> </div> </section> <p class="note" style="margin-top:16px">Embed on Squarespace: add a <em>Code</em> block, paste this file's HTML contents, or host the HTML and iframe it. All values persist in the browser via localStorage (no backend).</p> </div> <script> (function(){ const $ = (id)=>document.getElementById(id); const ids = [ 'projectName','markupPct','taxPct','currency','decimals', 'rateWallsPrimer','rateWallsC1','rateWallsC2','factorWallsLF', 'rateCeilPrimer','rateCeilC1','rateCeilC2','rateBaseLF','factorBaseLF', 'rateDoorIntP','rateDoorIntC1','rateDoorIntC2','rateDoorFrench','rateDoorExt','rateCasingLF','casingLFPerDoor', 'rateGarageP','rateGarageC1','rateGarageC2','factorGarageLF', 'rateExtP','rateExtC1','rateExtC2', 'wallsSqft','wallsHeight','wallsHeightCustom','wallsUsePrimer','wallsUseC1','wallsUseC2','wallsCond', 'ceilSqft','ceilUsePrimer','ceilUseC1','ceilUseC2', 'nDoorInt','nDoorFrench','nDoorExt','doorsPrime', 'houseSqftForBase','baseLFOverride','casingDoors','includeInstall', 'garageSqft','garageHeight','garageUseP','garageUseC1','garageUseC2', 'extPerim','extHeight','extPerim2','extHeight2','extUseP','extUseC1','extUseC2' ]; const storeKey = 'bcp-estimator-v1'; function fmt(n){ const dec = +$('decimals').value||2; const cur=$('currency').value; return cur + (Number(n).toFixed(dec)); } function n(v){ return Number(v)||0; } function save(){ const data = {}; ids.forEach(id=>{ const el=$(id); if(!el) return; data[id] = (el.type === 'checkbox') ? el.checked : el.value; }); localStorage.setItem(storeKey, JSON.stringify(data)); } function load(){ const raw = localStorage.getItem(storeKey); if(!raw) return; try{ const data = JSON.parse(raw); ids.forEach(id=>{ if(data[id]!==undefined){ const el=$(id); if(!el) return; if(el.type==='checkbox'){ el.checked = !!data[id]; } else { el.value = data[id]; } }}); }catch(e){ console.warn('load failed', e); } } function heightValue(selId, customId){ const sel = $(selId).value; if(sel==='custom'){ return n($(customId).value||0); } return n(sel); } function pushRow(rows, label, qty, unit, rate, coats=1){ const r = n(rate) * n(qty) * n(coats); if(r<=0) return 0; rows.push({label, qty, unit, rate, cost:r}); return r; } function calc(){ const rows=[]; // Globals const markupPct = n($('markupPct').value)/100; const taxPct = n($('taxPct').value)/100; // Walls const wallsSqft = n($('wallsSqft').value); const wallsLF = wallsSqft * n($('factorWallsLF').value); const wallsHeight = heightValue('wallsHeight','wallsHeightCustom'); const wallsTotalSqft = wallsLF * wallsHeight * n($('wallsCond').value); const wallsRates = { p: n($('rateWallsPrimer').value), c1: n($('rateWallsC1').value), c2: n($('rateWallsC2').value) }; const useWP = $('wallsUsePrimer').value==='1', useWC1 = $('wallsUseC1').value==='1', useWC2 = $('wallsUseC2').value==='1'; let sumWalls = 0; if(useWP) sumWalls += pushRow(rows, 'Interior Walls — Primer', wallsTotalSqft, 'sqft', wallsRates.p); if(useWC1) sumWalls += pushRow(rows, 'Interior Walls — Coat 1', wallsTotalSqft, 'sqft', wallsRates.c1); if(useWC2) sumWalls += pushRow(rows, 'Interior Walls — Coat 2', wallsTotalSqft, 'sqft', wallsRates.c2); $('wallsOut').innerText = `Linear Ft: ${wallsLF.toFixed(2)} | Total Sqft: ${wallsTotalSqft.toFixed(2)}`; // Ceilings const ceilSqft = n($('ceilSqft').value); const ceilRates = { p:n($('rateCeilPrimer').value), c1:n($('rateCeilC1').value), c2:n($('rateCeilC2').value) }; const useCP = $('ceilUsePrimer').value==='1', useCC1=$('ceilUseC1').value==='1', useCC2=$('ceilUseC2').value==='1'; let sumCeil=0; if(useCP) sumCeil += pushRow(rows, 'Ceilings — Primer', ceilSqft, 'sqft', ceilRates.p); if(useCC1) sumCeil += pushRow(rows, 'Ceilings — Coat 1', ceilSqft, 'sqft', ceilRates.c1); if(useCC2) sumCeil += pushRow(rows, 'Ceilings — Coat 2', ceilSqft, 'sqft', ceilRates.c2); $('ceilOut').innerText = `Ceiling Sqft: ${ceilSqft.toFixed(2)}`; // Doors & Trim const nDoorInt = n($('nDoorInt').value), nDoorFrench = n($('nDoorFrench').value), nDoorExt = n($('nDoorExt').value); const doorsPrime = $('doorsPrime').value==='1'; const dRates = { intP:n($('rateDoorIntP').value), intC1:n($('rateDoorIntC1').value), intC2:n($('rateDoorIntC2').value), french:n($('rateDoorFrench').value), ext:n($('rateDoorExt').value) }; let sumTrim=0; if(doorsPrime && dRates.intP>0) sumTrim += pushRow(rows,'Interior Doors — Primer', nDoorInt, 'each', dRates.intP); sumTrim += pushRow(rows,'Interior Doors — Coat 1', nDoorInt, 'each', dRates.intC1); sumTrim += pushRow(rows,'Interior Doors — Coat 2', nDoorInt, 'each', dRates.intC2); if(nDoorFrench>0){ sumTrim += pushRow(rows,'French Doors — Coat 1', nDoorFrench, 'each', dRates.french); sumTrim += pushRow(rows,'French Doors — Coat 2', nDoorFrench, 'each', dRates.french); } if(nDoorExt>0){ sumTrim += pushRow(rows,'Exterior Doors — Coat 1', nDoorExt, 'each', dRates.ext); sumTrim += pushRow(rows,'Exterior Doors — Coat 2', nDoorExt, 'each', dRates.ext); } // Baseboards const baseLF = $('baseLFOverride').value ? n($('baseLFOverride').value) : (n($('houseSqftForBase').value) * n($('factorBaseLF').value)); const baseRate = n($('rateBaseLF').value); // 2 coats by default (matches spreadsheet examples: $1.00,$0.50,$0.50 → if primer is $1, you could enable primer via doorsPrime or adjust rateBaseLF accordingly) sumTrim += pushRow(rows,'Baseboards (prep/paint) — Coat 1', baseLF, 'LF', baseRate); sumTrim += pushRow(rows,'Baseboards — Coat 2', baseLF, 'LF', baseRate); // Casings const casingDoors = n($('casingDoors').value); const casingLF = casingDoors * n($('casingLFPerDoor').value); const casingRate = n($('rateCasingLF').value); sumTrim += pushRow(rows,'Casings (prep/paint) — Coat 1', casingLF, 'LF', casingRate); sumTrim += pushRow(rows,'Casings — Coat 2', casingLF, 'LF', casingRate); const includeInstall = $('includeInstall').value==='1'; if(includeInstall){ // Example install placeholder using spreadsheet sample ($4,116.74 on ~392 LF → ~$10.50/LF). Adjust as needed. const approxBaseInstallRate = 10.50; // editable here if you want a fixed install adder const installCost = baseLF * approxBaseInstallRate; sumTrim += pushRow(rows,'Baseboard Install (complete)', baseLF, 'LF', approxBaseInstallRate); } $('trimOut').innerText = `Interior Doors: ${nDoorInt} | Baseboard LF: ${baseLF.toFixed(2)} | Casing LF: ${casingLF.toFixed(2)}`; // Garage const gSqft = n($('garageSqft').value); const gLF = gSqft * n($('factorGarageLF').value); const gHeight = n($('garageHeight').value); const gArea = gLF * gHeight; const gRates = { p:n($('rateGarageP').value), c1:n($('rateGarageC1').value), c2:n($('rateGarageC2').value) }; const useGP = $('garageUseP').value==='1', useGC1 = $('garageUseC1').value==='1', useGC2 = $('garageUseC2').value==='1'; let sumGarage=0; if(useGP) sumGarage += pushRow(rows,'Garage — Primer', gArea, 'sqft', gRates.p); if(useGC1) sumGarage += pushRow(rows,'Garage — Coat 1', gArea, 'sqft', gRates.c1); if(useGC2) sumGarage += pushRow(rows,'Garage — Coat 2', gArea, 'sqft', gRates.c2); $('garageOut').innerText = `LF: ${gLF.toFixed(2)} | Area: ${gArea.toFixed(2)}`; // Exterior (two levels optional) const extA1 = n($('extPerim').value) * n($('extHeight').value); const extA2 = n($('extPerim2').value) * n($('extHeight2').value); const extArea = extA1 + extA2; const extRates = { p:n($('rateExtP').value), c1:n($('rateExtC1').value), c2:n($('rateExtC2').value) }; const useEP = $('extUseP').value==='1', useEC1=$('extUseC1').value==='1', useEC2=$('extUseC2').value==='1'; let sumExt=0; if(useEP) sumExt += pushRow(rows,'Exterior — Primer', extArea, 'sqft', extRates.p); if(useEC1) sumExt += pushRow(rows,'Exterior — Coat 1', extArea, 'sqft', extRates.c1); if(useEC2) sumExt += pushRow(rows,'Exterior — Coat 2', extArea, 'sqft', extRates.c2); $('extOut').innerText = `Exterior Area: ${extArea.toFixed(2)} (P1: ${extA1.toFixed(0)} + P2: ${extA2.toFixed(0)})`; // Totals const subtotal = sumWalls + sumCeil + sumTrim + sumGarage + sumExt; const markup = subtotal * markupPct; const taxedBase = subtotal + markup; const tax = taxedBase * taxPct; const total = taxedBase + tax; $('sumWalls').innerText = fmt(sumWalls); $('sumCeil').innerText = fmt(sumCeil); $('sumTrim').innerText = fmt(sumTrim); $('sumGarage').innerText = fmt(sumGarage); $('sumExt').innerText = fmt(sumExt); $('sumSub').innerText = fmt(subtotal); $('sumMarkup').innerText = fmt(markup); $('sumTax').innerText = fmt(tax); $('sumTotal').innerText = fmt(total); // Detail table const tbody = document.querySelector('#detailTable tbody'); tbody.innerHTML = ''; rows.forEach(r=>{ const tr=document.createElement('tr'); tr.innerHTML = `<td>${r.label}</td><td>${r.qty.toLocaleString(undefined,{maximumFractionDigits:2})}</td><td>${r.unit}</td><td style="white-space:nowrap">${fmt(r.rate)}</td><td style="white-space:nowrap">${fmt(r.cost)}</td>`; tbody.appendChild(tr); }); $('tblSub').innerText = fmt(subtotal); // autosave save(); return {subtotal, markup, tax, total, rows}; } // wiring $('calcBtn').addEventListener('click', calc); $('resetBtn').addEventListener('click', ()=>{ localStorage.removeItem(storeKey); location.reload(); }); $('exportBtn').addEventListener('click', ()=>{ const out = calc(); const data = { project: $('projectName').value, currency: $('currency').value, decimals: n($('decimals').value), totals: { subtotal: out.subtotal, markup:n($('markupPct').value)/100, tax:n($('taxPct').value)/100, total: out.total }, rows: out.rows }; const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='estimate.json'; a.click(); URL.revokeObjectURL(url); }); // Show custom height box when needed $('wallsHeight').addEventListener('change', (e)=>{ const v=e.target.value; $('wallsHeightCustom').style.display = (v==='custom')? 'block':'none'; }); // Auto-calc on input changes ids.forEach(id=>{ const el=$(id); if(!el) return; el.addEventListener('input', ()=>{ if(id==='wallsHeight') return; calc(); }); el.addEventListener('change', calc); }); // init load(); calc(); })(); </script> </body> </html>