Real Estate — Listing Detail
A polished, editorial property detail page for a fictional luxury listing. It pairs a swappable hero gallery with thumbnails and a keyboard-friendly lightbox, a price and key-facts header, a description with read-more, a features grid, a stylized neighborhood map, rated nearby schools, an interactive monthly-payment estimator, and an agent contact card with a request-a-tour form. Built with semantic HTML, layered CSS, and vanilla JavaScript — no frameworks, no network.
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;
--sh-1: 0 1px 2px rgba(28, 42, 37, 0.05), 0 4px 14px rgba(28, 42, 37, 0.06);
--sh-2: 0 2px 6px rgba(28, 42, 37, 0.08), 0 18px 44px rgba(28, 42, 37, 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: var(--ivory);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 {
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 600;
margin: 0;
color: var(--ink);
letter-spacing: 0.2px;
}
p { margin: 0 0 0.8em; }
a { color: inherit; }
.wrap { width: min(1180px, 92vw); margin-inline: auto; }
/* ---------- Topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 30;
background: rgba(247, 244, 236, 0.86);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar__inner {
display: flex;
align-items: center;
gap: 22px;
height: 64px;
}
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; }
.brand__mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: var(--r-sm);
background: var(--green);
color: var(--brass-50);
font-family: "Cormorant Garamond", serif;
font-weight: 700;
font-size: 13px;
letter-spacing: 0.5px;
}
.brand__name {
font-family: "Cormorant Garamond", serif;
font-size: 19px;
font-weight: 600;
color: var(--green);
line-height: 1.1;
}
.brand__name em { color: var(--brass-d); font-style: italic; }
.topnav { margin-left: auto; display: flex; gap: 22px; }
.topnav a {
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
color: var(--ink-2);
padding-bottom: 2px;
border-bottom: 1.5px solid transparent;
transition: color 0.18s, border-color 0.18s;
}
.topnav a:hover { color: var(--green); border-color: var(--brass); }
/* ---------- Buttons ---------- */
.btn {
--bg: var(--white);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: inherit;
font-size: 14px;
font-weight: 600;
border-radius: var(--r-md);
padding: 11px 18px;
border: 1px solid var(--line-2);
background: var(--bg);
color: var(--ink);
cursor: pointer;
text-decoration: none;
transition: transform 0.12s ease, box-shadow 0.18s, background 0.18s, border-color 0.18s;
}
.btn:hover { box-shadow: var(--sh-1); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
.btn--sm { padding: 8px 14px; font-size: 13px; }
.btn--block { width: 100%; }
.btn--primary {
background: var(--green);
border-color: var(--green-d);
color: var(--paper);
}
.btn--primary:hover { background: var(--green-700); }
.btn--ghost { background: transparent; }
.btn--ghost:hover { background: var(--white); border-color: var(--brass); }
/* ---------- Page ---------- */
.page { padding: 26px 0 60px; }
.crumbs {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
font-size: 12.5px;
color: var(--muted);
margin-bottom: 18px;
}
.crumbs a { text-decoration: none; }
.crumbs a:hover { color: var(--green); }
.crumbs [aria-current] { color: var(--ink-2); font-weight: 600; }
/* ---------- Photo simulation ---------- */
.ph {
display: block;
width: 100%;
height: 100%;
background-size: cover;
}
.ph--living {
background:
radial-gradient(120% 90% at 78% 10%, rgba(255, 248, 232, 0.95) 0%, rgba(255, 248, 232, 0) 42%),
radial-gradient(90% 70% at 20% 100%, rgba(176, 141, 87, 0.5) 0%, rgba(176, 141, 87, 0) 55%),
linear-gradient(160deg, #d9c8a8 0%, #c2a87f 38%, #8a6f4c 100%);
}
.ph--kitchen {
background:
radial-gradient(80% 60% at 70% 20%, rgba(255, 252, 244, 0.9) 0%, rgba(255, 252, 244, 0) 45%),
linear-gradient(155deg, #efe7d6 0%, #cfc0a4 45%, #6f7a6d 100%);
}
.ph--bedroom {
background:
radial-gradient(90% 70% at 30% 18%, rgba(255, 246, 230, 0.92) 0%, rgba(255, 246, 230, 0) 48%),
linear-gradient(165deg, #e7d8c0 0%, #b89a74 55%, #5b4a39 100%);
}
.ph--bath {
background:
radial-gradient(70% 60% at 75% 25%, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0) 50%),
linear-gradient(150deg, #dfe6e2 0%, #a9bcb3 50%, #355247 100%);
}
.ph--garden {
background:
radial-gradient(100% 80% at 50% 0%, rgba(245, 240, 215, 0.85) 0%, rgba(245, 240, 215, 0) 45%),
linear-gradient(170deg, #b7c39a 0%, #6f8a5d 45%, #2f4a34 100%);
}
.ph--facade {
background:
radial-gradient(110% 80% at 50% 110%, rgba(176, 141, 87, 0.55) 0%, rgba(176, 141, 87, 0) 50%),
linear-gradient(200deg, #6b5b86 0%, #b07a63 45%, #e0a86a 100%);
}
/* ---------- Gallery ---------- */
.gallery {
display: grid;
grid-template-columns: 1fr 116px;
gap: 14px;
margin-bottom: 28px;
}
.gallery__main {
position: relative;
margin: 0;
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-2);
aspect-ratio: 16 / 10;
}
.gallery__mainbtn {
display: block;
width: 100%;
height: 100%;
padding: 0;
border: 0;
background: none;
cursor: zoom-in;
position: relative;
}
.gallery__mainbtn:focus-visible { outline: 3px solid var(--brass); outline-offset: -3px; }
.gallery__mainbtn .ph { transition: transform 0.5s ease; }
.gallery__mainbtn:hover .ph { transform: scale(1.03); }
.gallery__caption {
position: absolute;
left: 16px;
bottom: 14px;
font-size: 12.5px;
font-weight: 500;
color: #fff;
background: rgba(22, 48, 42, 0.55);
backdrop-filter: blur(4px);
padding: 6px 12px;
border-radius: 999px;
letter-spacing: 0.3px;
}
.gallery__count {
position: absolute;
right: 14px;
bottom: 14px;
font-size: 12px;
font-weight: 600;
color: #fff;
background: rgba(22, 48, 42, 0.55);
padding: 5px 11px;
border-radius: 999px;
}
.gallery__thumbs {
display: flex;
flex-direction: column;
gap: 14px;
}
.thumb {
position: relative;
flex: 1;
min-height: 0;
border: 2px solid transparent;
border-radius: var(--r-md);
overflow: hidden;
padding: 0;
cursor: pointer;
background: none;
box-shadow: var(--sh-1);
transition: border-color 0.18s, transform 0.18s;
}
.thumb:hover { transform: translateY(-2px); }
.thumb.is-active { border-color: var(--brass); }
.thumb.is-active::after {
content: "";
position: absolute;
inset: 0;
box-shadow: inset 0 0 0 2px var(--paper);
border-radius: 10px;
}
.thumb:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
/* ---------- Badges ---------- */
.badge {
position: absolute;
top: 16px;
left: 16px;
font-size: 11.5px;
font-weight: 700;
letter-spacing: 0.6px;
text-transform: uppercase;
padding: 6px 12px;
border-radius: 999px;
}
.badge--status {
background: var(--green);
color: var(--brass-50);
box-shadow: 0 4px 14px rgba(22, 48, 42, 0.3);
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 360px;
gap: 34px;
align-items: start;
}
/* ---------- Price header ---------- */
.head {
padding-bottom: 24px;
border-bottom: 1px solid var(--line);
margin-bottom: 8px;
}
.head__top {
display: flex;
justify-content: space-between;
gap: 20px;
align-items: flex-end;
flex-wrap: wrap;
}
.head__price {
font-family: "Cormorant Garamond", serif;
font-size: 40px;
font-weight: 700;
line-height: 1;
color: var(--green);
margin: 0 0 6px;
}
.head__addr { font-size: 27px; line-height: 1.1; margin-bottom: 2px; }
.head__sub { color: var(--muted); margin: 0; font-size: 14px; }
.head__actions { display: flex; gap: 10px; }
.heart { font-size: 16px; line-height: 1; }
.btn[aria-pressed="true"] .heart { color: var(--danger); }
.btn[aria-pressed="true"] { border-color: var(--brass); }
.facts {
list-style: none;
margin: 22px 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.facts li {
background: var(--paper);
padding: 14px 10px;
text-align: center;
}
.facts strong {
display: block;
font-family: "Cormorant Garamond", serif;
font-size: 22px;
font-weight: 600;
color: var(--ink);
}
.facts span {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.7px;
color: var(--muted);
}
/* ---------- Content blocks ---------- */
.block { padding: 26px 0; border-bottom: 1px solid var(--line); }
.block__title {
font-size: 25px;
margin-bottom: 12px;
position: relative;
padding-left: 16px;
}
.block__title::before {
content: "";
position: absolute;
left: 0;
top: 6px;
bottom: 6px;
width: 3px;
border-radius: 3px;
background: linear-gradient(var(--brass), var(--brass-d));
}
.block p { color: var(--ink-2); max-width: 64ch; }
.block__lead { font-size: 15.5px; }
.readmore {
background: none;
border: 0;
color: var(--brass-d);
font-weight: 600;
font-family: inherit;
font-size: 14px;
cursor: pointer;
padding: 0;
text-decoration: underline;
text-underline-offset: 3px;
}
.readmore:hover { color: var(--green); }
/* ---------- Features ---------- */
.feat {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 6px;
}
.feat__cell {
display: flex;
align-items: center;
gap: 10px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 13px 14px;
font-size: 13.5px;
font-weight: 500;
color: var(--ink-2);
transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s;
}
.feat__cell:hover {
border-color: var(--brass);
transform: translateY(-2px);
box-shadow: var(--sh-1);
}
.feat__ico { color: var(--brass-d); font-size: 13px; }
/* ---------- Map ---------- */
.map {
position: relative;
margin-top: 8px;
aspect-ratio: 16 / 7;
border-radius: var(--r-lg);
overflow: hidden;
border: 1px solid var(--line);
background:
radial-gradient(60% 50% at 85% 20%, rgba(176, 141, 87, 0.12), transparent 60%),
linear-gradient(135deg, #eef0e8 0%, #e7ece2 60%, #dfe6da 100%);
box-shadow: var(--sh-1);
}
.map__road { position: absolute; background: var(--white); box-shadow: 0 0 0 1px var(--line); }
.map__road--h { left: 0; right: 0; height: 9px; }
.map__road--v { top: 0; bottom: 0; width: 9px; }
.map__park {
position: absolute;
width: 100px;
height: 70px;
border-radius: 14px;
background: linear-gradient(160deg, #cdd9b8, #9fb586);
opacity: 0.85;
}
.map__water {
position: absolute;
right: -30px;
top: -20px;
width: 150px;
height: 120px;
border-radius: 50%;
background: linear-gradient(160deg, #bcd2d4, #8fb2b3);
opacity: 0.7;
}
.map__pin { position: absolute; transform: translate(-50%, -50%); }
.map__dot {
display: block;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--green);
border: 3px solid var(--paper);
box-shadow: 0 4px 12px rgba(22, 48, 42, 0.4);
}
.map__pin--home .map__dot {
width: 22px;
height: 22px;
background: var(--brass);
animation: pulse 2.4s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(176, 141, 87, 0.45); }
70% { box-shadow: 0 0 0 14px rgba(176, 141, 87, 0); }
100% { box-shadow: 0 0 0 0 rgba(176, 141, 87, 0); }
}
.map__plabel {
position: absolute;
left: 50%;
top: -10px;
transform: translate(-50%, -100%);
white-space: nowrap;
font-size: 11px;
font-weight: 600;
background: var(--green);
color: var(--brass-50);
padding: 4px 9px;
border-radius: 999px;
}
.map__poi {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--paper);
border: 3px solid var(--brass-d);
transform: translate(-50%, -50%);
cursor: help;
}
.map__poi::after {
content: attr(data-poi);
position: absolute;
left: 50%;
bottom: 140%;
transform: translateX(-50%) scale(0.9);
white-space: nowrap;
font-size: 11px;
font-weight: 600;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line);
padding: 3px 8px;
border-radius: 6px;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s, transform 0.15s;
}
.map__poi:hover::after { opacity: 1; transform: translateX(-50%) scale(1); }
.walk {
list-style: none;
display: flex;
gap: 10px;
flex-wrap: wrap;
padding: 0;
margin: 16px 0 0;
}
.walk li {
font-size: 12.5px;
color: var(--muted);
background: var(--green-50);
border-radius: 999px;
padding: 7px 14px;
}
.walk strong { color: var(--green); font-size: 14px; }
/* ---------- Schools ---------- */
.schools { list-style: none; padding: 0; margin: 8px 0 12px; display: grid; gap: 10px; }
.school {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 14px;
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 13px 16px;
transition: box-shadow 0.18s, transform 0.18s;
}
.school:hover { box-shadow: var(--sh-1); transform: translateY(-1px); }
.school__rating {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 10px;
font-family: "Cormorant Garamond", serif;
font-weight: 700;
font-size: 19px;
color: #fff;
}
.school__rating[data-score="9"] { background: var(--ok); }
.school__rating[data-score="8"] { background: #4a9e6f; }
.school__rating[data-score="7"] { background: var(--warn); }
.school__name { font-weight: 600; margin: 0; font-size: 14.5px; }
.school__meta { margin: 0; font-size: 12.5px; color: var(--muted); }
.school__bar {
display: block;
width: 80px;
height: 6px;
border-radius: 999px;
background: var(--green-50);
overflow: hidden;
}
.school__bar span { display: block; height: 100%; background: var(--brass); border-radius: 999px; }
.fineprint { font-size: 11.5px; color: var(--muted); margin: 8px 0 0; font-style: italic; }
/* ---------- Cards (sidebar) ---------- */
.col-side { position: sticky; top: 84px; display: grid; gap: 20px; }
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: var(--sh-1);
}
.card__title { font-size: 21px; margin-bottom: 6px; }
/* ---------- Payment ---------- */
.pay__total {
font-family: "Cormorant Garamond", serif;
font-size: 36px;
font-weight: 700;
color: var(--green);
margin: 0 0 16px;
line-height: 1;
}
.pay__per { font-family: "Inter", sans-serif; font-size: 13px; font-weight: 500; color: var(--muted); margin-left: 6px; }
.field { margin-bottom: 14px; }
.field label {
display: flex;
justify-content: space-between;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
margin-bottom: 6px;
}
.field label span { color: var(--brass-d); }
.field__money {
display: flex;
align-items: center;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--white);
overflow: hidden;
}
.field__money span { padding: 0 4px 0 12px; color: var(--muted); font-weight: 600; }
.field__money input { border: 0; }
.field input[type="text"],
.field input[type="email"],
.field input[type="date"],
.field input[type="number"],
.field select,
.field textarea {
width: 100%;
font-family: inherit;
font-size: 14px;
color: var(--ink);
padding: 10px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--white);
transition: border-color 0.16s, box-shadow 0.16s;
}
.field__money input { padding-left: 4px; }
.field input:focus,
.field select:focus,
.field textarea:focus {
outline: none;
border-color: var(--brass);
box-shadow: 0 0 0 3px var(--brass-50);
}
.field textarea { resize: vertical; }
.field input[type="range"] {
width: 100%;
margin: 4px 0 2px;
accent-color: var(--green);
}
.field__hint { font-size: 12px; color: var(--muted); }
.pay__break {
margin: 4px 0 0;
padding: 14px 0 4px;
border-top: 1px solid var(--line);
}
.pay__break div { display: flex; justify-content: space-between; padding: 4px 0; }
.pay__break dt { font-size: 13px; color: var(--ink-2); margin: 0; }
.pay__break dd { font-size: 13.5px; font-weight: 600; margin: 0; }
/* ---------- Agent ---------- */
.agent__top { display: flex; gap: 14px; align-items: center; margin-bottom: 18px; }
.agent__avatar {
display: grid;
place-items: center;
width: 56px;
height: 56px;
flex: 0 0 56px;
border-radius: 50%;
background: linear-gradient(155deg, var(--green-700), var(--green-d));
color: var(--brass-50);
font-family: "Cormorant Garamond", serif;
font-weight: 700;
font-size: 19px;
letter-spacing: 0.5px;
}
.agent__name { font-family: "Cormorant Garamond", serif; font-size: 20px; font-weight: 600; margin: 0; }
.agent__role { margin: 0; font-size: 12px; color: var(--muted); }
.agent__rating { margin: 4px 0 0; font-size: 13px; color: var(--brass); }
.agent__rating span { color: var(--muted); font-size: 12px; }
.tour__title { font-family: "Cormorant Garamond", serif; font-size: 18px; font-weight: 600; margin: 0 0 12px; color: var(--ink); }
.tour .btn--block + .btn--block { margin-top: 8px; }
/* ---------- Footer ---------- */
.foot {
border-top: 1px solid var(--line);
background: var(--paper);
margin-top: 30px;
}
.foot__inner { padding: 26px 0; }
.foot p { margin: 0; font-size: 13px; color: var(--ink-2); }
.foot__sm { color: var(--muted); font-size: 12px; margin-top: 4px; }
/* ---------- Lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 60;
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
padding: 24px;
background: rgba(18, 30, 26, 0.86);
backdrop-filter: blur(6px);
animation: fade 0.2s ease;
}
.lightbox[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.lightbox__fig { margin: 0; width: min(960px, 80vw); max-width: 80vw; }
.lightbox__img {
display: block;
width: 100%;
aspect-ratio: 16 / 10;
border-radius: var(--r-lg);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.5);
}
.lightbox__cap { text-align: center; color: var(--brass-50); font-size: 13px; margin-top: 14px; }
.lightbox__close {
position: absolute;
top: 20px;
right: 24px;
width: 42px;
height: 42px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.08);
color: #fff;
font-size: 18px;
cursor: pointer;
transition: background 0.16s;
}
.lightbox__close:hover { background: rgba(255, 255, 255, 0.2); }
.lightbox__nav {
width: 48px;
height: 48px;
flex: 0 0 48px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.08);
color: #fff;
font-size: 26px;
line-height: 1;
cursor: pointer;
transition: background 0.16s, transform 0.12s;
}
.lightbox__nav:hover { background: rgba(255, 255, 255, 0.2); }
.lightbox__nav:active { transform: scale(0.94); }
.lightbox :focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
z-index: 80;
background: var(--green-d);
color: var(--paper);
font-size: 14px;
font-weight: 500;
padding: 13px 22px;
border-radius: var(--r-md);
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
max-width: min(90vw, 420px);
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
.toast::before { content: "✦ "; color: var(--brass); }
/* ---------- Responsive ---------- */
@media (max-width: 980px) {
.layout { grid-template-columns: 1fr; }
.col-side { position: static; }
.topnav { display: none; }
}
@media (max-width: 720px) {
.feat { grid-template-columns: repeat(2, 1fr); }
.facts { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 520px) {
body { font-size: 14.5px; }
.gallery { grid-template-columns: 1fr; }
.gallery__thumbs {
flex-direction: row;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 4px;
}
.thumb { flex: 0 0 84px; height: 64px; }
.head__top { flex-direction: column; align-items: flex-start; }
.head__actions { width: 100%; }
.head__actions .btn { flex: 1; }
.head__price { font-size: 34px; }
.head__addr { font-size: 23px; }
.facts { grid-template-columns: repeat(2, 1fr); }
.feat { grid-template-columns: 1fr; }
.block__title { font-size: 22px; }
.lightbox__nav { width: 40px; height: 40px; flex-basis: 40px; }
.lightbox__fig { width: 86vw; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.querySelector("[data-toast]");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 3200);
}
/* ---------- Photo data ---------- */
var ORDER = ["living", "kitchen", "bedroom", "bath", "garden", "facade"];
var LABELS = {
living: "Living room · south light",
kitchen: "Chef's kitchen",
bedroom: "Primary suite",
bath: "Spa bath",
garden: "Walled garden",
facade: "Front façade at dusk"
};
var current = "living";
var mainPh = document.querySelector(".gallery__main .ph");
var mainCaption = document.querySelector(".gallery__caption");
var countEl = document.querySelector("[data-count]");
var thumbs = Array.prototype.slice.call(document.querySelectorAll(".thumb"));
function setPhotoClass(el, key) {
ORDER.forEach(function (k) {
el.classList.remove("ph--" + k);
});
el.classList.add("ph--" + key);
}
function selectPhoto(key) {
if (!LABELS[key]) return;
current = key;
setPhotoClass(mainPh, key);
if (mainCaption) mainCaption.textContent = LABELS[key];
if (countEl) countEl.textContent = (ORDER.indexOf(key) + 1) + " / " + ORDER.length;
thumbs.forEach(function (t) {
var active = t.getAttribute("data-thumb") === key;
t.classList.toggle("is-active", active);
t.setAttribute("aria-selected", active ? "true" : "false");
});
if (lbOpen) renderLightbox();
}
thumbs.forEach(function (t) {
t.addEventListener("click", function () {
selectPhoto(t.getAttribute("data-thumb"));
});
});
/* ---------- Lightbox ---------- */
var lb = document.querySelector("[data-lb]");
var lbImg = document.querySelector("[data-lb-img]");
var lbCap = document.querySelector("[data-lb-cap]");
var lbOpen = false;
var lastFocus = null;
function renderLightbox() {
setPhotoClass(lbImg, current);
lbCap.textContent = LABELS[current];
}
function openLightbox() {
lastFocus = document.activeElement;
renderLightbox();
lb.hidden = false;
lbOpen = true;
document.body.style.overflow = "hidden";
var closeBtn = lb.querySelector("[data-lb-close]");
if (closeBtn) closeBtn.focus();
}
function closeLightbox() {
lb.hidden = true;
lbOpen = false;
document.body.style.overflow = "";
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
function step(dir) {
var i = ORDER.indexOf(current);
var next = (i + dir + ORDER.length) % ORDER.length;
selectPhoto(ORDER[next]);
}
var mainBtn = document.querySelector("[data-lightbox]");
if (mainBtn) mainBtn.addEventListener("click", openLightbox);
var lbClose = document.querySelector("[data-lb-close]");
if (lbClose) lbClose.addEventListener("click", closeLightbox);
var lbPrev = document.querySelector("[data-lb-prev]");
if (lbPrev) lbPrev.addEventListener("click", function () { step(-1); });
var lbNext = document.querySelector("[data-lb-next]");
if (lbNext) lbNext.addEventListener("click", function () { step(1); });
if (lb) {
lb.addEventListener("click", function (e) {
if (e.target === lb) closeLightbox();
});
}
document.addEventListener("keydown", function (e) {
if (!lbOpen) return;
if (e.key === "Escape") closeLightbox();
else if (e.key === "ArrowLeft") step(-1);
else if (e.key === "ArrowRight") step(1);
});
/* ---------- Read more ---------- */
var readBtn = document.querySelector("[data-readmore]");
if (readBtn) {
var moreP = document.querySelector(".block__more");
readBtn.addEventListener("click", function () {
var expanded = readBtn.getAttribute("aria-expanded") === "true";
readBtn.setAttribute("aria-expanded", expanded ? "false" : "true");
moreP.hidden = expanded;
readBtn.textContent = expanded ? "Read more" : "Read less";
});
}
/* ---------- Save / Share ---------- */
var saveBtn = document.querySelector("[data-save]");
if (saveBtn) {
var saveLabel = saveBtn.querySelector("[data-save-label]");
saveBtn.addEventListener("click", function () {
var saved = saveBtn.getAttribute("aria-pressed") === "true";
saveBtn.setAttribute("aria-pressed", saved ? "false" : "true");
if (saveLabel) saveLabel.textContent = saved ? "Save" : "Saved";
var heart = saveBtn.querySelector(".heart");
if (heart) heart.textContent = saved ? "♡" : "♥";
toast(saved ? "Removed from saved homes." : "Saved to your collection.");
});
}
var shareBtn = document.querySelector("[data-share]");
if (shareBtn) {
shareBtn.addEventListener("click", function () {
toast("Listing link copied to clipboard.");
});
}
var callBtn = document.querySelector("[data-call]");
if (callBtn) {
callBtn.addEventListener("click", function () {
toast("Calling Eleanor Vane · (555) 0142-880…");
});
}
/* ---------- Payment estimate ---------- */
var priceInput = document.getElementById("price");
var downInput = document.getElementById("down");
var rateInput = document.getElementById("rate");
var termInput = document.getElementById("term");
var monthlyEl = document.querySelector("[data-monthly]");
var piEl = document.querySelector("[data-pi]");
var taxEl = document.querySelector("[data-tax]");
var downPctEl = document.querySelector("[data-downpct]");
var downAmtEl = document.querySelector("[data-downamt]");
var rateVEl = document.querySelector("[data-ratev]");
function money(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
function recalc() {
var price = parseFloat(priceInput.value) || 0;
var downPct = parseFloat(downInput.value) || 0;
var rate = parseFloat(rateInput.value) || 0;
var years = parseInt(termInput.value, 10) || 30;
var downAmt = price * (downPct / 100);
var loan = Math.max(price - downAmt, 0);
var monthlyRate = rate / 100 / 12;
var n = years * 12;
var pi;
if (monthlyRate === 0) {
pi = loan / n;
} else {
var f = Math.pow(1 + monthlyRate, n);
pi = loan * (monthlyRate * f) / (f - 1);
}
var tax = price * 0.0075 / 12; // ~0.75% annual property tax
var ins = price * 0.00169 / 12; // illustrative insurance
var total = pi + tax + ins;
if (downPctEl) downPctEl.textContent = downPct + "%";
if (downAmtEl) downAmtEl.textContent = money(downAmt) + " down";
if (rateVEl) rateVEl.textContent = rate.toFixed(1) + "%";
if (piEl) piEl.textContent = money(pi);
if (taxEl) taxEl.textContent = money(tax);
if (monthlyEl) monthlyEl.textContent = money(total);
}
[priceInput, downInput, rateInput, termInput].forEach(function (el) {
if (el) el.addEventListener("input", recalc);
});
recalc();
/* ---------- Tour form ---------- */
var form = document.querySelector("[data-tour]");
if (form) {
form.addEventListener("submit", function (e) {
e.preventDefault();
var name = form.querySelector("#name");
var email = form.querySelector("#email");
if (!name.value.trim()) {
name.focus();
toast("Please add your name so Eleanor can reach you.");
return;
}
if (!email.checkValidity() || !email.value.trim()) {
email.focus();
toast("Please enter a valid email address.");
return;
}
var first = name.value.trim().split(" ")[0];
toast("Thanks, " + first + "! Eleanor will confirm your tour shortly.");
form.reset();
});
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>The Calderwood Residence — Listing Detail</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>
<header class="topbar">
<div class="wrap topbar__inner">
<a class="brand" href="#" aria-label="Marlowe & Vane Estates home">
<span class="brand__mark" aria-hidden="true">M&V</span>
<span class="brand__name">Marlowe & Vane <em>Estates</em></span>
</a>
<nav class="topnav" aria-label="Primary">
<a href="#overview">Overview</a>
<a href="#features">Features</a>
<a href="#neighborhood">Neighborhood</a>
<a href="#schools">Schools</a>
<a href="#agent">Contact</a>
</nav>
<button class="btn btn--ghost btn--sm" data-share>Share</button>
</div>
</header>
<main class="wrap page">
<!-- Breadcrumb -->
<nav class="crumbs" aria-label="Breadcrumb">
<a href="#">Homes for Sale</a>
<span aria-hidden="true">/</span>
<a href="#">Westhaven, CA</a>
<span aria-hidden="true">/</span>
<span aria-current="page">42 Calderwood Lane</span>
</nav>
<!-- Gallery -->
<section class="gallery" id="overview" aria-label="Property photos">
<figure class="gallery__main">
<button class="gallery__mainbtn" data-lightbox aria-label="Open photo in fullscreen">
<span class="ph ph--living" data-photo="living" aria-hidden="true"></span>
<span class="gallery__caption">Living room · south light</span>
</button>
<span class="badge badge--status">For Sale</span>
<span class="gallery__count" data-count>1 / 6</span>
</figure>
<div class="gallery__thumbs" role="listbox" aria-label="Choose a photo">
<button class="thumb is-active" role="option" aria-selected="true" data-thumb="living" data-label="Living room · south light"><span class="ph ph--living" aria-hidden="true"></span></button>
<button class="thumb" role="option" aria-selected="false" data-thumb="kitchen" data-label="Chef's kitchen"><span class="ph ph--kitchen" aria-hidden="true"></span></button>
<button class="thumb" role="option" aria-selected="false" data-thumb="bedroom" data-label="Primary suite"><span class="ph ph--bedroom" aria-hidden="true"></span></button>
<button class="thumb" role="option" aria-selected="false" data-thumb="bath" data-label="Spa bath"><span class="ph ph--bath" aria-hidden="true"></span></button>
<button class="thumb" role="option" aria-selected="false" data-thumb="garden" data-label="Walled garden"><span class="ph ph--garden" aria-hidden="true"></span></button>
<button class="thumb" role="option" aria-selected="false" data-thumb="facade" data-label="Front façade at dusk"><span class="ph ph--facade" aria-hidden="true"></span></button>
</div>
</section>
<div class="layout">
<!-- Main column -->
<div class="col-main">
<!-- Price header -->
<section class="head">
<div class="head__top">
<div>
<p class="head__price">$2,395,000</p>
<h1 class="head__addr">42 Calderwood Lane</h1>
<p class="head__sub">Westhaven, CA 94521 · Caldera Heights</p>
</div>
<div class="head__actions">
<button class="btn btn--ghost" data-save aria-pressed="false">
<span class="heart" aria-hidden="true">♡</span> <span data-save-label>Save</span>
</button>
<a class="btn btn--primary" href="#agent">Request a tour</a>
</div>
</div>
<ul class="facts" aria-label="Key facts">
<li><strong>4</strong><span>Beds</span></li>
<li><strong>3.5</strong><span>Baths</span></li>
<li><strong>3,180</strong><span>Sq Ft</span></li>
<li><strong>0.42</strong><span>Acre Lot</span></li>
<li><strong>1929</strong><span>Year Built</span></li>
<li><strong>$753</strong><span>Per Sq Ft</span></li>
</ul>
</section>
<!-- Description -->
<section class="block">
<h2 class="block__title">About this home</h2>
<p>Set behind a low brass-tipped gate on one of Caldera Heights' most coveted streets, the Calderwood Residence pairs 1929 Spanish-Colonial bones with a quietly modern renovation. Hand-troweled plaster, reclaimed oak floors, and steel-framed windows frame a sun-washed great room that opens onto a walled garden with mature olive trees.</p>
<p>The chef's kitchen anchors the main level with honed marble, a paneled refrigerator, and a breakfast banquette. Upstairs, the primary suite reads like a private retreat — vaulted ceilings, a dressing room, and a spa bath clad in zellige tile. A detached studio over the two-car garage offers flexible space for a home office or guest quarters.</p>
<button class="readmore" data-readmore aria-expanded="false">Read more</button>
<p class="block__more" hidden>Additional upgrades include a fully owned solar array, smart climate zoning, EV charging, and a drought-tolerant landscape plan completed in 2024. Offered fully fictional and for illustration only.</p>
</section>
<!-- Features -->
<section class="block" id="features">
<h2 class="block__title">Features & finishes</h2>
<div class="feat">
<div class="feat__cell"><span class="feat__ico">✶</span>Spanish-Colonial, restored 2023</div>
<div class="feat__cell"><span class="feat__ico">✶</span>Honed marble chef's kitchen</div>
<div class="feat__cell"><span class="feat__ico">✶</span>Reclaimed white-oak floors</div>
<div class="feat__cell"><span class="feat__ico">✶</span>Steel-framed casement windows</div>
<div class="feat__cell"><span class="feat__ico">✶</span>Walled garden, olive grove</div>
<div class="feat__cell"><span class="feat__ico">✶</span>Detached studio over garage</div>
<div class="feat__cell"><span class="feat__ico">✶</span>Owned solar + EV charging</div>
<div class="feat__cell"><span class="feat__ico">✶</span>Zellige-tiled spa bath</div>
<div class="feat__cell"><span class="feat__ico">✶</span>Smart climate zoning</div>
</div>
</section>
<!-- Neighborhood map -->
<section class="block" id="neighborhood">
<h2 class="block__title">Neighborhood</h2>
<p class="block__lead">Caldera Heights is a leafy, walkable district known for its terracotta rooftops, weekend produce market, and a half-mile stroll to Westhaven's café row.</p>
<div class="map" role="img" aria-label="Stylized neighborhood map showing the home and nearby points of interest">
<span class="map__road map__road--h" style="top:38%"></span>
<span class="map__road map__road--h" style="top:68%"></span>
<span class="map__road map__road--v" style="left:30%"></span>
<span class="map__road map__road--v" style="left:66%"></span>
<span class="map__park" style="left:8%;top:46%"></span>
<span class="map__water"></span>
<span class="map__pin map__pin--home" style="left:30%;top:38%">
<span class="map__dot"></span><span class="map__plabel">This home</span>
</span>
<span class="map__poi" style="left:66%;top:68%" data-poi="Westhaven Market"></span>
<span class="map__poi" style="left:8%;top:64%" data-poi="Caldera Park"></span>
<span class="map__poi" style="left:66%;top:18%" data-poi="Café Row"></span>
</div>
<ul class="walk" aria-label="Walkability scores">
<li><strong>92</strong> Walk Score</li>
<li><strong>78</strong> Transit</li>
<li><strong>88</strong> Bike</li>
</ul>
</section>
<!-- Schools -->
<section class="block" id="schools">
<h2 class="block__title">Nearby schools</h2>
<ul class="schools">
<li class="school">
<span class="school__rating" data-score="9">9</span>
<div class="school__body">
<p class="school__name">Caldera Heights Elementary</p>
<p class="school__meta">Public · K–5 · 0.4 mi</p>
</div>
<span class="school__bar"><span style="width:90%"></span></span>
</li>
<li class="school">
<span class="school__rating" data-score="8">8</span>
<div class="school__body">
<p class="school__name">Westhaven Middle School</p>
<p class="school__meta">Public · 6–8 · 1.1 mi</p>
</div>
<span class="school__bar"><span style="width:80%"></span></span>
</li>
<li class="school">
<span class="school__rating" data-score="7">7</span>
<div class="school__body">
<p class="school__name">Marlowe Preparatory</p>
<p class="school__meta">Private · 9–12 · 1.8 mi</p>
</div>
<span class="school__bar"><span style="width:70%"></span></span>
</li>
</ul>
<p class="fineprint">Ratings are illustrative and do not reflect any real assessment.</p>
</section>
</div>
<!-- Sidebar -->
<aside class="col-side">
<!-- Payment estimate -->
<section class="card pay">
<h2 class="card__title">Monthly payment</h2>
<p class="pay__total"><span data-monthly>$13,940</span><span class="pay__per">/mo est.</span></p>
<div class="field">
<label for="price">Home price</label>
<div class="field__money"><span>$</span><input type="number" id="price" value="2395000" min="0" step="1000" inputmode="numeric" /></div>
</div>
<div class="field">
<label for="down">Down payment <span data-downpct>20%</span></label>
<input type="range" id="down" min="0" max="50" value="20" step="1" />
<div class="field__hint" data-downamt>$479,000 down</div>
</div>
<div class="field">
<label for="rate">Interest rate <span data-ratev>6.5%</span></label>
<input type="range" id="rate" min="2" max="9" value="6.5" step="0.1" />
</div>
<div class="field">
<label for="term">Loan term</label>
<select id="term">
<option value="30">30-year fixed</option>
<option value="20">20-year fixed</option>
<option value="15">15-year fixed</option>
</select>
</div>
<dl class="pay__break">
<div><dt>Principal & interest</dt><dd data-pi>$12,107</dd></div>
<div><dt>Property tax</dt><dd data-tax>$1,496</dd></div>
<div><dt>Insurance</dt><dd>$337</dd></div>
</dl>
<p class="fineprint">Estimate only — fictional figures, not financial advice.</p>
</section>
<!-- Agent card -->
<section class="card agent" id="agent">
<div class="agent__top">
<span class="agent__avatar" aria-hidden="true">EV</span>
<div>
<p class="agent__name">Eleanor Vane</p>
<p class="agent__role">Listing Agent · Lic. #CA-0148823</p>
<p class="agent__rating" aria-label="Rated 4.9 out of 5">★★★★★ <span>4.9 · 212 sales</span></p>
</div>
</div>
<form class="tour" data-tour novalidate>
<p class="tour__title">Request a private tour</p>
<div class="field">
<label for="name">Full name</label>
<input type="text" id="name" name="name" autocomplete="name" required placeholder="Your name" />
</div>
<div class="field">
<label for="email">Email</label>
<input type="email" id="email" name="email" autocomplete="email" required placeholder="[email protected]" />
</div>
<div class="field">
<label for="date">Preferred date</label>
<input type="date" id="date" name="date" />
</div>
<div class="field">
<label for="msg">Message</label>
<textarea id="msg" name="msg" rows="3" placeholder="I'd love to see the garden…"></textarea>
</div>
<button class="btn btn--primary btn--block" type="submit">Request tour</button>
<button class="btn btn--ghost btn--block" type="button" data-call>Call Eleanor</button>
</form>
</section>
</aside>
</div>
</main>
<footer class="foot">
<div class="wrap foot__inner">
<p>Marlowe & Vane Estates · Illustrative demo. Listings, prices, and people are fictional.</p>
<p class="foot__sm">© 2026 · For showcase use only.</p>
</div>
</footer>
<!-- Lightbox -->
<div class="lightbox" data-lb hidden>
<button class="lightbox__close" data-lb-close aria-label="Close fullscreen">✕</button>
<button class="lightbox__nav lightbox__nav--prev" data-lb-prev aria-label="Previous photo">‹</button>
<figure class="lightbox__fig">
<span class="ph ph--living lightbox__img" data-lb-img aria-hidden="true"></span>
<figcaption class="lightbox__cap" data-lb-cap>Living room · south light</figcaption>
</figure>
<button class="lightbox__nav lightbox__nav--next" data-lb-next aria-label="Next photo">›</button>
</div>
<div class="toast" data-toast role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Listing Detail
A full single-property detail page styled with an editorial real-estate look — serif Cormorant Garamond display type, an ivory canvas, and brass hairline accents. The hero gallery shows a large “photo” alongside a column of thumbnails; clicking any thumbnail swaps the main image and caption, and clicking the main image opens a fullscreen lightbox you can move through with the arrow buttons or your keyboard. Property photos are simulated entirely with warm CSS gradients, so the page is self-contained and needs no network.
Below the gallery, a price-and-address header presents the asking price and a key-facts strip (beds, baths, sq ft, lot, year, price per sq ft), followed by an expandable description, a features grid, a stylized neighborhood map with hover-labeled points of interest and walkability scores, and a list of nearby schools with rating badges. A sticky sidebar holds an interactive monthly-payment estimator — adjust price, down payment, rate, and term to recompute principal, interest, tax, and insurance live — plus an agent card with a request-a-tour form.
All interactions are plain vanilla JavaScript: gallery swap, lightbox navigation, save and share buttons, the read-more toggle, the mortgage math, and a small toast() helper that confirms form submissions and validation. The layout is responsive down to about 360px, where the thumbnail rail becomes a horizontal scroller and the columns stack.
Illustrative UI only — sample listings and data are fictional; not a real real-estate service.