Real Estate — Search Listings
An editorial real-estate search page for the fictional Verdant Estates. A sticky filter bar drives price range, beds, baths, home type and a more-filters panel with status chips; results render as refined gradient-photo listing cards showing price, beds, baths, sqft, address and a status badge. A simulated map panel on the right shows clickable price pins that highlight and scroll to the matching card, while a sort dropdown, live results count and save-search button keep everything synced in memory.
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(31,61,52,.06), 0 2px 8px rgba(31,61,52,.05);
--sh-2:0 4px 14px rgba(31,61,52,.10), 0 14px 34px rgba(31,61,52,.10);
--sh-3:0 10px 30px rgba(31,61,52,.16), 0 24px 60px rgba(31,61,52,.14);
}
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0;
font-family:"Inter",system-ui,sans-serif;
background:var(--ivory);
color:var(--ink);
line-height:1.55;
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
}
h1,h2,h3{font-family:"Cormorant Garamond",Georgia,serif;margin:0;font-weight:600;letter-spacing:.2px}
em{font-style:italic}
a{color:inherit;text-decoration:none}
button{font-family:inherit}
input,select{font-family:inherit}
.skip-link{
position:fixed;left:12px;top:-60px;z-index:200;background:var(--green);color:#fff;
padding:10px 16px;border-radius:var(--r-sm);transition:top .2s;
}
.skip-link:focus{top:12px}
/* ---------- Topbar ---------- */
.topbar{
position:sticky;top:0;z-index:60;background:rgba(255,253,248,.92);
backdrop-filter:saturate(1.2) blur(10px);
border-bottom:1px solid var(--line);
}
.topbar__inner{
max-width:1440px;margin:0 auto;padding:14px 28px;
display:flex;align-items:center;gap:24px;
}
.brand{display:flex;align-items:center;gap:11px;flex:none}
.brand__mark{
width:38px;height:38px;border-radius:11px;display:grid;place-items:center;
font-family:"Cormorant Garamond",serif;font-weight:700;font-size:22px;color:var(--brass-50);
background:linear-gradient(150deg,var(--green-700),var(--green-d));
box-shadow:inset 0 0 0 1px rgba(176,141,87,.45), var(--sh-1);
}
.brand__name{font-family:"Cormorant Garamond",serif;font-size:23px;font-weight:600;color:var(--green)}
.brand__name em{color:var(--brass);font-weight:500}
.searchbox{
flex:1;max-width:420px;display:flex;align-items:center;gap:10px;
background:var(--white);border:1px solid var(--line-2);border-radius:999px;
padding:0 16px;height:44px;color:var(--muted);box-shadow:var(--sh-1);
transition:border-color .15s, box-shadow .15s;
}
.searchbox:focus-within{border-color:var(--brass);box-shadow:0 0 0 3px var(--brass-50)}
.searchbox input{border:0;outline:0;background:transparent;flex:1;font-size:15px;color:var(--ink);min-width:0}
.topnav{display:flex;align-items:center;gap:22px;flex:none}
.topnav a{font-size:14.5px;font-weight:500;color:var(--ink-2);transition:color .15s}
.topnav a:hover{color:var(--green)}
.topnav__agent{
padding:9px 16px;border:1px solid var(--line-2);border-radius:999px;color:var(--green)!important;
}
.topnav__agent:hover{background:var(--green-50)}
/* ---------- Filter bar ---------- */
.filterbar{
position:sticky;top:67px;z-index:55;background:rgba(247,244,236,.94);
backdrop-filter:blur(8px);border-bottom:1px solid var(--line);
}
.filterbar__inner{
max-width:1440px;margin:0 auto;padding:13px 28px;
display:flex;align-items:flex-end;gap:14px;flex-wrap:wrap;
}
.field{display:flex;flex-direction:column;gap:5px}
.field label{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;padding-left:2px}
.control{
appearance:none;-webkit-appearance:none;
height:42px;padding:0 34px 0 14px;border-radius:var(--r-sm);
border:1px solid var(--line-2);background:var(--white);color:var(--ink);
font-size:14.5px;font-weight:500;cursor:pointer;box-shadow:var(--sh-1);
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24'><path fill='%231f3d34' d='M7 10l5 5 5-5z'/></svg>");
background-repeat:no-repeat;background-position:right 11px center;
transition:border-color .15s, box-shadow .15s, background-color .15s;
}
.control:hover{border-color:var(--brass)}
.control:focus-visible{outline:none;border-color:var(--brass);box-shadow:0 0 0 3px var(--brass-50)}
.control--ghost{
display:inline-flex;align-items:center;gap:6px;padding-right:14px;background-image:none;
}
.control--ghost[aria-expanded="true"]{background:var(--green-50);border-color:var(--brass)}
.control--text{
height:42px;background:transparent;border:0;box-shadow:none;color:var(--muted);
padding:0 8px;background-image:none;font-weight:600;
}
.control--text:hover{color:var(--danger);background:transparent}
.btn{
display:inline-flex;align-items:center;gap:8px;border:0;cursor:pointer;
height:42px;padding:0 18px;border-radius:var(--r-sm);font-size:14.5px;font-weight:600;
box-shadow:var(--sh-1);transition:transform .12s, box-shadow .15s, background-color .15s;
}
.btn:active{transform:translateY(1px)}
.btn--brass{background:linear-gradient(160deg,var(--brass),var(--brass-d));color:#fff}
.btn--brass:hover{box-shadow:var(--sh-2)}
.btn--brass.is-saved{background:var(--green);}
.btn--green{background:var(--green);color:var(--brass-50)}
.btn--green:hover{background:var(--green-d)}
.filterbar__save{margin-left:auto}
.morefilters{border-top:1px dashed var(--line-2)}
.morefilters__inner{
max-width:1440px;margin:0 auto;padding:14px 28px 16px;
display:flex;align-items:flex-end;gap:26px;flex-wrap:wrap;
}
.chips{border:0;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.chips legend{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;padding:0;float:left;width:100%;margin-bottom:6px}
.chip{
display:inline-flex;align-items:center;gap:7px;padding:8px 14px;border-radius:999px;
border:1px solid var(--line-2);background:var(--white);font-size:13.5px;font-weight:500;cursor:pointer;
transition:background-color .15s,border-color .15s;
}
.chip:has(input:checked){background:var(--green-50);border-color:var(--green-700);color:var(--green)}
.chip input{accent-color:var(--green);width:15px;height:15px}
/* ---------- Layout ---------- */
.layout{
max-width:1440px;margin:0 auto;padding:22px 28px 60px;
display:grid;grid-template-columns:minmax(0,1fr) 40%;gap:26px;align-items:start;
}
.resultsHead{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;margin-bottom:18px;flex-wrap:wrap}
.resultsHead__title{font-size:31px;line-height:1.1;color:var(--ink)}
.resultsHead__title em{color:var(--brass)}
.resultsHead__count{margin:6px 0 0;color:var(--muted);font-size:14px;font-weight:500}
.resultsHead__count span{color:var(--green);font-weight:700}
.sortWrap{display:flex;align-items:center;gap:9px}
.sortWrap__label{font-size:13px;color:var(--muted);font-weight:600}
.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}
/* ---------- Card ---------- */
.card{
background:var(--paper);border:1px solid var(--line);border-radius:var(--r-lg);
overflow:hidden;box-shadow:var(--sh-1);cursor:pointer;
transition:transform .18s ease, box-shadow .2s ease, border-color .2s ease;
display:flex;flex-direction:column;
}
.card:hover,.card:focus-visible{transform:translateY(-4px);box-shadow:var(--sh-3);border-color:var(--brass)}
.card:focus-visible{outline:none}
.card.is-active{border-color:var(--brass);box-shadow:0 0 0 2px var(--brass), var(--sh-2)}
.card__photo{
position:relative;aspect-ratio:16/11;width:100%;
display:flex;align-items:flex-end;justify-content:space-between;padding:12px;
}
.card__photo[data-img="0"]{background:linear-gradient(160deg,#cdb89a,#9c7e58 55%,#6b5435),radial-gradient(120% 80% at 20% 10%,rgba(255,255,255,.35),transparent 50%)}
.card__photo[data-img="1"]{background:linear-gradient(155deg,#a7b9ab,#5e7d6e 60%,#2f4a3e),radial-gradient(120% 90% at 80% 0%,rgba(255,255,255,.3),transparent 55%)}
.card__photo[data-img="2"]{background:linear-gradient(150deg,#e2cdb0,#c19a6b 50%,#8a6a45),radial-gradient(110% 70% at 30% 100%,rgba(31,61,52,.4),transparent 55%)}
.card__photo[data-img="3"]{background:linear-gradient(160deg,#b9c4d0,#7e91a6 55%,#445265),radial-gradient(120% 80% at 70% 10%,rgba(255,255,255,.32),transparent 55%)}
.card__photo[data-img="4"]{background:linear-gradient(155deg,#d8c7b3,#b59576 50%,#74543a),radial-gradient(120% 90% at 15% 20%,rgba(255,255,255,.34),transparent 50%)}
.card__photo[data-img="5"]{background:linear-gradient(150deg,#aeb6a3,#7c8a6a 55%,#48543a),radial-gradient(110% 80% at 85% 90%,rgba(31,61,52,.42),transparent 55%)}
.card__photo::after{
content:"";position:absolute;inset:0;pointer-events:none;
background:linear-gradient(180deg,rgba(28,42,37,0) 45%,rgba(28,42,37,.28));
}
.card__photo > *{position:relative;z-index:2}
.badge{
display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 11px;border-radius:999px;
font-size:11.5px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;
background:rgba(255,253,248,.95);color:var(--green);box-shadow:var(--sh-1);backdrop-filter:blur(3px);
}
.badge::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--green)}
.badge--new{color:var(--ok)} .badge--new::before{background:var(--ok)}
.badge--open{color:var(--brass-d)} .badge--open::before{background:var(--brass)}
.badge--pending{color:var(--warn)} .badge--pending::before{background:var(--warn)}
.fav{
width:34px;height:34px;border-radius:50%;border:0;cursor:pointer;display:grid;place-items:center;
background:rgba(255,253,248,.92);color:var(--green);box-shadow:var(--sh-1);
transition:transform .14s, color .15s, background-color .15s;
}
.fav:hover{transform:scale(1.08)}
.fav.is-fav{color:var(--danger)}
.fav.is-fav svg path{fill:var(--danger)}
.card__body{padding:14px 16px 16px;display:flex;flex-direction:column;gap:6px}
.card__price{font-family:"Cormorant Garamond",serif;font-size:27px;font-weight:700;color:var(--green);line-height:1}
.card__specs{display:flex;align-items:center;gap:0;color:var(--ink-2);font-size:13.5px;font-weight:500;flex-wrap:wrap}
.card__specs b{color:var(--ink);font-weight:700}
.card__specs span{display:inline-flex;align-items:center;gap:5px}
.card__specs span + span::before{content:"";width:4px;height:4px;border-radius:50%;background:var(--line-2);margin:0 10px}
.card__addr{color:var(--muted);font-size:13.5px;margin-top:2px}
.card__meta{
margin-top:8px;padding-top:11px;border-top:1px solid var(--line);
display:flex;align-items:center;justify-content:space-between;gap:10px;
}
.card__type{font-size:12px;font-weight:600;color:var(--brass-d);text-transform:uppercase;letter-spacing:.05em}
.card__agent{font-size:12.5px;color:var(--muted)}
/* ---------- Empty ---------- */
.empty{
grid-column:1/-1;text-align:center;padding:60px 20px;
background:var(--paper);border:1px dashed var(--line-2);border-radius:var(--r-lg);
}
.empty__art{font-size:48px;color:var(--brass);line-height:1}
.empty h2{font-size:26px;margin:14px 0 6px;color:var(--ink)}
.empty p{color:var(--muted);margin:0 0 18px}
/* ---------- Map ---------- */
.mapCol{position:sticky;top:140px}
.map{
position:relative;height:calc(100vh - 168px);min-height:520px;border-radius:var(--r-lg);
overflow:hidden;border:1px solid var(--line-2);box-shadow:var(--sh-2);
background:linear-gradient(160deg,#eef1ea,#e3e8de);
}
.map__grid{
position:absolute;inset:0;
background-image:linear-gradient(rgba(31,61,52,.06) 1px,transparent 1px),linear-gradient(90deg,rgba(31,61,52,.06) 1px,transparent 1px);
background-size:46px 46px;
}
.map__roads{position:absolute;inset:0}
.map__roads::before,.map__roads::after{content:"";position:absolute;background:#fbfaf4;box-shadow:0 0 0 1px rgba(31,61,52,.07)}
.map__roads::before{left:0;right:0;top:42%;height:16px;transform:rotate(-3deg)}
.map__roads::after{top:0;bottom:0;left:58%;width:14px;transform:rotate(2deg)}
.map__park{
position:absolute;left:6%;top:8%;width:34%;height:30%;border-radius:18px;
background:radial-gradient(120% 120% at 30% 20%,#bcd0b6,#92b292 70%,#6f9a76);
box-shadow:inset 0 0 0 1px rgba(31,61,52,.12);display:flex;align-items:flex-end;padding:10px;
}
.map__park span{font-size:11px;font-weight:600;color:#3a5544;opacity:.85}
.map__water{
position:absolute;right:-4%;bottom:-6%;width:42%;height:36%;border-radius:50%;
background:radial-gradient(circle at 40% 30%,#bcd2da,#8fb3c0 70%,#6f99a8);
box-shadow:inset 0 0 0 1px rgba(31,61,52,.1);
}
.map__badge{
position:absolute;left:14px;bottom:14px;z-index:5;
background:rgba(255,253,248,.95);color:var(--green);font-size:12px;font-weight:700;
padding:7px 13px;border-radius:999px;box-shadow:var(--sh-1);
}
.map__pins{position:absolute;inset:0;z-index:4}
.pin{
position:absolute;transform:translate(-50%,-100%);
display:inline-flex;align-items:center;gap:4px;height:30px;padding:0 11px;border-radius:999px;
background:var(--white);color:var(--green);font-size:13px;font-weight:700;cursor:pointer;
border:1.5px solid var(--green);box-shadow:var(--sh-1);
transition:transform .15s, background-color .15s, color .15s, box-shadow .15s;white-space:nowrap;
}
.pin::after{
content:"";position:absolute;left:50%;bottom:-6px;transform:translateX(-50%) rotate(45deg);
width:9px;height:9px;background:inherit;border-right:1.5px solid var(--green);border-bottom:1.5px solid var(--green);
}
.pin:hover{transform:translate(-50%,-100%) scale(1.06);z-index:8}
.pin.is-active,.pin:focus-visible{
background:var(--green);color:var(--brass-50);border-color:var(--green);outline:none;z-index:9;
box-shadow:0 0 0 4px rgba(176,141,87,.4), var(--sh-2);
}
.pin.is-active::after,.pin:focus-visible::after{background:var(--green);border-color:var(--green)}
/* ---------- Toast ---------- */
.toast{
position:fixed;left:50%;bottom:28px;transform:translate(-50%,18px);
background:var(--green-d);color:#fff;padding:12px 20px;border-radius:999px;
font-size:14px;font-weight:500;box-shadow:var(--sh-3);z-index:120;
opacity:0;pointer-events:none;transition:opacity .25s, transform .25s;
display:flex;align-items:center;gap:9px;max-width:90vw;
}
.toast::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--brass);flex:none}
.toast.is-on{opacity:1;transform:translate(-50%,0)}
/* ---------- Responsive ---------- */
@media (max-width:1040px){
.layout{grid-template-columns:1fr}
.mapCol{position:static;order:-1}
.map{height:340px;min-height:0}
}
@media (max-width:760px){
.topnav{display:none}
.searchbox{max-width:none}
.grid{grid-template-columns:1fr}
}
@media (max-width:520px){
.topbar__inner{padding:12px 16px;gap:12px}
.brand__name{display:none}
.filterbar__inner{padding:11px 16px;gap:10px}
.field{flex:1 1 calc(50% - 5px);min-width:0}
.control{width:100%}
.filterbar__save{margin-left:0;flex:1 1 100%;justify-content:center}
.morefilters__inner{padding:14px 16px;gap:16px}
.layout{padding:16px 16px 48px}
.resultsHead__title{font-size:25px}
.resultsHead{align-items:flex-start}
.map{height:280px}
.card__price{font-size:24px}
}(function () {
"use strict";
/* ---------- Data: fictional Maple Grove, OR listings ---------- */
var LISTINGS = [
{ id: "L1", price: 689000, beds: 3, baths: 2, sqft: 1840, type: "House", status: "New",
addr: "412 Larkspur Lane", agent: "Nora Avery", img: 0, featured: 9, days: 2, x: 22, y: 55 },
{ id: "L2", price: 1245000, beds: 4, baths: 3, sqft: 2960, type: "House", status: "For sale",
addr: "8 Wren Hollow Court", agent: "Desmond Hale", img: 1, featured: 7, days: 11, x: 64, y: 28 },
{ id: "L3", price: 472500, beds: 2, baths: 2, sqft: 1180, type: "Condo", status: "Open house",
addr: "210 Cedar Mill Rd, #4B", agent: "Priya Mehta", img: 3, featured: 6, days: 5, x: 41, y: 70 },
{ id: "L4", price: 815000, beds: 3, baths: 2, sqft: 2110, type: "Townhouse", status: "For sale",
addr: "57 Aspen Ridge Way", agent: "Marcus Boone", img: 2, featured: 5, days: 18, x: 78, y: 60 },
{ id: "L5", price: 1590000, beds: 5, baths: 3, sqft: 3680, type: "House", status: "New",
addr: "1 Heron Bluff Estate", agent: "Eleanor Frost", img: 5, featured: 10, days: 1, x: 30, y: 22 },
{ id: "L6", price: 539000, beds: 2, baths: 1, sqft: 980, type: "Loft", status: "For sale",
addr: "94 Foundry St, Loft 3", agent: "Theo Salas", img: 4, featured: 4, days: 24, x: 53, y: 44 },
{ id: "L7", price: 725000, beds: 3, baths: 2, sqft: 1720, type: "House", status: "Pending",
addr: "330 Willowmere Drive", agent: "Nora Avery", img: 1, featured: 3, days: 32, x: 15, y: 38 },
{ id: "L8", price: 968000, beds: 4, baths: 3, sqft: 2540, type: "House", status: "Open house",
addr: "6 Briarwood Crescent", agent: "Desmond Hale", img: 0, featured: 8, days: 4, x: 70, y: 80 },
{ id: "L9", price: 419000, beds: 1, baths: 1, sqft: 760, type: "Condo", status: "For sale",
addr: "188 Maple Square, #12", agent: "Priya Mehta", img: 3, featured: 2, days: 40, x: 47, y: 18 }
];
var STATUS_CLASS = { "New": "badge--new", "Open house": "badge--open", "Pending": "badge--pending", "For sale": "" };
/* ---------- DOM ---------- */
var grid = document.getElementById("grid");
var pinsWrap = document.getElementById("pins");
var emptyEl = document.getElementById("empty");
var countNum = document.getElementById("countNum");
var toastEl = document.getElementById("toast");
var fPrice = document.getElementById("fPrice");
var fBeds = document.getElementById("fBeds");
var fBaths = document.getElementById("fBaths");
var fType = document.getElementById("fType");
var fSqft = document.getElementById("fSqft");
var sortSel = document.getElementById("sortSel");
var statusBoxes = Array.prototype.slice.call(document.querySelectorAll('input[name="status"]'));
var activeId = null;
var favs = {};
/* ---------- Helpers ---------- */
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("is-on"); }, 2600);
}
function money(n) {
if (n >= 1000000) return "$" + (n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 2).replace(/\.?0+$/, "") + "M";
return "$" + n.toLocaleString("en-US");
}
function fullMoney(n) { return "$" + n.toLocaleString("en-US"); }
/* ---------- Filtering + sorting ---------- */
function getFiltered() {
var priceRange = fPrice.value;
var minBeds = parseInt(fBeds.value, 10);
var minBaths = parseInt(fBaths.value, 10);
var type = fType.value;
var minSqft = parseInt(fSqft.value, 10);
var allowedStatus = statusBoxes.filter(function (b) { return b.checked; }).map(function (b) { return b.value; });
var lo = 0, hi = Infinity;
if (priceRange !== "any") { var p = priceRange.split("-"); lo = +p[0]; hi = +p[1]; }
var out = LISTINGS.filter(function (l) {
if (l.price < lo || l.price > hi) return false;
if (l.beds < minBeds) return false;
if (l.baths < minBaths) return false;
if (type !== "any" && l.type !== type) return false;
if (l.sqft < minSqft) return false;
if (allowedStatus.indexOf(l.status) === -1) return false;
return true;
});
var s = sortSel.value;
out.sort(function (a, b) {
switch (s) {
case "price-asc": return a.price - b.price;
case "price-desc": return b.price - a.price;
case "beds-desc": return b.beds - a.beds || b.price - a.price;
case "sqft-desc": return b.sqft - a.sqft;
case "newest": return a.days - b.days;
default: return b.featured - a.featured;
}
});
return out;
}
/* ---------- Rendering ---------- */
function render() {
var items = getFiltered();
countNum.textContent = items.length;
grid.innerHTML = "";
pinsWrap.innerHTML = "";
if (!items.length) {
emptyEl.hidden = false;
grid.hidden = true;
return;
}
emptyEl.hidden = true;
grid.hidden = false;
items.forEach(function (l) {
grid.appendChild(buildCard(l));
pinsWrap.appendChild(buildPin(l));
});
if (activeId && !items.some(function (l) { return l.id === activeId; })) activeId = null;
syncActive();
}
function buildCard(l) {
var card = document.createElement("article");
card.className = "card";
card.tabIndex = 0;
card.dataset.id = l.id;
card.setAttribute("role", "button");
card.setAttribute("aria-label", money(l.price) + ", " + l.beds + " bed " + l.type + " at " + l.addr);
var badgeClass = STATUS_CLASS[l.status] || "";
var isFav = !!favs[l.id];
card.innerHTML =
'<div class="card__photo" data-img="' + l.img + '">' +
'<span class="badge ' + badgeClass + '">' + l.status + '</span>' +
'<button class="fav' + (isFav ? ' is-fav' : '') + '" type="button" aria-pressed="' + isFav + '" aria-label="Save listing">' +
'<svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true"><path fill="currentColor" d="M12 21l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.18L12 21z"/></svg>' +
'</button>' +
'</div>' +
'<div class="card__body">' +
'<div class="card__price">' + fullMoney(l.price) + '</div>' +
'<div class="card__specs">' +
'<span><b>' + l.beds + '</b> bd</span>' +
'<span><b>' + l.baths + '</b> ba</span>' +
'<span><b>' + l.sqft.toLocaleString("en-US") + '</b> sqft</span>' +
'</div>' +
'<div class="card__addr">' + l.addr + ', Maple Grove, OR</div>' +
'<div class="card__meta">' +
'<span class="card__type">' + l.type + '</span>' +
'<span class="card__agent">Listed by ' + l.agent + '</span>' +
'</div>' +
'</div>';
var fav = card.querySelector(".fav");
fav.addEventListener("click", function (e) {
e.stopPropagation();
toggleFav(l, fav);
});
card.addEventListener("click", function () { setActive(l.id, true); });
card.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setActive(l.id, true); }
});
card.addEventListener("mouseenter", function () { highlightPin(l.id, true); });
card.addEventListener("mouseleave", function () { if (activeId !== l.id) highlightPin(l.id, false); });
return card;
}
function buildPin(l) {
var pin = document.createElement("button");
pin.type = "button";
pin.className = "pin";
pin.dataset.id = l.id;
pin.style.left = l.x + "%";
pin.style.top = l.y + "%";
pin.textContent = money(l.price);
pin.setAttribute("aria-label", money(l.price) + " at " + l.addr);
pin.addEventListener("click", function () { setActive(l.id, false); });
pin.addEventListener("mouseenter", function () { highlightCard(l.id, true); });
pin.addEventListener("mouseleave", function () { if (activeId !== l.id) highlightCard(l.id, false); });
return pin;
}
/* ---------- Favorites ---------- */
function toggleFav(l, btn) {
favs[l.id] = !favs[l.id];
btn.classList.toggle("is-fav", favs[l.id]);
btn.setAttribute("aria-pressed", String(favs[l.id]));
toast(favs[l.id] ? "Saved " + l.addr + " to favorites" : "Removed from favorites");
}
/* ---------- Active sync (cards <-> pins) ---------- */
function setActive(id, fromCard) {
activeId = (activeId === id) ? null : id;
syncActive();
if (activeId) {
var sel = activeId;
if (fromCard) {
var pin = pinsWrap.querySelector('.pin[data-id="' + sel + '"]');
if (pin) pin.focus({ preventScroll: true });
} else {
var card = grid.querySelector('.card[data-id="' + sel + '"]');
if (card) card.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
}
function syncActive() {
Array.prototype.forEach.call(grid.querySelectorAll(".card"), function (c) {
c.classList.toggle("is-active", c.dataset.id === activeId);
});
Array.prototype.forEach.call(pinsWrap.querySelectorAll(".pin"), function (p) {
p.classList.toggle("is-active", p.dataset.id === activeId);
});
}
function highlightCard(id, on) {
var c = grid.querySelector('.card[data-id="' + id + '"]');
if (c) c.classList.toggle("is-active", on || activeId === id);
}
function highlightPin(id, on) {
var p = pinsWrap.querySelector('.pin[data-id="' + id + '"]');
if (p) p.classList.toggle("is-active", on || activeId === id);
}
/* ---------- More filters toggle ---------- */
var moreBtn = document.getElementById("moreFiltersBtn");
var morePanel = document.getElementById("moreFilters");
moreBtn.addEventListener("click", function () {
var open = morePanel.hasAttribute("hidden");
if (open) morePanel.removeAttribute("hidden"); else morePanel.setAttribute("hidden", "");
moreBtn.setAttribute("aria-expanded", String(open));
});
/* ---------- Save search ---------- */
var saveBtn = document.getElementById("saveSearchBtn");
var searchSaved = false;
saveBtn.addEventListener("click", function () {
searchSaved = !searchSaved;
saveBtn.classList.toggle("is-saved", searchSaved);
saveBtn.lastChild.textContent = searchSaved ? " Search saved" : " Save search";
var n = getFiltered().length;
toast(searchSaved ? "Search saved — we'll alert you when new homes match (" + n + " now)" : "Saved search removed");
});
/* ---------- Reset ---------- */
document.getElementById("resetBtn").addEventListener("click", function () {
fPrice.value = "any"; fBeds.value = "0"; fBaths.value = "0";
fType.value = "any"; fSqft.value = "0"; sortSel.value = "featured";
statusBoxes.forEach(function (b) { b.checked = true; });
activeId = null;
render();
toast("Filters reset");
});
/* ---------- Wire up live filtering ---------- */
[fPrice, fBeds, fBaths, fType, fSqft, sortSel].forEach(function (el) {
el.addEventListener("change", render);
});
statusBoxes.forEach(function (b) { b.addEventListener("change", render); });
/* ---------- Init ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Verdant · Search Listings</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>
<a class="skip-link" href="#results">Skip to results</a>
<header class="topbar">
<div class="topbar__inner">
<a class="brand" href="#" aria-label="Verdant Estates home">
<span class="brand__mark" aria-hidden="true">V</span>
<span class="brand__name">Verdant<em>Estates</em></span>
</a>
<div class="searchbox" role="search">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" focusable="false"><path fill="currentColor" d="M10 2a8 8 0 015.29 13.95l4.38 4.38-1.42 1.42-4.38-4.38A8 8 0 1110 2zm0 2a6 6 0 100 12 6 6 0 000-12z"/></svg>
<input id="locInput" type="search" value="Maple Grove, OR" aria-label="Search location" />
</div>
<nav class="topnav" aria-label="Primary">
<a href="#">Buy</a>
<a href="#">Rent</a>
<a href="#">Sell</a>
<a class="topnav__agent" href="#">Find an agent</a>
</nav>
</div>
</header>
<section class="filterbar" aria-label="Filters">
<div class="filterbar__inner">
<div class="field">
<label for="fPrice">Price</label>
<select id="fPrice" class="control">
<option value="any">Any price</option>
<option value="0-500000">Up to $500k</option>
<option value="500000-750000">$500k – $750k</option>
<option value="750000-1000000">$750k – $1M</option>
<option value="1000000-1500000">$1M – $1.5M</option>
<option value="1500000-99999999">$1.5M+</option>
</select>
</div>
<div class="field">
<label for="fBeds">Beds</label>
<select id="fBeds" class="control">
<option value="0">Any</option>
<option value="1">1+</option>
<option value="2">2+</option>
<option value="3">3+</option>
<option value="4">4+</option>
<option value="5">5+</option>
</select>
</div>
<div class="field">
<label for="fBaths">Baths</label>
<select id="fBaths" class="control">
<option value="0">Any</option>
<option value="1">1+</option>
<option value="2">2+</option>
<option value="3">3+</option>
</select>
</div>
<div class="field">
<label for="fType">Home type</label>
<select id="fType" class="control">
<option value="any">All types</option>
<option value="House">House</option>
<option value="Condo">Condo</option>
<option value="Townhouse">Townhouse</option>
<option value="Loft">Loft</option>
</select>
</div>
<button id="moreFiltersBtn" class="control control--ghost" aria-expanded="false" aria-controls="moreFilters">
More filters
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M7 10l5 5 5-5z"/></svg>
</button>
<button id="resetBtn" class="control control--text" type="button">Reset</button>
<button id="saveSearchBtn" class="btn btn--brass filterbar__save" type="button">
<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path fill="currentColor" d="M12 21l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.18L12 21z"/></svg>
Save search
</button>
</div>
<div id="moreFilters" class="morefilters" hidden>
<div class="morefilters__inner">
<div class="field">
<label for="fSqft">Min sqft</label>
<select id="fSqft" class="control">
<option value="0">Any</option>
<option value="800">800+</option>
<option value="1200">1,200+</option>
<option value="1800">1,800+</option>
<option value="2500">2,500+</option>
</select>
</div>
<fieldset class="chips" aria-label="Status">
<legend>Status</legend>
<label class="chip"><input type="checkbox" name="status" value="For sale" checked /> For sale</label>
<label class="chip"><input type="checkbox" name="status" value="New" checked /> New</label>
<label class="chip"><input type="checkbox" name="status" value="Open house" checked /> Open house</label>
<label class="chip"><input type="checkbox" name="status" value="Pending" checked /> Pending</label>
</fieldset>
</div>
</div>
</section>
<main class="layout">
<section class="resultsCol" id="results" aria-label="Search results">
<div class="resultsHead">
<div>
<h1 class="resultsHead__title">Homes for sale in <em>Maple Grove</em></h1>
<p class="resultsHead__count"><span id="countNum">0</span> listings</p>
</div>
<div class="sortWrap">
<label for="sortSel" class="sortWrap__label">Sort</label>
<select id="sortSel" class="control">
<option value="featured">Featured</option>
<option value="price-asc">Price: low to high</option>
<option value="price-desc">Price: high to low</option>
<option value="beds-desc">Most beds</option>
<option value="sqft-desc">Largest</option>
<option value="newest">Newest</option>
</select>
</div>
</div>
<div id="grid" class="grid" aria-live="polite"></div>
<div id="empty" class="empty" hidden>
<div class="empty__art" aria-hidden="true">⌂</div>
<h2>No listings match those filters</h2>
<p>Try widening your price range or clearing a filter.</p>
<button class="btn btn--green" type="button" onclick="document.getElementById('resetBtn').click()">Reset filters</button>
</div>
</section>
<aside class="mapCol" aria-label="Map of listings">
<div class="map" id="map">
<div class="map__grid" aria-hidden="true"></div>
<div class="map__roads" aria-hidden="true"></div>
<div class="map__park" aria-hidden="true"><span>Maple Grove Park</span></div>
<div class="map__water" aria-hidden="true"></div>
<div class="map__pins" id="pins"></div>
<div class="map__badge" aria-hidden="true">Maple Grove, OR</div>
</div>
</aside>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Search Listings
A full listing-search experience for Verdant Estates in fictional Maple Grove, OR. A sticky filter bar lets you narrow by price range, minimum beds and baths, and home type, with an expandable “more filters” panel adding a minimum-square-footage control and status chips (For sale, New, Open house, Pending). Every change re-filters and re-sorts the in-memory listing set live, and the results count updates instantly.
Results render as editorial cards: a CSS-gradient “photo” that evokes real architectural imagery, a status badge, a heart to favorite the home, the price in a serif display face, beds/baths/sqft, the street address, the home type, and the listing agent. A sort dropdown orders by featured, price, beds, size, or newest. Hovering or clicking a card highlights its counterpart on the map and vice-versa.
The right-hand panel is a simulated street map with parks, roads, and water drawn entirely in CSS. Each visible listing places a clickable price pin; selecting a pin highlights and smoothly scrolls to the matching card, and selecting a card focuses its pin — keeping the two views in sync. A save-search button and a toast helper round out the micro-interactions, and the layout collapses gracefully to a single column with the map on top down to ~360px.
Illustrative UI only — sample listings and data are fictional; not a real real-estate service.