Real Estate — Mortgage Calculator
An editorial real-estate mortgage calculator that estimates the true monthly cost of ownership for a fictional luxury listing. Adjust home price, down payment by amount or percent, loan term, interest rate, property tax, insurance and HOA dues, and watch an amortized payment recompute live. A segmented donut chart and tabular legend break the payment into principal and interest, taxes, insurance and association fees, alongside the loan amount, total interest over the life of the loan and the grand total of all payments.
MCP
Kod
:root {
--ivory: #f7f4ec;
--paper: #fffdf8;
--white: #ffffff;
--green: #1f3d34;
--green-d: #16302a;
--green-700: #26493e;
--green-50: #e8efea;
--brass: #b08d57;
--brass-d: #94733f;
--brass-50: #f3ead9;
--ink: #1c2a25;
--ink-2: #33433d;
--muted: #6b7a72;
--line: rgba(31, 61, 52, 0.12);
--line-2: rgba(31, 61, 52, 0.22);
--ok: #2f9e6f;
--warn: #c98a2b;
--danger: #c4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-sm: 0 1px 2px rgba(22, 48, 42, 0.06), 0 1px 1px rgba(22, 48, 42, 0.04);
--sh-md: 0 10px 24px -12px rgba(22, 48, 42, 0.22), 0 2px 6px rgba(22, 48, 42, 0.06);
--sh-lg: 0 28px 60px -28px rgba(22, 48, 42, 0.34), 0 6px 16px rgba(22, 48, 42, 0.08);
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, sans-serif;
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: var(--sans);
line-height: 1.55;
color: var(--ink);
background:
radial-gradient(120% 80% at 100% 0%, rgba(176, 141, 87, 0.08), transparent 60%),
radial-gradient(120% 80% at 0% 100%, rgba(31, 61, 52, 0.06), transparent 55%),
var(--ivory);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.shell {
max-width: 1140px;
margin: 0 auto;
padding: 28px 24px 64px;
}
/* ===== Masthead ===== */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding-bottom: 20px;
border-bottom: 1px solid var(--line);
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-mark {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: var(--r-sm);
background: linear-gradient(150deg, var(--green-700), var(--green-d));
color: var(--brass-50);
font-family: var(--serif);
font-weight: 700;
font-size: 1.1rem;
letter-spacing: 0.02em;
border: 1px solid rgba(176, 141, 87, 0.4);
box-shadow: var(--sh-sm);
}
.brand-kicker {
margin: 0;
font-family: var(--serif);
font-size: 1.25rem;
font-weight: 600;
color: var(--green-d);
letter-spacing: 0.01em;
}
.brand-sub {
margin: 0;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--muted);
}
.masthead-tag {
margin: 0;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--brass-d);
padding: 6px 12px;
border: 1px solid var(--line-2);
border-radius: 999px;
background: var(--brass-50);
}
/* ===== Hero ===== */
.hero {
padding: 38px 0 30px;
max-width: 640px;
}
.hero-eyebrow {
margin: 0 0 8px;
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--brass-d);
}
.hero-title {
margin: 0 0 14px;
font-family: var(--serif);
font-weight: 700;
font-size: clamp(2.4rem, 6vw, 3.6rem);
line-height: 1.04;
color: var(--green-d);
}
.hero-lede {
margin: 0;
font-size: 1.02rem;
color: var(--ink-2);
}
/* ===== Layout grid ===== */
.layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 22px;
align-items: start;
}
.listing {
grid-column: 1 / 2;
}
.inputs {
grid-column: 2 / 3;
grid-row: 1 / 3;
}
.results {
grid-column: 1 / 2;
}
/* ===== Card base ===== */
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-md);
}
.card-title {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: 1.5rem;
color: var(--green-d);
}
/* ===== Listing / photo ===== */
.listing {
overflow: hidden;
}
.photo {
position: relative;
aspect-ratio: 16 / 10;
background:
radial-gradient(140% 120% at 78% 18%, rgba(255, 244, 222, 0.85), transparent 45%),
linear-gradient(165deg, #3a5b4f 0%, #2a463c 38%, #1f3d34 70%, #16302a 100%);
}
.photo::before {
/* sky / glazing reflection */
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(120deg, rgba(176, 141, 87, 0.28) 0%, transparent 34%),
repeating-linear-gradient(
105deg,
rgba(255, 255, 255, 0.05) 0 2px,
transparent 2px 16px
);
}
.photo::after {
/* architectural massing silhouette */
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 58%;
background:
linear-gradient(180deg, transparent, rgba(15, 33, 28, 0.55)),
linear-gradient(90deg, #243f36 0 38%, #1c352d 38% 64%, #2a473c 64% 100%);
clip-path: polygon(0 36%, 22% 30%, 22% 10%, 46% 10%, 46% 30%, 70% 22%, 100% 40%, 100% 100%, 0 100%);
}
.photo-badge {
position: absolute;
top: 14px;
z-index: 2;
font-size: 0.74rem;
font-weight: 600;
letter-spacing: 0.04em;
padding: 6px 11px;
border-radius: 999px;
box-shadow: var(--sh-sm);
}
.photo-badge--status {
left: 14px;
background: var(--brass);
color: #2a1e0c;
border: 1px solid var(--brass-d);
}
.photo-badge--price {
right: 14px;
background: rgba(255, 253, 248, 0.94);
color: var(--green-d);
border: 1px solid var(--line-2);
}
.photo-caption {
position: absolute;
left: 16px;
bottom: 14px;
z-index: 2;
color: var(--paper);
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
}
.photo-caption-line {
display: block;
font-family: var(--serif);
font-weight: 600;
font-size: 1.35rem;
line-height: 1.1;
}
.photo-caption-sub {
display: block;
font-size: 0.78rem;
letter-spacing: 0.06em;
opacity: 0.9;
}
.listing-body {
padding: 18px 20px 20px;
}
.spec-row {
display: flex;
flex-wrap: wrap;
gap: 18px;
margin: 0 0 14px;
padding: 0 0 14px;
list-style: none;
border-bottom: 1px solid var(--line);
}
.spec-row li {
font-size: 0.82rem;
color: var(--muted);
}
.spec-row strong {
display: block;
font-family: var(--serif);
font-size: 1.3rem;
color: var(--green-d);
font-weight: 600;
}
.listing-blurb {
margin: 0 0 16px;
font-size: 0.92rem;
color: var(--ink-2);
}
.agent {
display: flex;
align-items: center;
gap: 12px;
}
.agent-avatar {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--green-50);
color: var(--green-d);
font-weight: 600;
font-size: 0.82rem;
border: 1px solid var(--line-2);
}
.agent-name {
display: block;
font-weight: 600;
font-size: 0.88rem;
color: var(--ink);
}
.agent-role {
display: block;
font-size: 0.74rem;
color: var(--muted);
}
/* ===== Inputs ===== */
.inputs {
padding: 24px 22px 22px;
}
.inputs .card-title {
margin-bottom: 18px;
padding-bottom: 14px;
border-bottom: 1px solid var(--line);
}
.field {
margin-bottom: 16px;
}
.field-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
}
.field label {
display: block;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 7px;
}
.field-aside {
font-size: 0.78rem;
font-weight: 600;
color: var(--brass-d);
}
.control {
position: relative;
display: flex;
align-items: center;
background: var(--white);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
box-shadow: var(--sh-sm);
transition: border-color 0.16s ease, box-shadow 0.16s ease;
}
.control:focus-within {
border-color: var(--green-700);
box-shadow: 0 0 0 3px rgba(38, 73, 62, 0.14);
}
.control input,
.control select {
width: 100%;
border: 0;
background: transparent;
font-family: var(--sans);
font-size: 1rem;
font-weight: 600;
color: var(--ink);
padding: 11px 12px;
outline: none;
appearance: none;
}
.control select {
cursor: pointer;
background-image:
linear-gradient(45deg, transparent 50%, var(--green-700) 50%),
linear-gradient(135deg, var(--green-700) 50%, transparent 50%);
background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 34px;
}
.control .prefix,
.control .suffix {
font-weight: 600;
color: var(--muted);
font-size: 0.95rem;
}
.control .prefix {
padding-left: 12px;
}
.control.money input {
padding-left: 6px;
}
.control .suffix {
padding-right: 12px;
}
.control.percent input {
padding-right: 4px;
}
/* hide number spinners */
.control input[type="number"]::-webkit-outer-spin-button,
.control input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.control input[type="number"] {
-moz-appearance: textfield;
}
.dual {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 10px;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
/* range slider */
.range {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
margin: 12px 0 2px;
border-radius: 999px;
background: var(--green-50);
outline: none;
cursor: pointer;
}
.range::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: linear-gradient(150deg, var(--brass), var(--brass-d));
border: 2px solid var(--paper);
box-shadow: var(--sh-sm);
cursor: pointer;
}
.range::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: linear-gradient(150deg, var(--brass), var(--brass-d));
border: 2px solid var(--paper);
box-shadow: var(--sh-sm);
cursor: pointer;
}
.range:focus-visible {
box-shadow: 0 0 0 3px rgba(176, 141, 87, 0.3);
}
.reset-btn {
margin-top: 6px;
width: 100%;
padding: 11px 14px;
font-family: var(--sans);
font-size: 0.84rem;
font-weight: 600;
letter-spacing: 0.04em;
color: var(--green-d);
background: var(--green-50);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
cursor: pointer;
transition: background 0.16s ease, transform 0.08s ease;
}
.reset-btn:hover {
background: #dde8e0;
}
.reset-btn:active {
transform: translateY(1px);
}
.reset-btn:focus-visible {
outline: 2px solid var(--green-700);
outline-offset: 2px;
}
/* ===== Results ===== */
.results {
padding: 24px 22px 22px;
background:
linear-gradient(180deg, rgba(38, 73, 62, 0.04), transparent 30%),
var(--paper);
}
.results-head {
text-align: center;
padding-bottom: 8px;
}
.results-label {
margin: 0;
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--muted);
}
.results-total {
margin: 4px 0 0;
font-family: var(--serif);
font-weight: 700;
font-size: clamp(2.6rem, 7vw, 3.4rem);
line-height: 1;
color: var(--green-d);
font-variant-numeric: tabular-nums;
}
.donut-wrap {
position: relative;
width: 200px;
margin: 14px auto 18px;
}
.donut {
width: 200px;
height: 200px;
transform: rotate(-90deg);
}
.donut-track {
fill: none;
stroke: var(--green-50);
stroke-width: 13;
}
.donut-seg {
fill: none;
stroke-width: 13;
stroke-linecap: butt;
transition: stroke-dasharray 0.55s cubic-bezier(0.22, 1, 0.36, 1),
stroke-dashoffset 0.55s cubic-bezier(0.22, 1, 0.36, 1);
}
.seg-pi { stroke: var(--green); }
.seg-tax { stroke: var(--brass); }
.seg-ins { stroke: var(--green-700); }
.seg-hoa { stroke: var(--warn); }
.donut-center {
position: absolute;
inset: 0;
display: grid;
place-content: center;
text-align: center;
pointer-events: none;
}
.donut-center-num {
font-family: var(--serif);
font-weight: 700;
font-size: 1.6rem;
color: var(--green-d);
font-variant-numeric: tabular-nums;
}
.donut-center-cap {
display: block;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--muted);
}
.legend {
list-style: none;
margin: 0 0 18px;
padding: 0;
display: grid;
gap: 2px;
}
.legend li {
display: grid;
grid-template-columns: 14px 1fr auto;
align-items: center;
gap: 10px;
padding: 9px 4px;
border-bottom: 1px solid var(--line);
}
.legend li:last-child {
border-bottom: 0;
}
.swatch {
width: 12px;
height: 12px;
border-radius: 3px;
}
.sw-pi { background: var(--green); }
.sw-tax { background: var(--brass); }
.sw-ins { background: var(--green-700); }
.sw-hoa { background: var(--warn); }
.legend-label {
font-size: 0.88rem;
color: var(--ink-2);
}
.legend-val {
font-weight: 600;
font-size: 0.92rem;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.summary {
margin: 0;
padding: 16px;
display: grid;
gap: 10px;
background: var(--green-50);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.summary > div {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.summary dt {
margin: 0;
font-size: 0.84rem;
color: var(--green-700);
}
.summary dd {
margin: 0;
font-weight: 600;
font-size: 0.96rem;
color: var(--green-d);
font-variant-numeric: tabular-nums;
}
.results-note {
margin: 14px 0 0;
font-size: 0.74rem;
color: var(--muted);
text-align: center;
}
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 16px);
background: var(--green-d);
color: var(--brass-50);
font-size: 0.86rem;
font-weight: 500;
padding: 11px 18px;
border-radius: 999px;
border: 1px solid rgba(176, 141, 87, 0.4);
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ===== Responsive ===== */
@media (max-width: 880px) {
.layout {
grid-template-columns: 1fr;
}
.listing,
.inputs,
.results {
grid-column: 1 / -1;
grid-row: auto;
}
}
@media (max-width: 520px) {
.shell {
padding: 20px 16px 48px;
}
.masthead {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.hero {
padding: 26px 0 22px;
}
.grid-2 {
grid-template-columns: 1fr;
}
.dual {
grid-template-columns: 1fr 1fr;
}
.inputs,
.results,
.listing-body {
padding-left: 16px;
padding-right: 16px;
}
.spec-row {
gap: 14px;
}
}/* Mortgage Calculator — Aldercrest & Vine
Vanilla JS. Recomputes an amortized monthly payment on every input change. */
(function () {
"use strict";
var $ = function (id) { return document.getElementById(id); };
var els = {
price: $("price"),
downAmt: $("downAmt"),
downPct: $("downPct"),
downRange: $("downRange"),
downHint: $("downHint"),
rate: $("rate"),
term: $("term"),
tax: $("tax"),
ins: $("ins"),
hoa: $("hoa"),
photoPrice: $("photoPrice"),
monthlyTotal: $("monthlyTotal"),
donutCenter: $("donutCenter"),
rowPI: $("rowPI"),
rowTax: $("rowTax"),
rowIns: $("rowIns"),
rowHOA: $("rowHOA"),
loanAmt: $("loanAmt"),
totalInterest: $("totalInterest"),
totalPaid: $("totalPaid"),
reset: $("reset")
};
var DEFAULTS = {
price: 845000,
downPct: 20,
rate: 6.45,
term: 30,
tax: 9200,
ins: 1740,
hoa: 145
};
var R = 52; // donut radius (matches SVG)
var CIRC = 2 * Math.PI * R;
var segEls = Array.prototype.slice.call(document.querySelectorAll(".donut-seg"));
/* ---------- formatting ---------- */
var fmt0 = new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD", maximumFractionDigits: 0
});
function money(n) {
if (!isFinite(n) || n < 0) n = 0;
return fmt0.format(Math.round(n));
}
function num(el) {
var v = parseFloat(el.value);
return isFinite(v) ? v : 0;
}
/* ---------- toast helper ---------- */
var toastEl = $("toast");
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
/* ---------- core math ---------- */
function clamp(n, lo, hi) {
return Math.min(hi, Math.max(lo, n));
}
// Standard amortized monthly principal + interest.
function monthlyPI(principal, annualRate, years) {
if (principal <= 0) return 0;
var n = years * 12;
var r = annualRate / 100 / 12;
if (r === 0) return principal / n;
var f = Math.pow(1 + r, n);
return (principal * r * f) / (f - 1);
}
function compute() {
var price = clamp(num(els.price), 0, 1e9);
var downPct = clamp(num(els.downPct), 0, 100);
var down = price * (downPct / 100);
var rate = clamp(num(els.rate), 0, 25);
var years = parseInt(els.term.value, 10) || 30;
var taxYr = Math.max(0, num(els.tax));
var insYr = Math.max(0, num(els.ins));
var hoaMo = Math.max(0, num(els.hoa));
var loan = Math.max(0, price - down);
var pi = monthlyPI(loan, rate, years);
var taxMo = taxYr / 12;
var insMo = insYr / 12;
var total = pi + taxMo + insMo + hoaMo;
var n = years * 12;
var totalInterest = pi * n - loan;
var totalPaid = total * n + down;
return {
price: price, down: down, loan: loan,
pi: pi, taxMo: taxMo, insMo: insMo, hoaMo: hoaMo,
total: total,
totalInterest: Math.max(0, totalInterest),
totalPaid: totalPaid
};
}
/* ---------- donut rendering ---------- */
function paintDonut(d) {
var parts = [
{ el: segEls[0], v: d.pi },
{ el: segEls[1], v: d.taxMo },
{ el: segEls[2], v: d.insMo },
{ el: segEls[3], v: d.hoaMo }
];
var sum = d.total || 1;
var offset = 0;
parts.forEach(function (p) {
var frac = p.v / sum;
var len = frac * CIRC;
// tiny gap between visible segments for definition
var gap = len > 1.5 ? 1.2 : 0;
p.el.setAttribute("stroke-dasharray", Math.max(0, len - gap) + " " + (CIRC - Math.max(0, len - gap)));
p.el.setAttribute("stroke-dashoffset", -offset);
offset += len;
});
}
/* ---------- render ---------- */
function render() {
var d = compute();
els.photoPrice.textContent = money(d.price);
els.monthlyTotal.textContent = money(d.total);
els.donutCenter.textContent = money(d.total);
els.rowPI.textContent = money(d.pi);
els.rowTax.textContent = money(d.taxMo);
els.rowIns.textContent = money(d.insMo);
els.rowHOA.textContent = money(d.hoaMo);
els.loanAmt.textContent = money(d.loan);
els.totalInterest.textContent = money(d.totalInterest);
els.totalPaid.textContent = money(d.totalPaid);
paintDonut(d);
}
/* ---------- down-payment sync (amount <-> percent <-> slider) ---------- */
function setDownPct(pct, source) {
pct = clamp(pct, 0, 100);
var price = Math.max(0, num(els.price));
var rounded = Math.round(pct * 10) / 10;
if (source !== "pct") els.downPct.value = rounded;
if (source !== "range") els.downRange.value = Math.round(pct);
if (source !== "amt") els.downAmt.value = Math.round(price * (pct / 100));
els.downHint.textContent = rounded + "% down";
}
function syncFromAmount() {
var price = Math.max(0, num(els.price));
var amt = clamp(num(els.downAmt), 0, price);
var pct = price > 0 ? (amt / price) * 100 : 0;
setDownPct(pct, "amt");
render();
}
function syncFromPct() {
setDownPct(num(els.downPct), "pct");
render();
}
function syncFromRange() {
setDownPct(num(els.downRange), "range");
render();
}
function syncFromPrice() {
// keep the percentage fixed, recompute the dollar amount
setDownPct(num(els.downPct), "price");
render();
}
/* ---------- wiring ---------- */
els.price.addEventListener("input", syncFromPrice);
els.downAmt.addEventListener("input", syncFromAmount);
els.downPct.addEventListener("input", syncFromPct);
els.downRange.addEventListener("input", syncFromRange);
[els.rate, els.term, els.tax, els.ins, els.hoa].forEach(function (el) {
el.addEventListener("input", render);
el.addEventListener("change", render);
});
els.reset.addEventListener("click", function () {
els.price.value = DEFAULTS.price;
els.rate.value = DEFAULTS.rate;
els.term.value = String(DEFAULTS.term);
els.tax.value = DEFAULTS.tax;
els.ins.value = DEFAULTS.ins;
els.hoa.value = DEFAULTS.hoa;
setDownPct(DEFAULTS.downPct, null);
render();
toast("Restored listing defaults");
});
/* ---------- init ---------- */
setDownPct(DEFAULTS.downPct, null); // populate amount, percent and slider together
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mortgage Calculator — Aldercrest & Vine</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<header class="masthead">
<div class="brand">
<span class="brand-mark" aria-hidden="true">A&V</span>
<div class="brand-text">
<p class="brand-kicker">Aldercrest & Vine</p>
<p class="brand-sub">Private Residential Brokerage</p>
</div>
</div>
<p class="masthead-tag">Financing Studio</p>
</header>
<section class="hero">
<p class="hero-eyebrow">Estimate your monthly cost of ownership</p>
<h1 class="hero-title">Mortgage Calculator</h1>
<p class="hero-lede">
Model the true monthly carry on a home — principal, interest, taxes,
insurance and association dues — and watch the breakdown update as you
adjust every figure.
</p>
</section>
<div class="layout">
<!-- ===== Featured listing card ===== -->
<section class="listing card" aria-label="Featured listing">
<div class="photo" role="img" aria-label="Architectural exterior of the featured residence">
<span class="photo-badge photo-badge--status">For Sale</span>
<span class="photo-badge photo-badge--price" id="photoPrice">$845,000</span>
<div class="photo-caption">
<span class="photo-caption-line">14 Aldercrest Terrace</span>
<span class="photo-caption-sub">Briarwood Heights, OR</span>
</div>
</div>
<div class="listing-body">
<ul class="spec-row" aria-label="Property specifications">
<li><strong>4</strong> Beds</li>
<li><strong>3.5</strong> Baths</li>
<li><strong>3,180</strong> Sq Ft</li>
<li><strong>0.41</strong> Acre</li>
</ul>
<p class="listing-blurb">
A cedar-and-glass contemporary on a quiet cul-de-sac, with vaulted
ceilings, a chef's kitchen and a south-facing garden terrace.
</p>
<div class="agent">
<span class="agent-avatar" aria-hidden="true">RM</span>
<div class="agent-meta">
<span class="agent-name">Listed by Rosa Marchetti</span>
<span class="agent-role">Senior Associate · Lic. #FIC-20418</span>
</div>
</div>
</div>
</section>
<!-- ===== Calculator inputs ===== -->
<section class="inputs card" aria-label="Mortgage inputs">
<h2 class="card-title">Loan Details</h2>
<div class="field">
<label for="price">Home price</label>
<div class="control money">
<span class="prefix">$</span>
<input id="price" type="number" inputmode="numeric" min="0" step="1000" value="845000" />
</div>
</div>
<div class="field">
<div class="field-head">
<label for="downAmt">Down payment</label>
<span class="field-aside" id="downHint">20% down</span>
</div>
<div class="dual">
<div class="control money">
<span class="prefix">$</span>
<input id="downAmt" type="number" inputmode="numeric" min="0" step="1000" value="169000" aria-label="Down payment amount" />
</div>
<div class="control percent">
<input id="downPct" type="number" inputmode="decimal" min="0" max="100" step="0.5" value="20" aria-label="Down payment percent" />
<span class="suffix">%</span>
</div>
</div>
<input id="downRange" class="range" type="range" min="0" max="100" step="1" value="20" aria-label="Down payment percent slider" />
</div>
<div class="grid-2">
<div class="field">
<label for="rate">Interest rate</label>
<div class="control percent">
<input id="rate" type="number" inputmode="decimal" min="0" max="25" step="0.05" value="6.45" />
<span class="suffix">%</span>
</div>
</div>
<div class="field">
<label for="term">Loan term</label>
<div class="control">
<select id="term">
<option value="30" selected>30 years</option>
<option value="20">20 years</option>
<option value="15">15 years</option>
<option value="10">10 years</option>
</select>
</div>
</div>
</div>
<div class="grid-2">
<div class="field">
<label for="tax">Property tax / yr</label>
<div class="control money">
<span class="prefix">$</span>
<input id="tax" type="number" inputmode="numeric" min="0" step="100" value="9200" />
</div>
</div>
<div class="field">
<label for="ins">Insurance / yr</label>
<div class="control money">
<span class="prefix">$</span>
<input id="ins" type="number" inputmode="numeric" min="0" step="50" value="1740" />
</div>
</div>
</div>
<div class="field">
<label for="hoa">HOA / month</label>
<div class="control money">
<span class="prefix">$</span>
<input id="hoa" type="number" inputmode="numeric" min="0" step="10" value="145" />
</div>
</div>
<button id="reset" type="button" class="reset-btn">Reset to listing defaults</button>
</section>
<!-- ===== Results panel ===== -->
<section class="results card" aria-label="Payment breakdown" aria-live="polite">
<div class="results-head">
<p class="results-label">Estimated monthly payment</p>
<p class="results-total" id="monthlyTotal">$0</p>
</div>
<div class="donut-wrap">
<svg class="donut" viewBox="0 0 120 120" role="img" aria-label="Payment composition chart">
<circle class="donut-track" cx="60" cy="60" r="52" />
<circle class="donut-seg seg-pi" cx="60" cy="60" r="52" data-key="pi" />
<circle class="donut-seg seg-tax" cx="60" cy="60" r="52" data-key="tax" />
<circle class="donut-seg seg-ins" cx="60" cy="60" r="52" data-key="ins" />
<circle class="donut-seg seg-hoa" cx="60" cy="60" r="52" data-key="hoa" />
</svg>
<div class="donut-center">
<span class="donut-center-num" id="donutCenter">$0</span>
<span class="donut-center-cap">per month</span>
</div>
</div>
<ul class="legend">
<li>
<span class="swatch sw-pi" aria-hidden="true"></span>
<span class="legend-label">Principal & interest</span>
<span class="legend-val" id="rowPI">$0</span>
</li>
<li>
<span class="swatch sw-tax" aria-hidden="true"></span>
<span class="legend-label">Property tax</span>
<span class="legend-val" id="rowTax">$0</span>
</li>
<li>
<span class="swatch sw-ins" aria-hidden="true"></span>
<span class="legend-label">Insurance</span>
<span class="legend-val" id="rowIns">$0</span>
</li>
<li>
<span class="swatch sw-hoa" aria-hidden="true"></span>
<span class="legend-label">HOA dues</span>
<span class="legend-val" id="rowHOA">$0</span>
</li>
</ul>
<dl class="summary">
<div>
<dt>Loan amount</dt>
<dd id="loanAmt">$0</dd>
</div>
<div>
<dt>Total interest paid</dt>
<dd id="totalInterest">$0</dd>
</div>
<div>
<dt>Total of all payments</dt>
<dd id="totalPaid">$0</dd>
</div>
</dl>
<p class="results-note">Estimate excludes closing costs, PMI and HOA special assessments.</p>
</section>
</div>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Mortgage Calculator
A polished, editorial financing panel for a fictional residential brokerage. A featured listing card — complete with a CSS-painted architectural “photo”, price and status badges, spec row and listing agent — sits beside a tidy form of loan inputs: home price, down payment, interest rate, term, property tax, insurance and monthly HOA dues.
The down-payment control stays in sync three ways: typing a dollar amount, a percent, or dragging the slider all update each other, and changing the home price keeps the percentage fixed while recomputing the dollar figure. Every keystroke runs a standard amortization formula to derive the monthly principal and interest, then layers in prorated taxes, insurance and HOA to produce the full monthly carry.
Results are summarized in an animated segmented donut and a tabular legend that splits the payment into its four parts, with a footer reporting the loan amount, total interest paid over the life of the loan and the grand total of all payments. A reset button restores the listing’s default figures and raises a small toast. The layout is keyboard-friendly and collapses gracefully down to narrow phone widths.
Illustrative UI only — sample listings and data are fictional; not a real real-estate service.