Wedding — Vendor Directory
An editorial blush-and-gold wedding vendor directory with a live category filter for florists, caterers, bands and venues. Each vendor card carries a star rating, price tier, tags and location, plus a save-heart toggle that tracks favourites and a book action that fires a graceful toast. Search, sort by rating or price, and a saved-only view make it feel like a real planning tool for couples browsing their dream team.
MCP
Code
:root {
--blush: #e8b7b0;
--blush-50: #fbeeec;
--rose: #c98a86;
--gold: #c9a24b;
--gold-d: #a8862f;
--ink: #3a2b2b;
--ink-2: #6b5555;
--muted: #9a8585;
--bg: #fbf7f4;
--cream: #f4ece4;
--white: #fff;
--line: rgba(58, 43, 43, 0.12);
--r-sm: 8px;
--r-md: 16px;
--r-lg: 26px;
--shadow: 0 18px 44px -26px rgba(58, 43, 43, 0.5);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 520px at 82% -8%, var(--blush-50), transparent 60%),
radial-gradient(900px 480px at -6% 4%, var(--cream), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1, h2, .price em, .vendor h3 { font-family: "Cormorant Garamond", Georgia, serif; }
.page {
max-width: 1120px;
margin: 0 auto;
padding: 44px 24px 64px;
}
/* Masthead */
.masthead { text-align: center; margin-bottom: 30px; }
.eyebrow {
margin: 0;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.34em;
text-transform: uppercase;
color: var(--gold-d);
}
.rule {
display: block;
width: 52px;
height: 1px;
margin: 16px auto;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
.masthead h1 {
margin: 0;
font-size: clamp(38px, 6vw, 62px);
font-weight: 500;
letter-spacing: 0.5px;
line-height: 1.02;
}
.lede {
max-width: 540px;
margin: 14px auto 0;
color: var(--ink-2);
font-size: 15.5px;
}
/* Toolbar */
.toolbar {
position: sticky;
top: 12px;
z-index: 30;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 14px;
padding: 14px;
margin-bottom: 14px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
}
.search {
position: relative;
flex: 1 1 240px;
min-width: 200px;
display: flex;
align-items: center;
}
.search-ic {
position: absolute;
left: 14px;
width: 18px;
height: 18px;
color: var(--muted);
pointer-events: none;
}
.search input {
width: 100%;
padding: 12px 14px 12px 40px;
font: inherit;
font-size: 14.5px;
color: var(--ink);
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search input::placeholder { color: var(--muted); }
.search input:focus {
outline: none;
border-color: var(--blush);
box-shadow: 0 0 0 4px var(--blush-50);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
padding: 9px 16px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
transition: transform 0.15s, background 0.2s, color 0.2s, border-color 0.2s;
}
.chip:hover { transform: translateY(-1px); border-color: var(--blush); }
.chip:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.chip.is-active {
color: var(--white);
background: linear-gradient(135deg, var(--rose), var(--blush));
border-color: transparent;
box-shadow: 0 8px 18px -10px var(--rose);
}
.tools-right {
display: flex;
align-items: center;
gap: 10px;
margin-left: auto;
}
.sort {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.sort select {
font: inherit;
font-size: 13px;
font-weight: 600;
text-transform: none;
letter-spacing: 0;
color: var(--ink);
padding: 9px 12px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
}
.sort select:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.saved-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink);
padding: 9px 15px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
transition: transform 0.15s, border-color 0.2s, background 0.2s;
}
.saved-toggle:hover { transform: translateY(-1px); border-color: var(--rose); }
.saved-toggle:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.saved-toggle svg { width: 16px; height: 16px; fill: none; stroke: var(--rose); stroke-width: 1.6; }
.saved-toggle em { font-style: normal; font-weight: 700; color: var(--rose); }
.saved-toggle[aria-pressed="true"] {
background: var(--blush-50);
border-color: var(--rose);
}
.saved-toggle[aria-pressed="true"] svg { fill: var(--rose); }
.result-line {
margin: 4px 6px 20px;
font-size: 13.5px;
color: var(--muted);
font-weight: 500;
}
/* Grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(288px, 1fr));
gap: 22px;
}
.vendor {
display: flex;
flex-direction: column;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: 0 10px 30px -24px rgba(58, 43, 43, 0.55);
transition: transform 0.22s ease, box-shadow 0.22s ease;
animation: rise 0.4s both;
}
.vendor:hover {
transform: translateY(-4px);
box-shadow: 0 26px 50px -28px rgba(58, 43, 43, 0.55);
}
@keyframes rise {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.cover {
position: relative;
height: 148px;
background-size: cover;
background-position: center;
}
.cover::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(58, 43, 43, 0) 40%, rgba(58, 43, 43, 0.28));
}
.price-badge {
position: absolute;
left: 14px;
bottom: 12px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--ink);
padding: 6px 11px;
background: rgba(255, 255, 255, 0.92);
border-radius: 999px;
box-shadow: 0 6px 16px -10px rgba(58, 43, 43, 0.6);
}
.price-badge .dim { color: var(--muted); }
.cat-badge {
position: absolute;
right: 14px;
top: 12px;
z-index: 2;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gold-d);
padding: 5px 10px;
background: rgba(255, 255, 255, 0.92);
border-radius: 999px;
}
.heart {
position: absolute;
right: 12px;
bottom: 10px;
z-index: 3;
width: 40px;
height: 40px;
display: grid;
place-items: center;
padding: 0;
background: rgba(255, 255, 255, 0.92);
border: none;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 6px 16px -8px rgba(58, 43, 43, 0.6);
transition: transform 0.18s;
}
.heart:hover { transform: scale(1.08); }
.heart:active { transform: scale(0.92); }
.heart:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
.heart svg { width: 20px; height: 20px; fill: none; stroke: var(--rose); stroke-width: 1.7; transition: fill 0.2s, transform 0.2s; }
.heart.is-saved svg { fill: var(--rose); }
.heart.pop svg { animation: pop 0.4s; }
@keyframes pop {
0% { transform: scale(1); }
40% { transform: scale(1.35); }
100% { transform: scale(1); }
}
.body {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px 18px 18px;
flex: 1;
}
.vendor-top {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 10px;
}
.vendor h3 {
margin: 0;
font-size: 24px;
font-weight: 600;
line-height: 1.1;
}
.rating {
display: inline-flex;
align-items: center;
gap: 5px;
white-space: nowrap;
font-size: 13px;
font-weight: 700;
color: var(--ink);
}
.rating .star { color: var(--gold); font-size: 14px; line-height: 1; }
.rating .rev { color: var(--muted); font-weight: 500; }
.loc {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--ink-2);
}
.loc svg { width: 14px; height: 14px; stroke: var(--rose); fill: none; stroke-width: 1.6; }
.blurb {
margin: 0;
font-size: 13.5px;
color: var(--ink-2);
}
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
.tag {
font-size: 11.5px;
font-weight: 600;
color: var(--gold-d);
padding: 4px 10px;
background: var(--cream);
border-radius: 999px;
}
.actions {
display: flex;
gap: 10px;
margin-top: auto;
padding-top: 6px;
}
.btn-book {
flex: 1;
font: inherit;
font-size: 14px;
font-weight: 700;
color: var(--white);
padding: 11px 14px;
background: linear-gradient(135deg, var(--gold), var(--gold-d));
border: none;
border-radius: 999px;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.2s, filter 0.2s;
box-shadow: 0 10px 22px -14px var(--gold-d);
}
.btn-book:hover { transform: translateY(-1px); filter: brightness(1.04); }
.btn-book:active { transform: translateY(0); }
.btn-book:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.btn-book.booked {
background: var(--blush-50);
color: var(--rose);
box-shadow: none;
}
/* Empty state */
.empty {
text-align: center;
padding: 60px 20px;
color: var(--ink-2);
}
.empty-mark { font-size: 44px; color: var(--blush); }
.empty h2 { margin: 8px 0 6px; font-size: 30px; font-weight: 500; }
.empty p { margin: 0 0 18px; }
.btn-ghost {
font: inherit;
font-size: 13.5px;
font-weight: 600;
color: var(--ink);
padding: 10px 20px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
cursor: pointer;
transition: border-color 0.2s, transform 0.15s;
}
.btn-ghost:hover { border-color: var(--rose); transform: translateY(-1px); }
.btn-ghost:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }
/* Footer */
.footfoot { text-align: center; margin-top: 48px; color: var(--muted); }
.footfoot p { margin: 0; font-size: 12.5px; letter-spacing: 0.04em; }
/* Toast */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
z-index: 90;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
font-weight: 600;
color: var(--ink);
padding: 13px 20px;
background: var(--white);
border: 1px solid var(--line);
border-left: 3px solid var(--gold);
border-radius: var(--r-md);
box-shadow: var(--shadow);
animation: toastIn 0.32s ease both;
}
.toast.out { animation: toastOut 0.3s ease forwards; }
.toast .ic { color: var(--gold-d); font-size: 16px; }
@keyframes toastIn {
from { opacity: 0; transform: translateY(16px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toastOut {
to { opacity: 0; transform: translateY(10px); }
}
@media (max-width: 520px) {
.page { padding: 30px 15px 48px; }
.toolbar {
position: static;
gap: 10px;
padding: 12px;
border-radius: var(--r-md);
}
.tools-right { margin-left: 0; width: 100%; justify-content: space-between; }
.chips { width: 100%; }
.chip { flex: 1; text-align: center; padding: 9px 10px; }
.grid { grid-template-columns: 1fr; gap: 16px; }
.masthead h1 { font-size: 40px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}(function () {
"use strict";
var GRADIENTS = {
florist: "linear-gradient(135deg,#e8b7b0,#f4ece4)",
caterer: "linear-gradient(135deg,#c9a24b,#e8cf9a)",
band: "linear-gradient(135deg,#6b5555,#c98a86)",
venue: "linear-gradient(135deg,#c98a86,#fbeeec)"
};
var CAT_LABEL = { florist: "Florist", caterer: "Caterer", band: "Band", venue: "Venue" };
var VENDORS = [
{ id: "v1", name: "Wildbloom Atelier", cat: "florist", price: 3, rating: 4.9, reviews: 214, city: "Savannah, GA", blurb: "Garden-gathered arrangements with heirloom roses and trailing greenery.", tags: ["Bridal bouquets", "Arches", "Seasonal"], featured: 9 },
{ id: "v2", name: "The Gilded Fork", cat: "caterer", price: 4, rating: 4.8, reviews: 176, city: "Charleston, SC", blurb: "Farm-to-table plated dinners and grazing tables for intimate receptions.", tags: ["Plated", "Vegan menu", "Tastings"], featured: 8 },
{ id: "v3", name: "Velvet & Brass", cat: "band", price: 3, rating: 4.7, reviews: 132, city: "Nashville, TN", blurb: "Eight-piece soul and motown band that keeps the dance floor glowing.", tags: ["Live band", "DJ set", "First dance"], featured: 7 },
{ id: "v4", name: "Marigold Manor", cat: "venue", price: 4, rating: 4.9, reviews: 98, city: "Napa Valley, CA", blurb: "Restored vineyard estate with olive-grove ceremony lawn and stone barn.", tags: ["Outdoor", "150 guests", "On-site suites"], featured: 10 },
{ id: "v5", name: "Petal & Vine Co.", cat: "florist", price: 2, rating: 4.6, reviews: 143, city: "Austin, TX", blurb: "Wild, unstructured stems in dusty rose and soft cream palettes.", tags: ["Boutonnieres", "Centerpieces", "Dried florals"], featured: 6 },
{ id: "v6", name: "Saffron Table", cat: "caterer", price: 3, rating: 4.7, reviews: 121, city: "Portland, OR", blurb: "Globally inspired family-style feasts with a wood-fired dessert bar.", tags: ["Family style", "Gluten-free", "Late-night bites"], featured: 6 },
{ id: "v7", name: "Golden Hour Strings", cat: "band", price: 2, rating: 4.8, reviews: 87, city: "Asheville, NC", blurb: "Acoustic trio and string quartet for ceremonies and cocktail hours.", tags: ["Ceremony", "Acoustic", "Requests"], featured: 5 },
{ id: "v8", name: "The Ivory Conservatory", cat: "venue", price: 3, rating: 4.8, reviews: 64, city: "Portland, ME", blurb: "Glasshouse ballroom flooded with light and framed by climbing jasmine.", tags: ["Indoor", "90 guests", "Rain-ready"], featured: 5 },
{ id: "v9", name: "Fern & Firefly", cat: "florist", price: 4, rating: 5.0, reviews: 76, city: "Sonoma, CA", blurb: "Editorial installations, hanging cloud florals and candlelit tablescapes.", tags: ["Installations", "Luxe", "Candlelight"], featured: 9 },
{ id: "v10", name: "Copper Kettle Kitchen", cat: "caterer", price: 2, rating: 4.5, reviews: 158, city: "Denver, CO", blurb: "Comfort-forward buffets, wood-smoked mains and a build-your-own bar.", tags: ["Buffet", "BBQ", "Bar service"], featured: 4 },
{ id: "v11", name: "Midnight Marmalade", cat: "band", price: 4, rating: 4.9, reviews: 54, city: "New Orleans, LA", blurb: "Brass-led party band with a second-line parade to send you off.", tags: ["Brass", "Second line", "Horns"], featured: 7 },
{ id: "v12", name: "Lakeshore Pavilion", cat: "venue", price: 2, rating: 4.6, reviews: 112, city: "Lake Geneva, WI", blurb: "Waterfront timber pavilion with sunset ceremonies over the pier.", tags: ["Waterfront", "120 guests", "Sunset"], featured: 4 }
];
var PRICE_TXT = { 1: "$", 2: "$$", 3: "$$$", 4: "$$$$" };
var grid = document.getElementById("grid");
var empty = document.getElementById("empty");
var searchEl = document.getElementById("search");
var sortEl = document.getElementById("sort");
var savedToggle = document.getElementById("savedToggle");
var savedCountEl = document.getElementById("savedCount");
var resultLine = document.getElementById("resultLine");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var toastWrap = document.getElementById("toastWrap");
var clearAll = document.getElementById("clearAll");
var state = { cat: "all", q: "", sort: "featured", savedOnly: false };
var saved = {};
function toast(msg, icon) {
var el = document.createElement("div");
el.className = "toast";
el.setAttribute("role", "status");
el.innerHTML = '<span class="ic">' + (icon || "✓") + "</span><span>" + msg + "</span>";
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
setTimeout(function () { el.remove(); }, 320);
}, 2600);
}
function stars(rating) {
var full = Math.round(rating);
var s = "";
for (var i = 0; i < 5; i++) s += i < full ? "★" : "☆";
return s;
}
function matches(v) {
if (state.cat !== "all" && v.cat !== state.cat) return false;
if (state.savedOnly && !saved[v.id]) return false;
if (state.q) {
var hay = (v.name + " " + v.city + " " + v.cat + " " + v.tags.join(" ")).toLowerCase();
if (hay.indexOf(state.q) === -1) return false;
}
return true;
}
function sortList(list) {
var arr = list.slice();
switch (state.sort) {
case "rating": arr.sort(function (a, b) { return b.rating - a.rating; }); break;
case "price-asc": arr.sort(function (a, b) { return a.price - b.price || b.rating - a.rating; }); break;
case "price-desc": arr.sort(function (a, b) { return b.price - a.price || b.rating - a.rating; }); break;
default: arr.sort(function (a, b) { return b.featured - a.featured || b.rating - a.rating; });
}
return arr;
}
function card(v) {
var el = document.createElement("article");
el.className = "vendor";
var isSaved = !!saved[v.id];
el.innerHTML =
'<div class="cover" style="background-image:' + GRADIENTS[v.cat] + '">' +
'<span class="cat-badge">' + CAT_LABEL[v.cat] + "</span>" +
'<span class="price-badge"><span>' + PRICE_TXT[v.price] + '</span><span class="dim">tier</span></span>' +
'<button class="heart' + (isSaved ? " is-saved" : "") + '" aria-pressed="' + isSaved + '" aria-label="' + (isSaved ? "Remove " : "Save ") + v.name + '">' +
'<svg viewBox="0 0 24 24"><path d="M12 21s-7.5-4.9-10-9.3C.2 8.3 1.7 4.5 5.2 4.5c2 0 3.3 1.1 4.1 2.3.7 1 .7 1 .7 1s0 0 .7-1c.8-1.2 2.1-2.3 4.1-2.3 3.5 0 5 3.8 3.2 7.2C19.5 16.1 12 21 12 21z"/></svg>' +
"</button>" +
"</div>" +
'<div class="body">' +
'<div class="vendor-top">' +
"<h3>" + v.name + "</h3>" +
'<span class="rating"><span class="star">' + stars(v.rating) + '</span>' + v.rating.toFixed(1) + ' <span class="rev">(' + v.reviews + ")</span></span>" +
"</div>" +
'<span class="loc"><svg viewBox="0 0 24 24"><path d="M12 21s7-6 7-11a7 7 0 10-14 0c0 5 7 11 7 11z"/><circle cx="12" cy="10" r="2.4"/></svg>' + v.city + "</span>" +
'<p class="blurb">' + v.blurb + "</p>" +
'<div class="tags">' + v.tags.map(function (t) { return '<span class="tag">' + t + "</span>"; }).join("") + "</div>" +
'<div class="actions">' +
'<button class="btn-book" data-book>Book vendor</button>' +
"</div>" +
"</div>";
var heart = el.querySelector(".heart");
heart.addEventListener("click", function () {
var nowSaved = !saved[v.id];
saved[v.id] = nowSaved;
heart.classList.toggle("is-saved", nowSaved);
heart.setAttribute("aria-pressed", String(nowSaved));
heart.setAttribute("aria-label", (nowSaved ? "Remove " : "Save ") + v.name);
if (nowSaved) { heart.classList.add("pop"); setTimeout(function () { heart.classList.remove("pop"); }, 400); }
updateSavedCount();
toast(nowSaved ? "Saved " + v.name + " to favourites" : "Removed " + v.name, nowSaved ? "♥" : "✗");
if (state.savedOnly) render();
});
var book = el.querySelector("[data-book]");
book.addEventListener("click", function () {
book.textContent = "Enquiry sent ✓";
book.classList.add("booked");
book.disabled = true;
toast("Enquiry sent to " + v.name + " · they reply within 48h", "✉");
});
return el;
}
function updateSavedCount() {
var n = Object.keys(saved).filter(function (k) { return saved[k]; }).length;
savedCountEl.textContent = n;
}
function render() {
var list = sortList(VENDORS.filter(matches));
grid.innerHTML = "";
if (!list.length) {
grid.hidden = true;
empty.hidden = false;
} else {
grid.hidden = false;
empty.hidden = true;
list.forEach(function (v, i) {
var c = card(v);
c.style.animationDelay = Math.min(i * 45, 360) + "ms";
grid.appendChild(c);
});
}
var total = VENDORS.length;
resultLine.textContent = "Showing " + list.length + " of " + total + " vendors" +
(state.cat !== "all" ? " · " + CAT_LABEL[state.cat] : "") +
(state.savedOnly ? " · saved only" : "");
}
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) { c.classList.remove("is-active"); c.setAttribute("aria-selected", "false"); });
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
state.cat = chip.getAttribute("data-cat");
render();
});
});
var t;
searchEl.addEventListener("input", function () {
clearTimeout(t);
t = setTimeout(function () {
state.q = searchEl.value.trim().toLowerCase();
render();
}, 120);
});
sortEl.addEventListener("change", function () {
state.sort = sortEl.value;
render();
});
savedToggle.addEventListener("click", function () {
state.savedOnly = !state.savedOnly;
savedToggle.setAttribute("aria-pressed", String(state.savedOnly));
render();
});
clearAll.addEventListener("click", function () {
state.cat = "all";
state.q = "";
state.savedOnly = false;
searchEl.value = "";
savedToggle.setAttribute("aria-pressed", "false");
chips.forEach(function (c) {
var on = c.getAttribute("data-cat") === "all";
c.classList.toggle("is-active", on);
c.setAttribute("aria-selected", String(on));
});
render();
});
updateSavedCount();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Evermore — Wedding Vendor Directory</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@400;500;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<p class="eyebrow">The Evermore Collective</p>
<span class="rule" aria-hidden="true"></span>
<h1>Wedding Vendor Directory</h1>
<p class="lede">Curated florists, caterers, bands and venues to bring your day to life. Save your favourites and send an enquiry in a single tap.</p>
</header>
<div class="toolbar" role="region" aria-label="Filter vendors">
<div class="search">
<svg viewBox="0 0 24 24" aria-hidden="true" class="search-ic"><path d="M21 21l-4.3-4.3M11 19a8 8 0 100-16 8 8 0 000 16z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<input id="search" type="search" placeholder="Search by name, style or city…" aria-label="Search vendors" autocomplete="off" />
</div>
<div class="chips" role="tablist" aria-label="Vendor category">
<button class="chip is-active" role="tab" aria-selected="true" data-cat="all">All</button>
<button class="chip" role="tab" aria-selected="false" data-cat="florist">Florist</button>
<button class="chip" role="tab" aria-selected="false" data-cat="caterer">Caterer</button>
<button class="chip" role="tab" aria-selected="false" data-cat="band">Band</button>
<button class="chip" role="tab" aria-selected="false" data-cat="venue">Venue</button>
</div>
<div class="tools-right">
<label class="sort">
<span>Sort</span>
<select id="sort" aria-label="Sort vendors">
<option value="featured">Featured</option>
<option value="rating">Top rated</option>
<option value="price-asc">Price: low to high</option>
<option value="price-desc">Price: high to low</option>
</select>
</label>
<button id="savedToggle" class="saved-toggle" aria-pressed="false">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.9-10-9.3C.2 8.3 1.7 4.5 5.2 4.5c2 0 3.3 1.1 4.1 2.3.7 1 .7 1 .7 1s0 0 .7-1c.8-1.2 2.1-2.3 4.1-2.3 3.5 0 5 3.8 3.2 7.2C19.5 16.1 12 21 12 21z" /></svg>
<span>Saved <em id="savedCount">0</em></span>
</button>
</div>
</div>
<p class="result-line" id="resultLine" aria-live="polite"></p>
<main class="grid" id="grid" aria-label="Vendors"></main>
<div class="empty" id="empty" hidden>
<div class="empty-mark" aria-hidden="true">✽</div>
<h2>No vendors match just yet</h2>
<p>Try another category or clear your search to see the full collection.</p>
<button id="clearAll" class="btn-ghost">Clear filters</button>
</div>
<footer class="footfoot">
<span class="rule" aria-hidden="true"></span>
<p>Handpicked with love · Evermore Collective · Fictional showcase</p>
</footer>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>A romantic, editorial vendor directory for couples assembling their wedding dream team. The header pairs a Cormorant Garamond serif title with thin gold rules, then a sticky toolbar offers a live search field, category chips (All, Florist, Caterer, Band, Venue), a sort control, and a “Saved” toggle. Below, a responsive grid of vendor cards each shows a gradient cover, a price-tier badge, star rating with review count, location, service tags, a heart to save favourites and a Book button.
Every control is genuinely interactive: typing filters vendors by name, tag or city; category chips and the sort menu recombine the results instantly; the empty state appears when nothing matches. The heart toggles a persistent favourite count in the toolbar, and the Book button spawns an accessible toast confirming the enquiry was sent. Hover, active and keyboard focus states are wired throughout.
The palette stays airy and warm — blush, cream and gold on a soft ivory background — with generous serifs, subtle card lift on hover and smooth micro-interactions. It is fully responsive down to ~360px, collapsing the toolbar and grid gracefully while keeping the heart, rating and booking affordances comfortably tappable.