Real Estate — CMA / Comp Report
A print-style Comparative Market Analysis laid out like an editorial brokerage report. A subject-property masthead pairs simulated listing photography with beds, baths, square footage and the preparing broker, then a table of fictional comparable sales lists sold price, dollars per square foot, beds and baths, sold date, distance and a signed dollar adjustment. Toggling any comp in or out instantly recomputes the adjusted average, the suggested low, target and high list range, the range slider and a price-distribution bar chart.
MCP
الكود
: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;
--shadow-sm: 0 1px 2px rgba(22, 48, 42, 0.06), 0 2px 8px rgba(22, 48, 42, 0.05);
--shadow-md: 0 4px 14px rgba(22, 48, 42, 0.08), 0 18px 40px rgba(22, 48, 42, 0.08);
--shadow-lg: 0 10px 30px rgba(22, 48, 42, 0.10), 0 30px 70px rgba(22, 48, 42, 0.12);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
font-size: 15px;
line-height: 1.55;
color: var(--ink);
background:
radial-gradient(1200px 600px at 85% -10%, rgba(176, 141, 87, 0.10), transparent 60%),
radial-gradient(900px 500px at -10% 110%, rgba(31, 61, 52, 0.08), transparent 60%),
var(--ivory);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: clamp(16px, 4vw, 48px);
}
h1, h2, h3 {
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 600;
letter-spacing: 0.2px;
margin: 0;
color: var(--green-d);
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
/* ===== Sheet ===== */
.sheet {
max-width: 980px;
margin: 0 auto;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
padding: clamp(22px, 4.5vw, 54px);
position: relative;
overflow: hidden;
}
.sheet::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 5px;
background: linear-gradient(90deg, var(--green), var(--brass) 55%, var(--brass-d));
}
/* ===== Masthead ===== */
.masthead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark {
width: 46px; height: 46px;
border-radius: 12px;
background: linear-gradient(150deg, var(--green-700), var(--green-d));
color: var(--brass-50);
display: grid; place-items: center;
font-family: "Cormorant Garamond", serif;
font-weight: 700;
font-size: 20px;
letter-spacing: 1px;
box-shadow: inset 0 0 0 1px rgba(176, 141, 87, 0.4), var(--shadow-sm);
}
.brand-name {
margin: 0;
font-family: "Cormorant Garamond", serif;
font-size: 22px;
font-weight: 700;
color: var(--green-d);
line-height: 1.1;
}
.brand-sub {
margin: 2px 0 0;
font-size: 11.5px;
letter-spacing: 1.4px;
text-transform: uppercase;
color: var(--muted);
}
.masthead-meta { text-align: right; display: flex; flex-direction: column; gap: 4px; }
.doc-kind {
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--brass-d);
font-weight: 600;
}
.doc-ref { font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.rule-brass {
height: 1px;
margin: 20px 0 26px;
background: linear-gradient(90deg, transparent, var(--brass) 8%, var(--brass) 92%, transparent);
opacity: 0.6;
}
/* ===== Subject ===== */
.subject {
display: grid;
grid-template-columns: 300px 1fr;
gap: 28px;
align-items: stretch;
}
.subject-photo {
position: relative;
border-radius: var(--r-md);
min-height: 230px;
overflow: hidden;
box-shadow: var(--shadow-md), inset 0 0 0 1px rgba(255, 255, 255, 0.25);
background:
linear-gradient(180deg, rgba(28, 42, 37, 0) 40%, rgba(22, 48, 42, 0.55) 100%),
radial-gradient(120% 80% at 75% 18%, rgba(255, 226, 170, 0.55), transparent 55%),
linear-gradient(115deg, #c98a4e 0%, #a86a3c 30%, #5d4a3a 60%, #2c3a33 100%);
}
.subject-photo::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0;
height: 46%;
background:
linear-gradient(0deg, rgba(20, 30, 26, 0.7), transparent),
repeating-linear-gradient(90deg, rgba(0,0,0,0.18) 0 2px, transparent 2px 22px);
-webkit-mask: linear-gradient(180deg, transparent, #000 60%);
mask: linear-gradient(180deg, transparent, #000 60%);
opacity: 0.5;
}
.photo-tag {
position: absolute;
top: 12px; left: 12px;
background: rgba(22, 48, 42, 0.82);
color: var(--brass-50);
font-size: 10.5px;
letter-spacing: 1.6px;
text-transform: uppercase;
font-weight: 600;
padding: 5px 11px;
border-radius: 999px;
backdrop-filter: blur(2px);
z-index: 2;
}
.photo-credit {
position: absolute;
bottom: 10px; left: 14px;
color: rgba(255, 253, 248, 0.82);
font-size: 11px;
letter-spacing: 0.4px;
z-index: 2;
}
.eyebrow {
margin: 0 0 4px;
font-size: 11px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--brass-d);
font-weight: 600;
}
.subject-address {
font-size: clamp(30px, 5vw, 42px);
line-height: 1.02;
font-weight: 700;
}
.subject-loc { margin: 6px 0 0; color: var(--muted); font-size: 14px; }
.spec-row {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0;
margin: 20px 0 0;
padding: 14px 0;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
}
.spec-row li {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0 22px;
border-right: 1px solid var(--line);
}
.spec-row li:first-child { padding-left: 0; }
.spec-row li:last-child { border-right: 0; }
.spec-n {
font-family: "Cormorant Garamond", serif;
font-size: 24px;
font-weight: 700;
color: var(--green-d);
line-height: 1;
}
.spec-l {
font-size: 10.5px;
letter-spacing: 1.4px;
text-transform: uppercase;
color: var(--muted);
margin-top: 4px;
}
.subject-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-top: 18px;
flex-wrap: wrap;
}
.agent-chip { display: flex; align-items: center; gap: 11px; }
.agent-avatar {
width: 40px; height: 40px;
border-radius: 50%;
background: linear-gradient(150deg, var(--brass), var(--brass-d));
color: var(--white);
display: grid; place-items: center;
font-weight: 600;
font-size: 13px;
letter-spacing: 0.5px;
box-shadow: var(--shadow-sm);
}
.agent-name { margin: 0; font-weight: 600; font-size: 14px; color: var(--ink); }
.agent-role { margin: 1px 0 0; font-size: 12px; color: var(--muted); }
.prep-date { margin: 0; font-size: 12.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
/* ===== Section shared ===== */
.section-title { font-size: clamp(22px, 3.4vw, 28px); }
.section-note { margin: 4px 0 0; font-size: 13px; color: var(--muted); max-width: 60ch; }
.summary, .comps, .dist { margin-top: 34px; }
.summary-head, .comps-head { margin-bottom: 16px; }
/* ===== Suggested range ===== */
.range-card {
background: linear-gradient(160deg, var(--white), var(--green-50));
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: clamp(18px, 3vw, 28px);
box-shadow: var(--shadow-md);
position: relative;
}
.range-numbers {
display: grid;
grid-template-columns: 1fr 1.3fr 1fr;
gap: 12px;
align-items: end;
}
.range-cell { display: flex; flex-direction: column; gap: 4px; }
.range-cell--mid {
align-items: center;
text-align: center;
padding: 0 16px;
border-left: 1px solid var(--line);
border-right: 1px solid var(--line);
}
.range-cell--end { align-items: flex-end; text-align: right; }
.range-label {
font-size: 10.5px;
letter-spacing: 1.6px;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.range-value {
font-family: "Cormorant Garamond", serif;
font-size: clamp(26px, 4.2vw, 36px);
font-weight: 700;
color: var(--green-d);
line-height: 1;
font-variant-numeric: tabular-nums;
}
.range-value--mid {
font-size: clamp(34px, 6vw, 52px);
color: var(--brass-d);
}
.range-ppsf { font-size: 12px; color: var(--muted); margin-top: 2px; }
.range-bar { margin-top: 24px; }
.range-bar-track {
position: relative;
height: 14px;
border-radius: 999px;
background: var(--green-50);
box-shadow: inset 0 0 0 1px var(--line);
}
.range-bar-fill {
position: absolute;
top: 0; bottom: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--green-700), var(--brass));
box-shadow: 0 1px 4px rgba(148, 115, 63, 0.4);
transition: left 0.5s cubic-bezier(.22,.61,.36,1), width 0.5s cubic-bezier(.22,.61,.36,1);
}
.range-bar-marker {
position: absolute;
top: 50%;
width: 2px;
height: 26px;
background: var(--green-d);
transform: translate(-50%, -50%);
transition: left 0.5s cubic-bezier(.22,.61,.36,1);
}
.range-bar-marker span {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
font-size: 10px;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--green-d);
font-weight: 600;
white-space: nowrap;
}
.range-bar-scale {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 11.5px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ===== Comps table ===== */
.comps-table-wrap {
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: auto;
box-shadow: var(--shadow-sm);
background: var(--white);
}
.comps-table {
width: 100%;
border-collapse: collapse;
font-size: 13.5px;
min-width: 720px;
}
.comps-table thead th {
text-align: left;
font-size: 10.5px;
letter-spacing: 1.2px;
text-transform: uppercase;
font-weight: 600;
color: var(--green-d);
background: var(--brass-50);
padding: 12px 14px;
border-bottom: 1px solid var(--line-2);
white-space: nowrap;
}
.comps-table th.num, .comps-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.comps-table tbody td {
padding: 13px 14px;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.comps-table tbody tr { transition: background 0.18s ease; }
.comps-table tbody tr:hover { background: var(--green-50); }
.comps-table tbody tr.is-excluded {
color: var(--muted);
background: repeating-linear-gradient(135deg, rgba(31,61,52,0.025) 0 8px, transparent 8px 16px);
}
.comps-table tbody tr.is-excluded .comp-addr,
.comps-table tbody tr.is-excluded .num { opacity: 0.55; text-decoration: line-through; text-decoration-color: var(--line-2); }
.col-toggle { width: 44px; }
.col-addr { min-width: 190px; }
.comp-addr { font-weight: 600; color: var(--ink); }
.comp-area { display: block; font-size: 11.5px; color: var(--muted); font-weight: 400; margin-top: 1px; }
.cb {
appearance: none;
-webkit-appearance: none;
width: 20px; height: 20px;
border: 1.5px solid var(--line-2);
border-radius: 6px;
background: var(--white);
cursor: pointer;
display: grid;
place-items: center;
transition: background 0.15s ease, border-color 0.15s ease;
}
.cb:hover { border-color: var(--brass); }
.cb:checked {
background: var(--green-700);
border-color: var(--green-700);
}
.cb:checked::after {
content: "";
width: 5px; height: 9px;
border: solid var(--brass-50);
border-width: 0 2px 2px 0;
transform: rotate(40deg) translateY(-1px);
}
.cb:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
.adj-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.adj-pill--up { background: rgba(47,158,111,0.12); color: var(--ok); }
.adj-pill--down { background: rgba(196,80,62,0.12); color: var(--danger); }
.adj-pill--zero { background: var(--green-50); color: var(--muted); }
.comps-foot td {
padding: 13px 14px;
border-top: 2px solid var(--line-2);
font-weight: 700;
color: var(--green-d);
background: var(--brass-50);
}
.comps-foot .col-addr {
font-family: "Cormorant Garamond", serif;
font-size: 16px;
}
.foot-count { font-family: "Inter", sans-serif; font-size: 11px; color: var(--muted); font-weight: 500; }
.adj-legend {
margin: 12px 2px 0;
font-size: 11.5px;
color: var(--muted);
display: flex;
flex-wrap: wrap;
gap: 6px 18px;
align-items: center;
}
.legend-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.legend-dot--up { background: var(--ok); }
.legend-dot--down { background: var(--danger); }
/* ===== Distribution chart ===== */
.chart {
position: relative;
display: flex;
align-items: flex-end;
gap: clamp(8px, 2vw, 20px);
height: 240px;
padding: 16px 6px 0;
border: 1px solid var(--line);
border-radius: var(--r-md);
background:
linear-gradient(0deg, var(--white), var(--white)),
var(--paper);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.chart-band {
position: absolute;
top: 0; bottom: 0;
background: linear-gradient(180deg, rgba(176,141,87,0.14), rgba(176,141,87,0.05));
border-left: 1px dashed var(--brass);
border-right: 1px dashed var(--brass);
z-index: 0;
pointer-events: none;
}
.chart-band-label {
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
font-size: 9.5px;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--brass-d);
font-weight: 600;
white-space: nowrap;
}
.bar {
position: relative;
flex: 1 1 0;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
height: 100%;
z-index: 1;
}
.bar-col {
width: 100%;
max-width: 56px;
border-radius: 8px 8px 3px 3px;
background: linear-gradient(180deg, var(--green-700), var(--green-d));
box-shadow: inset 0 0 0 1px rgba(176,141,87,0.3);
transition: height 0.55s cubic-bezier(.22,.61,.36,1), opacity 0.3s ease, background 0.3s ease;
position: relative;
}
.bar.is-subject .bar-col {
background: linear-gradient(180deg, var(--brass), var(--brass-d));
}
.bar.is-excluded .bar-col {
background: var(--line-2);
opacity: 0.5;
}
.bar-value {
font-size: 11px;
font-weight: 600;
color: var(--green-d);
margin-bottom: 6px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.bar.is-subject .bar-value { color: var(--brass-d); }
.bar.is-excluded .bar-value { color: var(--muted); }
.bar-label {
font-size: 10px;
color: var(--muted);
margin-top: 8px;
text-align: center;
line-height: 1.25;
max-width: 100%;
}
.bar.is-subject .bar-label { color: var(--brass-d); font-weight: 600; }
/* ===== Footer ===== */
.report-foot {
margin-top: 38px;
padding-top: 20px;
border-top: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
}
.report-foot p { margin: 0; font-size: 11.5px; color: var(--muted); max-width: 62ch; }
.btn-print {
font-family: inherit;
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.3px;
color: var(--green-d);
background: var(--white);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 9px 20px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease, transform 0.1s ease;
white-space: nowrap;
}
.btn-print:hover { background: var(--green-700); color: var(--brass-50); border-color: var(--green-700); }
.btn-print:active { transform: translateY(1px); }
.btn-print:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 18px);
background: var(--green-d);
color: var(--brass-50);
font-size: 13px;
font-weight: 500;
padding: 11px 20px;
border-radius: 999px;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
max-width: calc(100vw - 40px);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ===== Responsive ===== */
@media (max-width: 760px) {
.subject { grid-template-columns: 1fr; gap: 18px; }
.subject-photo { min-height: 190px; }
}
@media (max-width: 520px) {
body { padding: 12px; }
.sheet { padding: 20px 16px; border-radius: var(--r-md); }
.masthead-meta { text-align: left; align-items: flex-start; }
.spec-row { gap: 10px 0; }
.spec-row li { padding: 0 14px; }
.range-numbers { grid-template-columns: 1fr; gap: 14px; }
.range-cell, .range-cell--mid, .range-cell--end { align-items: flex-start; text-align: left; }
.range-cell--mid { border: 0; padding: 14px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.chart { height: 200px; gap: 5px; }
.bar-value { font-size: 9.5px; }
.bar-label { font-size: 9px; }
.report-foot { flex-direction: column; align-items: flex-start; }
}
@media print {
body { background: #fff; padding: 0; }
.sheet { box-shadow: none; border: 0; max-width: none; }
.btn-print { display: none; }
.toast { display: none; }
.comps-table-wrap { overflow: visible; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}(function () {
"use strict";
/* ---- Subject property ---- */
var SUBJECT = { sqft: 2480, beds: 4, baths: 2.5 };
/* ---- Comparable sales (fictional). adj = total dollar adjustment
applied to the comp's sold price to make it comparable to the subject.
Positive = subject superior (comp raised); negative = comp superior. ---- */
var COMPS = [
{ id: "c1", addr: "388 Magnolia Crescent", area: "Brookhaven Park", price: 742000, sqft: 2410, beds: 4, baths: 2.5, sold: "2026-05-02", dist: 0.2, adj: 16000, on: true },
{ id: "c2", addr: "1207 Sycamore Hollow", area: "Maple Ridge", price: 805000, sqft: 2690, beds: 4, baths: 3, sold: "2026-04-18", dist: 0.8, adj: -34000, on: true },
{ id: "c3", addr: "54 Linden Court", area: "Brookhaven Park", price: 698000, sqft: 2280, beds: 3, baths: 2.5, sold: "2026-03-29", dist: 0.5, adj: 41000, on: true },
{ id: "c4", addr: "920 Cedar Bluff Lane", area: "Maple Ridge", price: 769000, sqft: 2520, beds: 4, baths: 2.5, sold: "2026-05-21", dist: 1.1, adj: -7000, on: true },
{ id: "c5", addr: "33 Willow Bend", area: "Orchard Glen", price: 712000, sqft: 2350, beds: 4, baths: 2, sold: "2026-02-14", dist: 1.6, adj: 28000, on: true },
{ id: "c6", addr: "1488 Hawthorne Row", area: "Maple Ridge", price: 884000, sqft: 3010, beds: 5, baths: 3.5, sold: "2026-04-05", dist: 2.0, adj: -96000, on: false }
];
/* ---- Formatters ---- */
var fmtMoney = function (n) {
return "$" + Math.round(n).toLocaleString("en-US");
};
var fmtMoneyShort = function (n) {
return "$" + (Math.round(n / 1000)).toLocaleString("en-US") + "K";
};
var fmtSigned = function (n) {
var s = n > 0 ? "+" : n < 0 ? "−" : "±";
return s + "$" + Math.abs(Math.round(n)).toLocaleString("en-US");
};
var fmtDate = function (iso) {
var d = new Date(iso + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "2-digit" });
};
/* ---- DOM refs ---- */
var body = document.getElementById("comps-body");
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
/* ---- Build table rows ---- */
function buildRows() {
var html = "";
COMPS.forEach(function (c) {
var ppsf = c.price / c.sqft;
var adjPrice = c.price + c.adj;
var pillCls = c.adj > 0 ? "adj-pill--up" : c.adj < 0 ? "adj-pill--down" : "adj-pill--zero";
html +=
'<tr data-id="' + c.id + '" class="' + (c.on ? "" : "is-excluded") + '">' +
'<td class="col-toggle">' +
'<input type="checkbox" class="cb" ' + (c.on ? "checked" : "") +
' aria-label="Include ' + c.addr + ' in analysis" data-id="' + c.id + '">' +
"</td>" +
'<td class="col-addr">' +
'<span class="comp-addr">' + c.addr + "</span>" +
'<span class="comp-area">' + c.area + " · " + c.sqft.toLocaleString("en-US") + " sq ft</span>" +
"</td>" +
'<td class="num">' + fmtMoney(c.price) + "</td>" +
'<td class="num">$' + Math.round(ppsf) + "</td>" +
"<td>" + c.beds + " / " + c.baths + "</td>" +
"<td>" + fmtDate(c.sold) + "</td>" +
'<td class="num">' + c.dist.toFixed(1) + " mi</td>" +
'<td class="num"><span class="adj-pill ' + pillCls + '">' + fmtSigned(c.adj) + "</span></td>" +
'<td class="num"><strong>' + fmtMoney(adjPrice) + "</strong></td>" +
"</tr>";
});
body.innerHTML = html;
body.querySelectorAll(".cb").forEach(function (cb) {
cb.addEventListener("change", function () {
var comp = COMPS.find(function (c) { return c.id === cb.getAttribute("data-id"); });
if (!comp) return;
comp.on = cb.checked;
cb.closest("tr").classList.toggle("is-excluded", !cb.checked);
recalc();
toast(cb.checked ? comp.addr.split(" ").slice(0, 2).join(" ") + " included" :
comp.addr.split(" ").slice(0, 2).join(" ") + " excluded");
});
});
}
/* ---- Recalculate suggested range, footer and chart ---- */
function recalc() {
var active = COMPS.filter(function (c) { return c.on; });
var n = active.length;
var lowEl = document.getElementById("range-low");
var midEl = document.getElementById("range-mid");
var highEl = document.getElementById("range-high");
var ppsfEl = document.getElementById("range-ppsf");
var activeCountEl = document.getElementById("active-count");
activeCountEl.textContent = String(n);
if (n === 0) {
lowEl.textContent = midEl.textContent = highEl.textContent = "—";
ppsfEl.textContent = "select a comp";
document.getElementById("foot-price").textContent = "—";
document.getElementById("foot-ppsf").textContent = "—";
document.getElementById("foot-adj").textContent = "—";
document.getElementById("foot-count").textContent = "(0 of " + COMPS.length + ")";
document.getElementById("range-bar-fill").style.width = "0%";
renderChart(0, 0);
return;
}
var adjPrices = active.map(function (c) { return c.price + c.adj; });
var sumAdj = adjPrices.reduce(function (a, b) { return a + b; }, 0);
var sumRaw = active.reduce(function (a, c) { return a + c.price; }, 0);
var sumSqft = active.reduce(function (a, c) { return a + c.sqft; }, 0);
var avgAdj = sumAdj / n;
var blendedPpsf = sumAdj / sumSqft; // $/sqft on adjusted basis
// Subject-implied value from blended adjusted $/sqft.
var subjectValue = blendedPpsf * SUBJECT.sqft;
// Target reconciles the adjusted average with the $/sqft-implied value.
var target = avgAdj * 0.6 + subjectValue * 0.4;
// Range band: tighten/widen with comp dispersion (std dev), clamped.
var variance = adjPrices.reduce(function (a, p) { return a + Math.pow(p - avgAdj, 2); }, 0) / n;
var std = Math.sqrt(variance);
var band = Math.min(Math.max(std * 0.85, target * 0.025), target * 0.07);
var low = roundTo(target - band, 1000);
var high = roundTo(target + band, 1000);
var mid = roundTo(target, 500);
lowEl.textContent = fmtMoney(low);
midEl.textContent = fmtMoney(mid);
highEl.textContent = fmtMoney(high);
ppsfEl.textContent = "$" + Math.round(mid / SUBJECT.sqft) + " / sq ft";
// Footer (adjusted average row)
document.getElementById("foot-price").textContent = fmtMoney(sumRaw / n);
document.getElementById("foot-ppsf").textContent = "$" + Math.round(blendedPpsf);
document.getElementById("foot-adj").textContent = fmtMoney(avgAdj);
document.getElementById("foot-count").textContent = "(" + n + " of " + COMPS.length + ")";
updateRangeBar(low, mid, high);
renderChart(low, high);
}
function roundTo(n, step) { return Math.round(n / step) * step; }
/* ---- Range bar geometry ---- */
function updateRangeBar(low, mid, high) {
var span = high - low;
var pad = span * 0.9;
var scaleMin = low - pad;
var scaleMax = high + pad;
var total = scaleMax - scaleMin;
var pct = function (v) { return ((v - scaleMin) / total) * 100; };
var fill = document.getElementById("range-bar-fill");
fill.style.left = pct(low) + "%";
fill.style.width = (pct(high) - pct(low)) + "%";
document.getElementById("range-bar-marker").style.left = pct(mid) + "%";
document.getElementById("scale-min").textContent = fmtMoneyShort(scaleMin);
document.getElementById("scale-max").textContent = fmtMoneyShort(scaleMax);
}
/* ---- Distribution chart ---- */
var chartEl = document.getElementById("chart");
function renderChart(low, high) {
var values = COMPS.map(function (c) { return c.price + c.adj; });
// Include the subject target as a reference bar.
var midTarget = (low + high) / 2;
var all = values.concat([midTarget]);
var max = Math.max.apply(null, all) * 1.06;
var min = Math.min.apply(null, all) * 0.9;
var range = max - min || 1;
var pctH = function (v) { return Math.max(6, ((v - min) / range) * 100); };
chartEl.innerHTML = "";
COMPS.forEach(function (c) {
var v = c.price + c.adj;
var bar = document.createElement("div");
bar.className = "bar" + (c.on ? "" : " is-excluded");
var val = document.createElement("span");
val.className = "bar-value";
val.textContent = fmtMoneyShort(v);
var col = document.createElement("div");
col.className = "bar-col";
col.style.height = pctH(v) + "%";
var lbl = document.createElement("span");
lbl.className = "bar-label";
lbl.textContent = c.addr.split(" ").slice(0, 2).join(" ");
bar.appendChild(val);
bar.appendChild(col);
bar.appendChild(lbl);
chartEl.appendChild(bar);
});
// Subject target bar
var sBar = document.createElement("div");
sBar.className = "bar is-subject";
var sVal = document.createElement("span");
sVal.className = "bar-value";
sVal.textContent = fmtMoneyShort(midTarget);
var sCol = document.createElement("div");
sCol.className = "bar-col";
sCol.style.height = pctH(midTarget) + "%";
var sLbl = document.createElement("span");
sLbl.className = "bar-label";
sLbl.textContent = "Subject target";
sBar.appendChild(sVal);
sBar.appendChild(sCol);
sBar.appendChild(sLbl);
chartEl.appendChild(sBar);
}
/* ---- Print ---- */
document.getElementById("btn-print").addEventListener("click", function () {
toast("Opening print dialog…");
setTimeout(function () { window.print(); }, 250);
});
/* ---- Init ---- */
buildRows();
recalc();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Comparative Market Analysis — 412 Magnolia Crescent</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>
<div class="sheet" role="document">
<!-- ===== Masthead ===== -->
<header class="masthead">
<div class="brand">
<div class="brand-mark" aria-hidden="true">CR</div>
<div class="brand-text">
<p class="brand-name">Crescent & Howe</p>
<p class="brand-sub">Residential Brokerage · Est. 1998</p>
</div>
</div>
<div class="masthead-meta">
<span class="doc-kind">Comparative Market Analysis</span>
<span class="doc-ref">Report #CMA‑2026‑0612</span>
</div>
</header>
<div class="rule-brass" aria-hidden="true"></div>
<!-- ===== Subject property ===== -->
<section class="subject" aria-labelledby="subject-title">
<div class="subject-photo" role="img" aria-label="Photograph of the subject property, a craftsman home at dusk">
<span class="photo-tag">Subject</span>
<span class="photo-credit">Listing photography</span>
</div>
<div class="subject-body">
<p class="eyebrow">Prepared for the owners of</p>
<h1 id="subject-title" class="subject-address">412 Magnolia Crescent</h1>
<p class="subject-loc">Brookhaven Park · Maple Ridge, OR 97041</p>
<ul class="spec-row" aria-label="Subject property specifications">
<li><span class="spec-n">4</span><span class="spec-l">Beds</span></li>
<li><span class="spec-n">2.5</span><span class="spec-l">Baths</span></li>
<li><span class="spec-n">2,480</span><span class="spec-l">Sq Ft</span></li>
<li><span class="spec-n">0.21</span><span class="spec-l">Acres</span></li>
<li><span class="spec-n">1996</span><span class="spec-l">Built</span></li>
</ul>
<div class="subject-foot">
<div class="agent-chip">
<div class="agent-avatar" aria-hidden="true">DR</div>
<div>
<p class="agent-name">Delphine Howe</p>
<p class="agent-role">Principal Broker · Lic. #OR‑884213</p>
</div>
</div>
<p class="prep-date">Prepared June 8, 2026</p>
</div>
</div>
</section>
<!-- ===== Suggested range / summary ===== -->
<section class="summary" aria-labelledby="range-title">
<div class="summary-head">
<h2 id="range-title" class="section-title">Suggested List Range</h2>
<p class="section-note" id="range-note">Derived from <span id="active-count">5</span> active comparables · adjusted for differences against the subject.</p>
</div>
<div class="range-card">
<div class="range-numbers">
<div class="range-cell">
<span class="range-label">Low</span>
<span class="range-value" id="range-low">$0</span>
</div>
<div class="range-cell range-cell--mid">
<span class="range-label">Target</span>
<span class="range-value range-value--mid" id="range-mid">$0</span>
<span class="range-ppsf" id="range-ppsf">$0 / sq ft</span>
</div>
<div class="range-cell range-cell--end">
<span class="range-label">High</span>
<span class="range-value" id="range-high">$0</span>
</div>
</div>
<div class="range-bar" aria-hidden="true">
<div class="range-bar-track">
<div class="range-bar-fill" id="range-bar-fill"></div>
<div class="range-bar-marker" id="range-bar-marker"><span>Target</span></div>
</div>
<div class="range-bar-scale">
<span id="scale-min">$0</span>
<span id="scale-max">$0</span>
</div>
</div>
</div>
</section>
<!-- ===== Comparable sales table ===== -->
<section class="comps" aria-labelledby="comps-title">
<div class="comps-head">
<h2 id="comps-title" class="section-title">Comparable Sales</h2>
<p class="section-note">Toggle a comp out to exclude it from the suggested range and the distribution below.</p>
</div>
<div class="comps-table-wrap" role="region" aria-label="Comparable sales table" tabindex="0">
<table class="comps-table">
<thead>
<tr>
<th scope="col" class="col-toggle"><span class="sr-only">Include</span></th>
<th scope="col" class="col-addr">Address</th>
<th scope="col" class="num">Sold Price</th>
<th scope="col" class="num">$ / Sq Ft</th>
<th scope="col">Bd / Ba</th>
<th scope="col">Sold</th>
<th scope="col" class="num">Dist.</th>
<th scope="col" class="num">Adjust.</th>
<th scope="col" class="num">Adj. Price</th>
</tr>
</thead>
<tbody id="comps-body"><!-- rows injected by script.js --></tbody>
<tfoot>
<tr class="comps-foot">
<td></td>
<td class="col-addr">Adjusted average <span id="foot-count" class="foot-count">(5 of 6)</span></td>
<td class="num" id="foot-price">$0</td>
<td class="num" id="foot-ppsf">$0</td>
<td colspan="3"></td>
<td class="num"></td>
<td class="num" id="foot-adj">$0</td>
</tr>
</tfoot>
</table>
</div>
<p class="adj-legend">
<span class="legend-dot legend-dot--up" aria-hidden="true"></span> Positive adjustment = subject is superior, comp price raised.
<span class="legend-dot legend-dot--down" aria-hidden="true"></span> Negative = comp is superior, price lowered.
</p>
</section>
<!-- ===== Price distribution chart ===== -->
<section class="dist" aria-labelledby="dist-title">
<div class="comps-head">
<h2 id="dist-title" class="section-title">Adjusted Price Distribution</h2>
<p class="section-note">Each bar is an included comp at its adjusted value. The shaded band marks the suggested range.</p>
</div>
<div class="chart" id="chart" role="img" aria-label="Bar chart of adjusted comparable prices with the suggested list range shaded"></div>
</section>
<!-- ===== Footer ===== -->
<footer class="report-foot">
<p>This Comparative Market Analysis is an opinion of value, not an appraisal. All listings, sales and figures shown are fictional and for demonstration only.</p>
<button class="btn-print" id="btn-print" type="button">Print report</button>
</footer>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>CMA / Comp Report
A premium, print-ready Comparative Market Analysis presented as a single editorial sheet. The masthead carries the brokerage identity and a report reference, then the subject property leads with warm CSS “photography”, a serif address, and a hairline spec strip for beds, baths, square footage, lot size and year built — closing with the preparing broker and the date the analysis was run.
The heart of the report is the comparable-sales table. Each fictional row shows the sold price, dollars per square foot, bed and bath count, sold date, distance from the subject, and a signed adjustment pill that raises or lowers the comp to account for differences against the subject home. A live checkbox on every row lets you include or exclude that comp; struck-through styling marks the ones left out, and a footer reconciles the adjusted average across only the comps still in play.
Every toggle recalculates the suggested list range — a blended figure that weighs the adjusted average against a dollars-per-square-foot valuation of the subject — and animates the low/target/high figures, a brass-filled range slider with a target marker, and an adjusted-price distribution chart where each comp is a bar alongside a highlighted subject-target column. A small toast confirms each change, and a print action drops the chrome for a clean hard copy.
Illustrative UI only — sample listings and data are fictional; not a real real-estate service.