Salon — Service Catalog
An editorial service menu for Maison Lumière Salon presenting fourteen hair, color, nail, spa, and brow rituals in a refined typographic card grid. Pill-style category chips with live counts filter the list, a search box narrows treatments by name in real time, and a result tally stays in sync. Each entry carries a short description, duration, dotted-leader price line with optional from pricing, and a Book button that fires an elegant toast confirmation.
MCP
Code
:root {
--gold: #b08d57;
--gold-d: #8c6d3f;
--gold-soft: #efe2cf;
--rose: #c9a78f;
--rose-soft: #f3e6dc;
--ink: #1c1814;
--ink-2: #3d362f;
--muted: #8a7d70;
--cream: #f7f1e8;
--bg: #faf6ef;
--white: #ffffff;
--line: rgba(28, 24, 20, 0.1);
--line-2: rgba(28, 24, 20, 0.18);
--ok: #5f8a6b;
--warn: #c08a3e;
--danger: #b3503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--shadow-sm: 0 1px 2px rgba(28, 24, 20, 0.05), 0 1px 1px rgba(28, 24, 20, 0.04);
--shadow-md: 0 14px 40px -20px rgba(28, 24, 20, 0.35);
--shadow-lg: 0 30px 70px -34px rgba(28, 24, 20, 0.42);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
font-size: 16px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3 {
font-family: var(--serif);
font-weight: 600;
margin: 0;
letter-spacing: 0.01em;
}
.page {
max-width: 1080px;
margin: 0 auto;
padding: 0 24px 80px;
}
/* ---------- Masthead ---------- */
.masthead {
display: flex;
align-items: center;
gap: 24px;
padding: 26px 0 22px;
border-bottom: 1px solid var(--line);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand__mark {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border-radius: 50%;
border: 1px solid var(--gold);
color: var(--gold-d);
font-family: var(--serif);
font-weight: 700;
font-size: 16px;
letter-spacing: 0.04em;
}
.brand__name {
font-family: var(--serif);
font-size: 22px;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--ink);
}
.masthead__nav {
display: flex;
gap: 26px;
margin-left: auto;
}
.masthead__nav a {
font-size: 13px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
text-decoration: none;
padding-bottom: 3px;
border-bottom: 1px solid transparent;
transition: color 0.2s ease, border-color 0.2s ease;
}
.masthead__nav a:hover,
.masthead__nav a:focus-visible {
color: var(--ink);
}
.masthead__nav a.is-active {
color: var(--gold-d);
border-bottom-color: var(--gold);
}
.masthead__cta {
flex-shrink: 0;
}
/* ---------- Hero ---------- */
.hero {
text-align: center;
padding: 56px 0 38px;
}
.eyebrow {
margin: 0 0 14px;
font-size: 12px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--gold-d);
}
.hero__title {
font-size: clamp(44px, 8vw, 76px);
line-height: 1.02;
margin: 0 0 18px;
}
.hero__lede {
max-width: 540px;
margin: 0 auto;
color: var(--ink-2);
font-size: 17px;
}
/* ---------- Toolbar / search ---------- */
.toolbar {
display: flex;
align-items: center;
gap: 18px;
padding: 8px 0 18px;
flex-wrap: wrap;
}
.search {
position: relative;
flex: 1 1 320px;
min-width: 240px;
}
.search__icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
fill: none;
stroke: var(--muted);
stroke-width: 1.7;
stroke-linecap: round;
pointer-events: none;
}
.search__input {
width: 100%;
padding: 14px 18px 14px 46px;
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
background: var(--white);
font-family: inherit;
font-size: 15px;
color: var(--ink);
box-shadow: var(--shadow-sm);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.search__input::placeholder {
color: var(--muted);
}
.search__input:focus-visible {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px var(--gold-soft);
}
.result-count {
margin: 0;
color: var(--muted);
font-size: 13px;
letter-spacing: 0.04em;
white-space: nowrap;
}
.result-count #count {
color: var(--gold-d);
font-weight: 600;
}
/* ---------- Filter chips ---------- */
.filters {
display: flex;
gap: 10px;
flex-wrap: wrap;
padding-bottom: 30px;
border-bottom: 1px solid var(--line);
}
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 18px;
border: 1px solid var(--line-2);
border-radius: 999px;
background: var(--white);
color: var(--ink-2);
font-family: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.04em;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease,
box-shadow 0.2s ease, transform 0.12s ease;
}
.chip:hover {
border-color: var(--gold);
color: var(--ink);
}
.chip:active {
transform: translateY(1px);
}
.chip:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--gold-soft);
}
.chip.is-active {
background: var(--ink);
border-color: var(--ink);
color: var(--cream);
box-shadow: var(--shadow-md);
}
.chip__n {
font-size: 11px;
font-variant-numeric: tabular-nums;
color: var(--muted);
}
.chip.is-active .chip__n {
color: var(--gold-soft);
}
/* ---------- Catalog ---------- */
.catalog {
list-style: none;
margin: 30px 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 26px 40px;
}
.service {
display: flex;
flex-direction: column;
padding-bottom: 24px;
border-bottom: 1px solid var(--line);
animation: rise 0.4s ease both;
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(10px);
}
}
.service__head {
display: flex;
align-items: baseline;
gap: 12px;
}
.service__name {
font-size: 25px;
line-height: 1.15;
color: var(--ink);
}
.service__leader {
flex: 1;
height: 1px;
margin-bottom: 6px;
background-image: linear-gradient(
to right,
transparent,
var(--line-2) 40%,
var(--line-2)
);
background-size: 6px 1px;
}
.service__price {
font-family: var(--serif);
font-size: 24px;
font-weight: 600;
color: var(--gold-d);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.service__from {
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
margin-right: 4px;
}
.service__desc {
margin: 10px 0 16px;
color: var(--ink-2);
font-size: 14.5px;
max-width: 46ch;
}
.service__meta {
display: flex;
align-items: center;
gap: 16px;
margin-top: auto;
}
.tag {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.tag__cat {
color: var(--gold-d);
border: 1px solid var(--gold-soft);
background: var(--rose-soft);
padding: 4px 10px;
border-radius: 999px;
}
.tag__dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--rose);
}
.service__book {
margin-left: auto;
}
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 11px 22px;
border-radius: 999px;
border: 1px solid transparent;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease,
box-shadow 0.2s ease, transform 0.12s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--gold-soft);
}
.btn--book {
background: var(--ink);
color: var(--cream);
}
.btn--book:hover {
background: var(--gold-d);
}
.btn--ghost {
background: transparent;
color: var(--gold-d);
border-color: var(--gold);
}
.btn--ghost:hover {
background: var(--gold);
color: var(--white);
border-color: var(--gold);
}
/* ---------- Empty state ---------- */
.empty {
text-align: center;
padding: 64px 20px;
}
.empty__title {
font-family: var(--serif);
font-size: 30px;
margin: 0 0 6px;
color: var(--ink);
}
.empty__sub {
margin: 0 0 22px;
color: var(--muted);
}
/* ---------- Footer ---------- */
.footer {
margin-top: 60px;
padding-top: 30px;
border-top: 1px solid var(--line);
text-align: center;
}
.footer__name {
font-family: var(--serif);
font-size: 22px;
margin: 0 0 6px;
color: var(--ink);
}
.footer__line {
margin: 0;
font-size: 12px;
letter-spacing: 0.06em;
color: var(--muted);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 32px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--cream);
padding: 14px 22px;
border-radius: var(--r-md);
font-size: 14px;
letter-spacing: 0.02em;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.28s ease, transform 0.28s ease;
z-index: 50;
max-width: calc(100vw - 40px);
text-align: center;
}
.toast::before {
content: "✦";
color: var(--gold);
margin-right: 8px;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
pointer-events: auto;
}
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.catalog {
grid-template-columns: 1fr;
gap: 22px;
}
}
@media (max-width: 520px) {
.page {
padding: 0 18px 60px;
}
.masthead {
flex-wrap: wrap;
gap: 14px;
}
.masthead__nav {
order: 3;
width: 100%;
margin-left: 0;
justify-content: space-between;
gap: 12px;
}
.masthead__cta {
margin-left: auto;
}
.hero {
padding: 38px 0 28px;
}
.toolbar {
flex-direction: column;
align-items: stretch;
}
.result-count {
text-align: right;
}
.service__head {
flex-wrap: wrap;
}
.service__leader {
display: none;
}
.service__price {
margin-left: auto;
}
.service__meta {
flex-wrap: wrap;
gap: 12px;
}
.service__book {
margin-left: 0;
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
var CATEGORY_LABELS = {
hair: "Hair",
color: "Color",
nails: "Nails",
spa: "Spa",
brows: "Brows & Lashes",
};
var SERVICES = [
{
name: "Signature Cut & Style",
category: "hair",
desc: "A bespoke consultation, precision cut, and finish tailored to your face and hair architecture.",
duration: 75,
price: 95,
},
{
name: "Dry Bar Blowout",
category: "hair",
desc: "Wash, scalp ritual, and a long-lasting smooth or tousled blow-dry finish.",
duration: 45,
price: 55,
},
{
name: "Keratin Smoothing Ritual",
category: "hair",
desc: "A botanical keratin treatment that tames frizz and adds a glassy, weightless shine.",
duration: 150,
price: 220,
from: true,
},
{
name: "Gentlemen's Cut & Beard",
category: "hair",
desc: "Tailored clipper-and-scissor cut with a hot-towel beard sculpt and finish.",
duration: 50,
price: 60,
},
{
name: "Full Balayage",
category: "color",
desc: "Hand-painted, sun-kissed dimension blended seamlessly through mid-lengths and ends.",
duration: 180,
price: 240,
from: true,
},
{
name: "Gloss & Tone Refresh",
category: "color",
desc: "A demi-permanent glaze to revive tone, neutralize brass, and restore mirror shine.",
duration: 45,
price: 65,
},
{
name: "Root Touch-Up",
category: "color",
desc: "Seamless regrowth coverage matched precisely to your existing color story.",
duration: 90,
price: 85,
},
{
name: "Couture Gel Manicure",
category: "nails",
desc: "Shape, cuticle care, and a chip-resistant gel finish in a curated seasonal palette.",
duration: 60,
price: 48,
},
{
name: "Luxe Spa Pedicure",
category: "nails",
desc: "Warm soak, exfoliating scrub, mask, and extended massage with a flawless polish.",
duration: 70,
price: 62,
},
{
name: "Aurelia Facial",
category: "spa",
desc: "A deep-cleanse, lymphatic massage, and brightening botanical mask for luminous skin.",
duration: 60,
price: 110,
},
{
name: "Hot Stone Back Ritual",
category: "spa",
desc: "A grounding stone-and-oil massage focused on the back, neck, and shoulders.",
duration: 50,
price: 95,
},
{
name: "Scalp Detox & Head Spa",
category: "spa",
desc: "A clarifying scalp treatment with steam, massage, and a restorative serum seal.",
duration: 45,
price: 78,
},
{
name: "Brow Lamination & Tint",
category: "brows",
desc: "Sculpted, fuller-looking brows set in place with a custom tint and conditioning finish.",
duration: 45,
price: 58,
},
{
name: "Volume Lash Set",
category: "brows",
desc: "Featherlight hand-fanned extensions for soft, wide-eyed dimension that lasts.",
duration: 120,
price: 145,
from: true,
},
];
var catalog = document.getElementById("catalog");
var searchInput = document.getElementById("search");
var countEl = document.getElementById("count");
var emptyEl = document.getElementById("empty");
var resetBtn = document.getElementById("reset");
var toastEl = document.getElementById("toast");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var state = { filter: "all", query: "" };
var toastTimer = null;
function fmtPrice(n) {
return "$" + n.toFixed(0);
}
function fmtDuration(min) {
if (min < 60) return min + " min";
var h = Math.floor(min / 60);
var m = min % 60;
return m ? h + " hr " + m + " min" : h + " hr";
}
function escapeHtml(s) {
return s.replace(/[&<>"']/g, function (c) {
return {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
}[c];
});
}
function cardHtml(svc, i) {
var from = svc.from
? '<span class="service__from">from</span>'
: "";
return (
'<li class="service" data-category="' +
svc.category +
'" style="animation-delay:' +
Math.min(i * 35, 280) +
'ms">' +
'<div class="service__head">' +
'<h3 class="service__name">' +
escapeHtml(svc.name) +
"</h3>" +
'<span class="service__leader" aria-hidden="true"></span>' +
'<span class="service__price">' +
from +
fmtPrice(svc.price) +
"</span>" +
"</div>" +
'<p class="service__desc">' +
escapeHtml(svc.desc) +
"</p>" +
'<div class="service__meta">' +
'<span class="tag tag__cat">' +
escapeHtml(CATEGORY_LABELS[svc.category]) +
"</span>" +
'<span class="tag"><span class="tag__dot" aria-hidden="true"></span>' +
fmtDuration(svc.duration) +
"</span>" +
'<button class="btn btn--book service__book" type="button" data-book="' +
escapeHtml(svc.name) +
'">Book</button>' +
"</div>" +
"</li>"
);
}
function matches(svc) {
if (state.filter !== "all" && svc.category !== state.filter) return false;
if (state.query) {
var q = state.query;
var hay = (svc.name + " " + svc.desc + " " + CATEGORY_LABELS[svc.category]).toLowerCase();
if (hay.indexOf(q) === -1) return false;
}
return true;
}
function render() {
var visible = SERVICES.filter(matches);
catalog.innerHTML = visible.map(cardHtml).join("");
countEl.textContent = String(visible.length);
var hasNone = visible.length === 0;
emptyEl.hidden = !hasNone;
catalog.hidden = hasNone;
}
function updateChipCounts() {
chips.forEach(function (chip) {
var key = chip.getAttribute("data-filter");
var n =
key === "all"
? SERVICES.length
: SERVICES.filter(function (s) {
return s.category === key;
}).length;
var span = chip.querySelector(".chip__n");
if (span) span.textContent = n;
});
}
function setFilter(key) {
state.filter = key;
chips.forEach(function (chip) {
var active = chip.getAttribute("data-filter") === key;
chip.classList.toggle("is-active", active);
chip.setAttribute("aria-selected", active ? "true" : "false");
});
render();
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
/* ---- events ---- */
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
setFilter(chip.getAttribute("data-filter"));
});
});
searchInput.addEventListener("input", function () {
state.query = searchInput.value.trim().toLowerCase();
render();
});
document.addEventListener("click", function (e) {
var btn = e.target.closest("[data-book]");
if (!btn) return;
var name = btn.getAttribute("data-book");
if (name === "__appointment__") {
toast("Booking request started — we'll confirm your visit shortly.");
} else {
toast("Reserved · " + name + " — a stylist will confirm your time.");
}
});
resetBtn.addEventListener("click", function () {
searchInput.value = "";
state.query = "";
setFilter("all");
searchInput.focus();
});
/* ---- init ---- */
updateChipCounts();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Service Catalog · Maison Lumière Salon</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&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<div class="brand">
<span class="brand__mark" aria-hidden="true">ML</span>
<span class="brand__name">Maison Lumière</span>
</div>
<nav class="masthead__nav" aria-label="Primary">
<a href="#" class="is-active">Services</a>
<a href="#">Stylists</a>
<a href="#">Journal</a>
<a href="#">Contact</a>
</nav>
<button class="btn btn--ghost masthead__cta" type="button" data-book="__appointment__">
Book a visit
</button>
</header>
<section class="hero">
<p class="eyebrow">The Atelier Menu</p>
<h1 class="hero__title">Service Catalog</h1>
<p class="hero__lede">
A considered collection of hair, color, nail, and spa rituals — each performed by
a senior artist using small-batch, botanical-led formulas.
</p>
</section>
<div class="toolbar" role="search">
<div class="search">
<svg class="search__icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="7" />
<line x1="16.5" y1="16.5" x2="21" y2="21" />
</svg>
<input
id="search"
class="search__input"
type="search"
placeholder="Search treatments…"
autocomplete="off"
aria-label="Search services by name"
/>
</div>
<p class="result-count" aria-live="polite">
<span id="count">14</span> services
</p>
</div>
<div class="filters" role="tablist" aria-label="Filter services by category">
<button class="chip is-active" type="button" role="tab" aria-selected="true" data-filter="all">
All <span class="chip__n" data-count="all"></span>
</button>
<button class="chip" type="button" role="tab" aria-selected="false" data-filter="hair">
Hair <span class="chip__n" data-count="hair"></span>
</button>
<button class="chip" type="button" role="tab" aria-selected="false" data-filter="color">
Color <span class="chip__n" data-count="color"></span>
</button>
<button class="chip" type="button" role="tab" aria-selected="false" data-filter="nails">
Nails <span class="chip__n" data-count="nails"></span>
</button>
<button class="chip" type="button" role="tab" aria-selected="false" data-filter="spa">
Spa <span class="chip__n" data-count="spa"></span>
</button>
<button class="chip" type="button" role="tab" aria-selected="false" data-filter="brows">
Brows & Lashes <span class="chip__n" data-count="brows"></span>
</button>
</div>
<main>
<ul id="catalog" class="catalog" aria-label="Service menu"></ul>
<div id="empty" class="empty" hidden>
<p class="empty__title">No treatments found</p>
<p class="empty__sub">Try a different search or clear the filters.</p>
<button class="btn btn--ghost" type="button" id="reset">Reset</button>
</div>
</main>
<footer class="footer">
<p class="footer__name">Maison Lumière Salon</p>
<p class="footer__line">
14 Rue des Lilas · By appointment · Prices in USD, gratuity not included
</p>
</footer>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Service Catalog
A boutique service menu for the fictional Maison Lumière Salon, styled like a printed atelier card. A serif display masthead and centered hero set the editorial tone, while thin gold hairlines, small-caps labels, and dotted price leaders give each treatment the feel of a curated menu rather than a generic list. Fourteen realistic services span Hair, Color, Nails, Spa, and Brows & Lashes — each with a short description, duration, and a price that can read as a fixed figure or as from pricing for variable work.
The toolbar pairs a live search field with a running result count. Category chips carry per-section counts and live-filter the grid; the active chip flips to a matte-black pill, and search and filter compose together so you can narrow to, say, color treatments containing balayage. When nothing matches, a graceful empty state offers a one-tap reset that clears the query and returns to All.
Every card ends with a Book button, and a brand-level Book a visit action sits in the masthead; both surface a gold-flecked toast confirmation. The whole screen is built with vanilla HTML, CSS, and JavaScript — no frameworks — is keyboard-usable with visible focus rings, respects reduced-motion preferences, and reflows cleanly down to a single column on narrow phones.