Real Estate — Team / Agent Roster
An editorial brokerage admin for managing a sales team — a responsive roster of agent cards showing avatar, role, team, active listings, YTD volume and active or inactive status. Search by name, role or region, filter with team chips and a status segmented control, fire per-row profile, message and overflow actions, and invite a new agent through an accessible modal that validates input and adds a pending desk to the roster with a confirmation toast.
MCP
Codice
: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 1px 3px rgba(28, 42, 37, 0.06);
--sh-2: 0 6px 18px rgba(28, 42, 37, 0.08), 0 2px 6px rgba(28, 42, 37, 0.05);
--sh-3: 0 24px 60px rgba(22, 48, 42, 0.22), 0 6px 18px rgba(22, 48, 42, 0.14);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.55;
color: var(--ink);
background:
radial-gradient(1200px 480px at 78% -10%, rgba(176, 141, 87, 0.08), transparent 60%),
var(--ivory);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 {
font-family: "Cormorant Garamond", Georgia, serif;
margin: 0;
}
.shell { max-width: 1180px; margin: 0 auto; padding: 0 22px 64px; }
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 20px;
padding: 18px 0;
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
background:
linear-gradient(var(--ivory), var(--ivory)) padding-box;
backdrop-filter: saturate(1.1);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand__mark {
width: 44px; height: 44px;
display: grid; place-items: center;
border-radius: 12px;
background: linear-gradient(150deg, var(--green-700), var(--green-d));
color: var(--brass-50);
font-family: "Cormorant Garamond", serif;
font-weight: 700;
font-size: 16px;
letter-spacing: 0.5px;
box-shadow: inset 0 0 0 1px rgba(176, 141, 87, 0.35), var(--sh-1);
}
.brand__txt { display: flex; flex-direction: column; line-height: 1.15; }
.brand__name { font-family: "Cormorant Garamond", serif; font-weight: 600; font-size: 19px; color: var(--green); }
.brand__sub { font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--brass-d); }
.topnav { display: flex; gap: 6px; margin-left: 14px; }
.topnav__link {
text-decoration: none;
color: var(--ink-2);
font-size: 14px; font-weight: 500;
padding: 8px 12px;
border-radius: var(--r-sm);
transition: background 0.18s, color 0.18s;
}
.topnav__link:hover { background: var(--green-50); color: var(--green); }
.topnav__link.is-active {
color: var(--green);
background: var(--green-50);
box-shadow: inset 0 -2px 0 var(--brass);
}
/* ---------- Buttons ---------- */
.btn {
font-family: inherit;
font-size: 14px; font-weight: 600;
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 10px 16px;
cursor: pointer;
display: inline-flex; align-items: center; gap: 8px;
transition: transform 0.12s, box-shadow 0.18s, background 0.18s, border-color 0.18s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
.btn--primary {
margin-left: auto;
color: var(--paper);
background: linear-gradient(150deg, var(--green-700), var(--green-d));
box-shadow: var(--sh-2), inset 0 0 0 1px rgba(176, 141, 87, 0.28);
}
.btn--primary:hover { box-shadow: var(--sh-3), inset 0 0 0 1px rgba(176, 141, 87, 0.45); }
.btn__plus { font-size: 17px; line-height: 1; color: var(--brass-50); }
.btn--ghost {
background: var(--white);
color: var(--ink-2);
border-color: var(--line-2);
}
.btn--ghost:hover { background: var(--green-50); border-color: var(--green-700); color: var(--green); }
/* ---------- Head ---------- */
.page { padding-top: 30px; }
.head {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 28px;
flex-wrap: wrap;
}
.eyebrow {
margin: 0 0 6px;
font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
color: var(--brass-d); font-weight: 600;
}
.display { font-size: clamp(38px, 6vw, 56px); font-weight: 600; line-height: 1.02; color: var(--green); letter-spacing: -0.01em; }
.lede { margin: 10px 0 0; max-width: 46ch; color: var(--muted); font-size: 15px; }
.stats { display: flex; gap: 14px; margin: 0; }
.stat {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 18px;
min-width: 116px;
box-shadow: var(--sh-1);
}
.stat dt { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.stat dd { margin: 4px 0 0; font-family: "Cormorant Garamond", serif; font-weight: 700; font-size: 26px; color: var(--green); }
/* ---------- Toolbar ---------- */
.toolbar {
margin: 30px 0 22px;
display: flex;
gap: 14px;
align-items: center;
flex-wrap: wrap;
padding-bottom: 20px;
border-bottom: 1px solid var(--line);
}
.search {
position: relative;
flex: 1 1 260px;
min-width: 220px;
}
.search__icon {
position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
width: 18px; height: 18px; color: var(--muted); pointer-events: none;
}
.search input {
width: 100%;
font-family: inherit; font-size: 14px;
padding: 11px 14px 11px 40px;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background: var(--white);
color: var(--ink);
transition: border-color 0.18s, box-shadow 0.18s;
}
.search input::placeholder { color: var(--muted); }
.search input:focus { outline: none; border-color: var(--green-700); box-shadow: 0 0 0 3px rgba(38, 73, 62, 0.12); }
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
font-family: inherit; font-size: 13px; font-weight: 500;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid var(--line-2);
background: var(--white);
color: var(--ink-2);
cursor: pointer;
transition: all 0.16s;
}
.chip:hover { border-color: var(--brass); color: var(--brass-d); }
.chip.is-active {
background: var(--green);
color: var(--brass-50);
border-color: var(--green);
box-shadow: var(--sh-1);
}
.chip:focus-visible, .seg:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
.status-toggle {
display: inline-flex;
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 3px;
background: var(--white);
}
.seg {
font-family: inherit; font-size: 13px; font-weight: 500;
border: none; background: transparent; cursor: pointer;
padding: 6px 14px; border-radius: 999px; color: var(--ink-2);
transition: all 0.16s;
}
.seg.is-active { background: var(--brass-50); color: var(--brass-d); box-shadow: inset 0 0 0 1px rgba(176, 141, 87, 0.4); }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(310px, 1fr));
gap: 20px;
}
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--sh-1);
display: flex;
flex-direction: column;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
animation: rise 0.4s ease both;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--sh-3); border-color: var(--line-2); }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.card__cover {
position: relative;
aspect-ratio: 16 / 7;
background-size: cover;
}
.card__cover::after {
content: "";
position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(22, 48, 42, 0.05), rgba(22, 48, 42, 0.35));
}
.cover-label {
position: absolute; top: 12px; left: 12px; z-index: 2;
font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--paper); font-weight: 600;
background: rgba(22, 48, 42, 0.42);
border: 1px solid rgba(243, 234, 217, 0.4);
padding: 4px 9px; border-radius: 999px;
backdrop-filter: blur(2px);
}
.card__avatar {
position: absolute;
bottom: -28px; left: 22px; z-index: 3;
width: 64px; height: 64px;
border-radius: 16px;
display: grid; place-items: center;
font-family: "Cormorant Garamond", serif;
font-weight: 700; font-size: 24px;
color: var(--paper);
border: 3px solid var(--paper);
box-shadow: var(--sh-2);
}
.card__body { padding: 38px 22px 18px; }
.card__name { font-size: 23px; font-weight: 600; color: var(--green); line-height: 1.05; }
.card__role { margin: 2px 0 0; font-size: 13px; color: var(--muted); }
.card__role .dot { color: var(--brass); margin: 0 6px; }
.card__badges { display: flex; gap: 7px; flex-wrap: wrap; margin: 14px 0 16px; }
.badge {
font-size: 11px; font-weight: 600; letter-spacing: 0.04em;
padding: 4px 9px; border-radius: 999px;
display: inline-flex; align-items: center; gap: 5px;
}
.badge--team { background: var(--green-50); color: var(--green-700); }
.badge--status { gap: 6px; }
.badge--status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.badge--active { background: rgba(47, 158, 111, 0.12); color: var(--ok); }
.badge--inactive { background: rgba(107, 122, 114, 0.14); color: var(--muted); }
.badge--pending { background: rgba(201, 138, 43, 0.14); color: var(--warn); }
.metrics {
display: grid; grid-template-columns: 1fr 1fr;
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.metric { background: var(--paper); padding: 11px 14px; }
.metric__k { font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
.metric__v { font-family: "Cormorant Garamond", serif; font-weight: 700; font-size: 21px; color: var(--ink); }
.card__foot {
margin-top: auto;
display: flex; gap: 8px;
padding: 14px 22px;
border-top: 1px solid var(--line);
background: linear-gradient(var(--white), var(--paper));
}
.act {
flex: 1;
font-family: inherit; font-size: 13px; font-weight: 500;
padding: 8px 10px;
border-radius: var(--r-sm);
border: 1px solid var(--line-2);
background: var(--white);
color: var(--ink-2);
cursor: pointer;
transition: all 0.15s;
}
.act:hover { background: var(--green-50); border-color: var(--green-700); color: var(--green); }
.act--icon { flex: 0 0 auto; min-width: 40px; font-size: 15px; }
.act:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
/* ---------- Empty ---------- */
.empty { text-align: center; color: var(--muted); padding: 48px 0; font-size: 15px; }
.linkbtn { background: none; border: none; color: var(--green-700); font-weight: 600; cursor: pointer; text-decoration: underline; font: inherit; }
/* ---------- Modal ---------- */
.modal { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 20px; }
.modal[hidden] { display: none; }
.modal__backdrop { position: absolute; inset: 0; background: rgba(22, 34, 30, 0.5); backdrop-filter: blur(3px); animation: fade 0.2s ease; }
.modal__card {
position: relative;
width: min(520px, 100%);
background: var(--paper);
border-radius: var(--r-lg);
border: 1px solid var(--line-2);
box-shadow: var(--sh-3);
padding: 26px 26px 24px;
animation: pop 0.24s cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
}
@keyframes fade { from { opacity: 0; } }
@keyframes pop { from { opacity: 0; transform: translateY(14px) scale(0.97); } }
.modal__bar { display: flex; justify-content: space-between; align-items: flex-start; }
.modal__title { font-size: 30px; font-weight: 600; color: var(--green); }
.modal__x { border: none; background: none; font-size: 26px; line-height: 1; color: var(--muted); cursor: pointer; padding: 0 4px; border-radius: 6px; }
.modal__x:hover { color: var(--danger); }
.modal__lede { margin: 4px 0 18px; color: var(--muted); font-size: 14px; }
.form { display: flex; flex-direction: column; gap: 14px; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: 12px; font-weight: 600; letter-spacing: 0.04em; color: var(--ink-2); text-transform: uppercase; }
.field input, .field select {
font-family: inherit; font-size: 14px;
padding: 10px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--white);
color: var(--ink);
}
.field input:focus, .field select:focus { outline: none; border-color: var(--green-700); box-shadow: 0 0 0 3px rgba(38, 73, 62, 0.12); }
.field input.invalid { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(196, 80, 62, 0.12); }
.row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.modal__actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; }
/* ---------- Toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(12px);
background: var(--green-d);
color: var(--brass-50);
padding: 12px 18px;
border-radius: var(--r-md);
font-size: 14px; font-weight: 500;
box-shadow: var(--sh-3), inset 0 0 0 1px rgba(176, 141, 87, 0.35);
z-index: 80;
opacity: 0;
transition: opacity 0.25s, transform 0.25s;
pointer-events: none;
max-width: calc(100% - 40px);
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.topnav { display: none; }
.btn--primary { margin-left: auto; }
}
@media (max-width: 520px) {
.shell { padding: 0 14px 48px; }
.topbar { gap: 12px; }
.brand__sub { display: none; }
.head { align-items: stretch; }
.stats { width: 100%; }
.stat { flex: 1; min-width: 0; padding: 12px; }
.stat dd { font-size: 22px; }
.toolbar { gap: 12px; }
.grid { grid-template-columns: 1fr; }
.row2 { grid-template-columns: 1fr; }
.status-toggle, .chips { width: 100%; }
.chips { justify-content: flex-start; }
.modal__card { padding: 22px 18px; }
}(function () {
"use strict";
/* ---------- Data (fictional) ---------- */
var TEAM_LABELS = {
luxury: "Luxury",
residential: "Residential",
commercial: "Commercial",
rentals: "Rentals"
};
// Warm, architectural "photography" simulated with gradients.
var COVERS = [
"linear-gradient(135deg, #2c4a3e 0%, #4a6b58 45%, #b08d57 110%)",
"linear-gradient(135deg, #7a5a36 0%, #b08d57 50%, #e8d3a8 105%)",
"linear-gradient(120deg, #1f3d34 0%, #2f5a4a 55%, #8fae9a 110%)",
"linear-gradient(140deg, #5b4327 0%, #94733f 50%, #d8b483 110%)",
"linear-gradient(130deg, #26493e 0%, #6b8f7c 60%, #c8a36a 115%)",
"linear-gradient(135deg, #3a3a2e 0%, #6b6a4a 50%, #b08d57 110%)",
"linear-gradient(125deg, #1c3a31 0%, #486b5c 55%, #a8c0ad 110%)",
"linear-gradient(140deg, #6a4a2c 0%, #a3814d 50%, #ead2a8 110%)"
];
var AGENTS = [
{ name: "Eleanor Whitfield", role: "Senior Broker", team: "luxury", listings: 14, volume: 48.2, status: "active", region: "Marin Heights" },
{ name: "Diego Salazar", role: "Listing Agent", team: "residential", listings: 11, volume: 22.9, status: "active", region: "Cedar Park" },
{ name: "Priya Ramanathan", role: "Team Lead", team: "commercial", listings: 9, volume: 61.4, status: "active", region: "Harbor District" },
{ name: "Marcus Bélanger", role: "Sales Associate", team: "rentals", listings: 18, volume: 7.3, status: "active", region: "Old Mill Quarter" },
{ name: "Sofia Castellano", role: "Listing Agent", team: "luxury", listings: 8, volume: 39.6, status: "active", region: "Vale Ridge" },
{ name: "Theo Nakamura", role: "Sales Associate", team: "residential", listings: 13, volume: 18.1, status: "inactive", region: "Birchwood" },
{ name: "Amara Okafor", role: "Senior Broker", team: "commercial", listings: 6, volume: 14.7, status: "active", region: "Foundry Row" },
{ name: "Lucas Hartmann", role: "Listing Agent", team: "rentals", listings: 7, volume: 2.4, status: "inactive", region: "Greenfield" }
];
var BRASS_TONES = ["#b08d57", "#26493e", "#94733f", "#2f5a4a", "#6b8f7c", "#5b4327", "#1f3d34", "#a3814d"];
/* ---------- State ---------- */
var state = { search: "", team: "all", status: "all" };
/* ---------- Helpers ---------- */
var $ = function (sel, ctx) { return (ctx || document).querySelector(sel); };
var $$ = function (sel, ctx) { return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); };
function initials(name) {
return name.split(/\s+/).map(function (p) { return p[0]; }).join("").slice(0, 2).toUpperCase();
}
function money(m) {
return "$" + (m >= 1 ? m.toFixed(1) + "M" : (m * 1000).toFixed(0) + "K");
}
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.hidden = false;
requestAnimationFrame(function () { toastEl.classList.add("show"); });
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
setTimeout(function () { toastEl.hidden = true; }, 280);
}, 2600);
}
/* ---------- Render ---------- */
var roster = $("#roster");
var emptyEl = $("#empty");
function filtered() {
var q = state.search.trim().toLowerCase();
return AGENTS.filter(function (a) {
if (state.team !== "all" && a.team !== state.team) return false;
if (state.status !== "all" && a.status !== state.status) return false;
if (q) {
var hay = (a.name + " " + a.role + " " + TEAM_LABELS[a.team] + " " + a.region + " " + a.status).toLowerCase();
if (hay.indexOf(q) === -1) return false;
}
return true;
});
}
function statusBadge(s) {
var label = s === "active" ? "Active" : s === "pending" ? "Pending" : "Inactive";
return '<span class="badge badge--status badge--' + s + '">' + label + "</span>";
}
function cardHTML(a, i) {
var tone = BRASS_TONES[i % BRASS_TONES.length];
var cover = COVERS[i % COVERS.length];
return (
'<article class="card" data-name="' + esc(a.name) + '">' +
'<div class="card__cover" style="background-image:' + cover + '">' +
'<span class="cover-label">' + esc(a.region) + "</span>" +
'<div class="card__avatar" style="background:linear-gradient(150deg,' + tone + ',rgba(0,0,0,.35))">' + initials(a.name) + "</div>" +
"</div>" +
'<div class="card__body">' +
'<h3 class="card__name">' + esc(a.name) + "</h3>" +
'<p class="card__role">' + esc(a.role) + '<span class="dot">•</span>' + TEAM_LABELS[a.team] + "</p>" +
'<div class="card__badges">' +
'<span class="badge badge--team">' + TEAM_LABELS[a.team] + "</span>" +
statusBadge(a.status) +
"</div>" +
'<div class="metrics">' +
'<div class="metric"><div class="metric__k">Active listings</div><div class="metric__v">' + a.listings + "</div></div>" +
'<div class="metric"><div class="metric__k">YTD volume</div><div class="metric__v">' + money(a.volume) + "</div></div>" +
"</div>" +
"</div>" +
'<div class="card__foot">' +
'<button class="act" data-act="profile">View profile</button>' +
'<button class="act" data-act="message">Message</button>' +
'<button class="act act--icon" data-act="more" aria-label="More actions">⋯</button>' +
"</div>" +
"</article>"
);
}
function render() {
var list = filtered();
if (!list.length) {
roster.innerHTML = "";
emptyEl.hidden = false;
} else {
emptyEl.hidden = true;
roster.innerHTML = list.map(cardHTML).join("");
}
updateStats();
}
function updateStats() {
var live = AGENTS.filter(function (a) { return a.status === "active"; });
var listings = AGENTS.reduce(function (s, a) { return s + a.listings; }, 0);
var vol = AGENTS.reduce(function (s, a) { return s + a.volume; }, 0);
$("#statAgents").textContent = AGENTS.length;
$("#statListings").textContent = listings;
$("#statVolume").textContent = "$" + vol.toFixed(1) + "M";
void live;
}
/* ---------- Filter wiring ---------- */
$("#search").addEventListener("input", function (e) {
state.search = e.target.value;
render();
});
$$(".chip").forEach(function (chip) {
chip.addEventListener("click", function () {
$$(".chip").forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
state.team = chip.dataset.team;
render();
});
});
$$(".seg").forEach(function (seg) {
seg.addEventListener("click", function () {
$$(".seg").forEach(function (s) {
s.classList.remove("is-active");
s.setAttribute("aria-pressed", "false");
});
seg.classList.add("is-active");
seg.setAttribute("aria-pressed", "true");
state.status = seg.dataset.status;
render();
});
});
$("#clearAll").addEventListener("click", function () {
state.search = "";
state.team = "all";
state.status = "all";
$("#search").value = "";
$$(".chip").forEach(function (c) {
var on = c.dataset.team === "all";
c.classList.toggle("is-active", on);
c.setAttribute("aria-pressed", String(on));
});
$$(".seg").forEach(function (s) {
var on = s.dataset.status === "all";
s.classList.toggle("is-active", on);
s.setAttribute("aria-pressed", String(on));
});
render();
});
/* ---------- Per-row actions (delegated) ---------- */
roster.addEventListener("click", function (e) {
var btn = e.target.closest(".act");
if (!btn) return;
var card = e.target.closest(".card");
var name = card ? card.dataset.name : "Agent";
var act = btn.dataset.act;
if (act === "profile") toast("Opening profile — " + name);
else if (act === "message") toast("Message draft started for " + name);
else if (act === "more") toast("Reassign · Deactivate · Export — " + name);
});
/* ---------- Modal ---------- */
var modal = $("#inviteModal");
var lastFocus = null;
function openModal() {
lastFocus = document.activeElement;
modal.hidden = false;
document.body.style.overflow = "hidden";
setTimeout(function () { $("#fName").focus(); }, 30);
}
function closeModal() {
modal.hidden = true;
document.body.style.overflow = "";
$("#inviteForm").reset();
$$(".invalid", modal).forEach(function (el) { el.classList.remove("invalid"); });
if (lastFocus) lastFocus.focus();
}
$("#inviteOpen").addEventListener("click", openModal);
$$("[data-close]", modal).forEach(function (el) { el.addEventListener("click", closeModal); });
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !modal.hidden) closeModal();
});
$("#inviteForm").addEventListener("submit", function (e) {
e.preventDefault();
var nameEl = $("#fName");
var emailEl = $("#fEmail");
var ok = true;
[nameEl, emailEl].forEach(function (el) {
var bad = !el.value.trim() || (el.type === "email" && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(el.value));
el.classList.toggle("invalid", bad);
if (bad) ok = false;
});
if (!ok) { toast("Please complete the highlighted fields."); return; }
var name = nameEl.value.trim();
var team = $("#fTeam").value;
AGENTS.unshift({
name: name,
role: $("#fRole").value,
team: team,
listings: 0,
volume: 0,
status: "pending",
region: "Awaiting desk"
});
closeModal();
state.search = ""; state.team = "all"; state.status = "all";
$("#search").value = "";
$$(".chip").forEach(function (c) {
var on = c.dataset.team === "all";
c.classList.toggle("is-active", on);
c.setAttribute("aria-pressed", String(on));
});
$$(".seg").forEach(function (s) {
var on = s.dataset.status === "all";
s.classList.toggle("is-active", on);
s.setAttribute("aria-pressed", String(on));
});
render();
toast("Invitation sent to " + name + " · " + TEAM_LABELS[team]);
});
/* ---------- Boot ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Verdant & Vale — Agent Roster</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="shell">
<!-- Header -->
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true">V&V</span>
<div class="brand__txt">
<span class="brand__name">Verdant & Vale</span>
<span class="brand__sub">Estate & Brokerage</span>
</div>
</div>
<nav class="topnav" aria-label="Primary">
<a href="#" class="topnav__link">Listings</a>
<a href="#" class="topnav__link is-active" aria-current="page">Team</a>
<a href="#" class="topnav__link">Deals</a>
<a href="#" class="topnav__link">Reports</a>
</nav>
<button class="btn btn--primary" id="inviteOpen" type="button">
<span class="btn__plus" aria-hidden="true">+</span> Invite agent
</button>
</header>
<main class="page">
<!-- Title block -->
<section class="head">
<div>
<p class="eyebrow">Brokerage administration</p>
<h1 class="display">Agent Roster</h1>
<p class="lede">Manage your sales team, monitor live inventory and track year-to-date production across every desk.</p>
</div>
<dl class="stats" aria-label="Team summary">
<div class="stat">
<dt>Agents</dt><dd id="statAgents">8</dd>
</div>
<div class="stat">
<dt>Active listings</dt><dd id="statListings">86</dd>
</div>
<div class="stat">
<dt>YTD volume</dt><dd id="statVolume">$214.6M</dd>
</div>
</dl>
</section>
<!-- Toolbar -->
<section class="toolbar">
<div class="search">
<svg class="search__icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<input id="search" type="search" placeholder="Search agents, roles, regions…" aria-label="Search agents" autocomplete="off" />
</div>
<div class="chips" role="group" aria-label="Filter by team">
<button class="chip is-active" data-team="all" type="button" aria-pressed="true">All teams</button>
<button class="chip" data-team="luxury" type="button" aria-pressed="false">Luxury</button>
<button class="chip" data-team="residential" type="button" aria-pressed="false">Residential</button>
<button class="chip" data-team="commercial" type="button" aria-pressed="false">Commercial</button>
<button class="chip" data-team="rentals" type="button" aria-pressed="false">Rentals</button>
</div>
<div class="status-toggle" role="group" aria-label="Filter by status">
<button class="seg is-active" data-status="all" type="button" aria-pressed="true">All</button>
<button class="seg" data-status="active" type="button" aria-pressed="false">Active</button>
<button class="seg" data-status="inactive" type="button" aria-pressed="false">Inactive</button>
</div>
</section>
<!-- Grid -->
<section class="grid" id="roster" aria-live="polite"></section>
<p class="empty" id="empty" hidden>No agents match your filters. <button type="button" id="clearAll" class="linkbtn">Clear filters</button></p>
</main>
</div>
<!-- Invite modal -->
<div class="modal" id="inviteModal" hidden>
<div class="modal__backdrop" data-close></div>
<div class="modal__card" role="dialog" aria-modal="true" aria-labelledby="inviteTitle">
<div class="modal__bar">
<h2 id="inviteTitle" class="modal__title">Invite a new agent</h2>
<button class="modal__x" type="button" data-close aria-label="Close dialog">×</button>
</div>
<p class="modal__lede">Send a desk invitation. They’ll receive onboarding access and appear as <em>pending</em> until they accept.</p>
<form id="inviteForm" class="form" novalidate>
<div class="field">
<label for="fName">Full name</label>
<input id="fName" name="name" type="text" placeholder="e.g. Marisol Quintero" required />
</div>
<div class="field">
<label for="fEmail">Work email</label>
<input id="fEmail" name="email" type="email" placeholder="[email protected]" required />
</div>
<div class="row2">
<div class="field">
<label for="fTeam">Team</label>
<select id="fTeam" name="team">
<option value="luxury">Luxury</option>
<option value="residential" selected>Residential</option>
<option value="commercial">Commercial</option>
<option value="rentals">Rentals</option>
</select>
</div>
<div class="field">
<label for="fRole">Role</label>
<select id="fRole" name="role">
<option>Sales Associate</option>
<option selected>Listing Agent</option>
<option>Senior Broker</option>
<option>Team Lead</option>
</select>
</div>
</div>
<div class="modal__actions">
<button class="btn btn--ghost" type="button" data-close>Cancel</button>
<button class="btn btn--primary" type="submit">Send invitation</button>
</div>
</form>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>Team / Agent Roster
An editorial brokerage back office for Verdant & Vale, built around a responsive grid of agent cards. Each card pairs a simulated architectural cover photo and monogram avatar with the agent’s role, team, active-listing count, year-to-date volume and a colour-coded status badge — active, inactive or pending. A summary strip in the header rolls up total agents, listings and volume across the desk.
The toolbar drives three filters that compose together: a debounced search across name, role, region and status; team chips for Luxury, Residential, Commercial and Rentals; and a status segmented control. When nothing matches, a friendly empty state offers a one-click reset. Every card exposes per-row actions — view profile, message and an overflow menu — each surfacing a toast for feedback.
The Invite agent button opens an accessible modal (focus management, Escape to close, backdrop dismissal, inline validation). On submit it prepends the new hire to the roster as a pending desk and confirms with a toast. Everything is vanilla HTML, CSS and JavaScript — no frameworks, no network — using warm CSS gradients to stand in for listing imagery.
Illustrative UI only — sample listings and data are fictional; not a real real-estate service.