Salon — Appointment Detail Card
A luxe, editorial appointment detail card for boutique salons: client avatar with VIP badge and phone, itemised service plus add-ons with line prices, stylist, date, time and duration, and a colour-coded status pill. One primary button advances the booking from Confirmed to Checked-in to In service to Done, while Reschedule and Message fire elegant toasts. A live subtotal, member discount and grand total complete the footer.
MCP
Codice
:root {
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--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;
--sh-sm: 0 1px 2px rgba(28, 24, 20, 0.06);
--sh-md: 0 10px 30px -12px rgba(28, 24, 20, 0.22);
--sh-lg: 0 30px 70px -28px rgba(28, 24, 20, 0.34);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
font-family: var(--sans);
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(120% 80% at 50% -10%, var(--cream) 0%, var(--bg) 55%) fixed;
min-height: 100vh;
}
.stage {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(18px, 4vw, 56px);
}
/* ── Card ─────────────────────────────── */
.appt {
width: 100%;
max-width: 520px;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
padding: clamp(22px, 4vw, 34px);
position: relative;
overflow: hidden;
}
.appt::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 3px;
background: linear-gradient(90deg, transparent, var(--gold), var(--rose), transparent);
opacity: 0.9;
}
/* ── Header ───────────────────────────── */
.appt__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand__mark {
width: 40px;
height: 40px;
display: grid;
place-items: center;
border-radius: 50%;
border: 1px solid var(--gold);
color: var(--gold-d);
font-family: var(--serif);
font-weight: 700;
font-size: 1rem;
letter-spacing: 0.04em;
background: var(--gold-soft);
}
.brand__txt {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.brand__name {
font-family: var(--serif);
font-weight: 600;
font-size: 1.15rem;
color: var(--ink);
}
.brand__sub {
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--muted);
}
/* ── Status pill ──────────────────────── */
.pill {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 13px 6px 11px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
border: 1px solid var(--line-2);
background: var(--cream);
color: var(--ink-2);
transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
white-space: nowrap;
}
.pill__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 22%, transparent);
}
.pill[data-status="confirmed"] {
color: var(--gold-d);
border-color: color-mix(in srgb, var(--gold) 45%, transparent);
background: var(--gold-soft);
}
.pill[data-status="checkedin"] {
color: #4d6fa3;
border-color: rgba(77, 111, 163, 0.4);
background: rgba(77, 111, 163, 0.1);
}
.pill[data-status="service"] {
color: var(--warn);
border-color: rgba(192, 138, 62, 0.4);
background: rgba(192, 138, 62, 0.12);
}
.pill[data-status="service"] .pill__dot {
animation: pulse 1.4s ease-in-out infinite;
}
.pill[data-status="done"] {
color: var(--ok);
border-color: rgba(95, 138, 107, 0.42);
background: rgba(95, 138, 107, 0.12);
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.8); }
}
/* ── Client ───────────────────────────── */
.client {
display: flex;
align-items: center;
gap: 16px;
margin-top: 22px;
}
.avatar {
flex: none;
width: 56px;
height: 56px;
border-radius: 50%;
display: grid;
place-items: center;
font-family: var(--serif);
font-weight: 600;
font-size: 1.25rem;
color: var(--white);
background: linear-gradient(140deg, var(--rose), var(--gold-d));
box-shadow: var(--sh-sm), inset 0 0 0 2px var(--white);
}
.client__info {
flex: 1;
min-width: 0;
}
.client__name {
margin: 0;
font-family: var(--serif);
font-weight: 600;
font-size: 1.55rem;
line-height: 1.1;
color: var(--ink);
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
}
.vip {
display: inline-flex;
align-items: center;
gap: 4px;
font-family: var(--sans);
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold-d);
background: var(--gold-soft);
border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
padding: 3px 8px;
border-radius: 999px;
}
.client__phone {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 4px;
font-size: 0.85rem;
color: var(--muted);
text-decoration: none;
transition: color 0.2s ease;
}
.client__phone:hover {
color: var(--gold-d);
}
.client__meta {
flex: none;
text-align: right;
display: flex;
flex-direction: column;
gap: 3px;
}
/* ── Meta primitives ──────────────────── */
.metalabel {
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--muted);
font-weight: 600;
}
.metaval {
font-size: 0.9rem;
color: var(--ink-2);
font-weight: 600;
}
.metaval--lead {
font-family: var(--serif);
font-size: 1.18rem;
font-weight: 600;
color: var(--ink);
}
.metasub {
font-size: 0.76rem;
color: var(--muted);
}
.rule {
height: 1px;
margin: 22px 0;
background: linear-gradient(90deg, transparent, var(--line-2) 18%, var(--line-2) 82%, transparent);
}
/* ── Schedule grid ────────────────────── */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.field + .field {
padding-left: 16px;
border-left: 1px solid var(--line);
}
/* ── Services ─────────────────────────── */
.section-cap {
display: block;
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--muted);
font-weight: 600;
margin-bottom: 12px;
}
.lines {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 11px 0;
border-bottom: 1px dashed var(--line);
}
.line:last-child {
border-bottom: none;
}
.line__name {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.94rem;
color: var(--ink-2);
font-weight: 500;
}
.line__price {
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--ink);
}
.line__tag {
font-size: 0.58rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gold-d);
background: var(--gold-soft);
padding: 2px 7px;
border-radius: 999px;
}
.line__addon {
font-size: 0.58rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
border: 1px solid var(--line-2);
padding: 2px 7px;
border-radius: 999px;
}
/* ── Notes ────────────────────────────── */
.notes {
display: flex;
gap: 10px;
margin-top: 18px;
padding: 13px 15px;
background: var(--rose-soft);
border: 1px solid color-mix(in srgb, var(--rose) 30%, transparent);
border-radius: var(--r-md);
}
.notes__icon {
flex: none;
color: var(--gold-d);
margin-top: 2px;
}
.notes__txt {
margin: 0;
font-size: 0.83rem;
line-height: 1.45;
color: var(--ink-2);
}
/* ── Footer / totals ──────────────────── */
.appt__foot {
margin-top: 22px;
}
.totals {
margin: 0 0 20px;
display: flex;
flex-direction: column;
gap: 7px;
}
.totals__row {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 0.9rem;
}
.totals__row dt {
color: var(--muted);
}
.totals__row dd {
margin: 0;
font-variant-numeric: tabular-nums;
font-weight: 600;
color: var(--ink-2);
}
.totals__row--muted dd {
color: var(--ok);
}
.totals__row--grand {
padding-top: 11px;
border-top: 1px solid var(--line-2);
align-items: baseline;
}
.totals__row--grand dt {
font-family: var(--serif);
font-size: 1.05rem;
color: var(--ink);
}
.totals__row--grand dd {
font-family: var(--serif);
font-size: 1.4rem;
font-weight: 700;
color: var(--gold-d);
}
/* ── Actions ──────────────────────────── */
.actions {
display: flex;
gap: 10px;
}
.btn {
flex: 1;
font-family: var(--sans);
font-size: 0.86rem;
font-weight: 600;
letter-spacing: 0.01em;
padding: 12px 14px;
border-radius: var(--r-md);
border: 1px solid var(--line-2);
background: var(--white);
color: var(--ink-2);
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.btn:hover {
border-color: var(--gold);
color: var(--gold-d);
box-shadow: var(--sh-sm);
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.btn--primary {
flex: 1.4;
background: linear-gradient(140deg, var(--ink), var(--ink-2));
color: var(--cream);
border-color: var(--ink);
}
.btn--primary:hover {
background: linear-gradient(140deg, var(--gold-d), var(--gold));
color: var(--white);
border-color: var(--gold-d);
box-shadow: var(--sh-md);
}
.btn--primary:disabled {
background: var(--cream);
color: var(--muted);
border-color: var(--line-2);
cursor: default;
box-shadow: none;
transform: none;
}
/* ── Toasts ───────────────────────────── */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 28px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 50;
pointer-events: none;
width: min(92vw, 380px);
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--ink);
color: var(--cream);
border-radius: var(--r-md);
box-shadow: var(--sh-md);
font-size: 0.85rem;
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.08);
opacity: 0;
transform: translateY(14px) scale(0.98);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast.show {
opacity: 1;
transform: translateY(0) scale(1);
}
.toast__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--gold);
flex: none;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}
/* ── Responsive ───────────────────────── */
@media (max-width: 520px) {
.appt {
padding: 20px 18px;
border-radius: var(--r-md);
}
.grid {
grid-template-columns: 1fr 1fr;
}
.field:nth-child(3) {
grid-column: 1 / -1;
padding-left: 0;
padding-top: 14px;
border-left: none;
border-top: 1px solid var(--line);
}
.client__meta {
display: none;
}
.actions {
flex-wrap: wrap;
}
.btn--primary {
flex-basis: 100%;
}
.client__name {
font-size: 1.35rem;
}
}(function () {
"use strict";
/* ── Toast helper ─────────────────────── */
var toastWrap = document.getElementById("toasts");
function toast(msg) {
var el = document.createElement("div");
el.className = "toast";
el.setAttribute("role", "status");
el.innerHTML = '<span class="toast__dot" aria-hidden="true"></span><span></span>';
el.querySelector("span:last-child").textContent = msg;
toastWrap.appendChild(el);
// force reflow so the transition runs
void el.offsetWidth;
el.classList.add("show");
window.setTimeout(function () {
el.classList.remove("show");
window.setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 320);
}, 2600);
}
/* ── Status machine ───────────────────── */
// Each step: machine-readable key, the pill label, and the label for the
// button that advances *to the next* step.
var FLOW = [
{ key: "confirmed", label: "Confirmed", next: "Check in" },
{ key: "checkedin", label: "Checked-in", next: "Begin service" },
{ key: "service", label: "In service", next: "Complete" },
{ key: "done", label: "Done", next: null }
];
var stepIndex = 0;
var pill = document.getElementById("status");
var pillLabel = document.getElementById("status-label");
var advanceBtn = document.getElementById("advance");
var ARRIVAL_MSG = {
checkedin: "Aria Vance checked in — Léa has been notified.",
service: "Service started at chair 3. Timer running.",
done: "Appointment complete. Receipt ready to send."
};
function render() {
var step = FLOW[stepIndex];
pill.setAttribute("data-status", step.key);
pillLabel.textContent = step.label;
if (step.next) {
advanceBtn.textContent = step.next;
advanceBtn.disabled = false;
} else {
advanceBtn.textContent = "Completed";
advanceBtn.disabled = true;
}
}
advanceBtn.addEventListener("click", function () {
if (stepIndex >= FLOW.length - 1) return;
stepIndex += 1;
render();
var step = FLOW[stepIndex];
if (ARRIVAL_MSG[step.key]) toast(ARRIVAL_MSG[step.key]);
});
/* ── Totals (derived from the line items) ─ */
var DISCOUNT_RATE = 0.1; // Atelier member
function money(n) {
var sign = n < 0 ? "−" : "";
var v = Math.abs(n).toFixed(2);
// trim a trailing ".00" to a clean dollar figure
v = v.replace(/\.00$/, "");
return sign + "$" + v;
}
function parsePrice(txt) {
var m = txt.replace(/[^0-9.]/g, "");
return m ? parseFloat(m) : 0;
}
function recalc() {
var subtotal = 0;
document.querySelectorAll("#lines .line__price").forEach(function (el) {
subtotal += parsePrice(el.textContent);
});
var discount = subtotal * DISCOUNT_RATE;
var total = subtotal - discount;
document.getElementById("subtotal").textContent = money(subtotal);
document.getElementById("discount").textContent = money(-discount);
document.getElementById("total").textContent = money(total);
}
/* ── Secondary actions ────────────────── */
document.getElementById("reschedule").addEventListener("click", function () {
toast("Reschedule link sent to Aria — choose a new slot.");
});
document.getElementById("message").addEventListener("click", function () {
toast("Message thread opened with Aria Vance.");
});
/* ── Init ─────────────────────────────── */
recalc();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Appointment Detail · 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&family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="stage">
<article class="appt" id="appt" aria-labelledby="appt-client">
<!-- Header -->
<header class="appt__head">
<div class="brand">
<span class="brand__mark" aria-hidden="true">ML</span>
<span class="brand__txt">
<span class="brand__name">Maison Lumière</span>
<span class="brand__sub">Appointment Detail</span>
</span>
</div>
<span class="pill" id="status" data-status="confirmed">
<span class="pill__dot" aria-hidden="true"></span>
<span class="pill__label" id="status-label">Confirmed</span>
</span>
</header>
<!-- Client -->
<section class="client" aria-label="Client">
<span class="avatar" aria-hidden="true">AV</span>
<div class="client__info">
<h1 class="client__name" id="appt-client">
Aria Vance
<span class="vip" title="VIP — Atelier member">
<svg viewBox="0 0 24 24" width="12" height="12" aria-hidden="true">
<path d="M12 2l2.5 6.5L21 9l-5 4.2L17.6 20 12 16.3 6.4 20 8 13.2 3 9l6.5-.5z" fill="currentColor"/>
</svg>
VIP
</span>
</h1>
<a class="client__phone" href="tel:+13105550148">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M6.6 10.8a15 15 0 006.6 6.6l2.2-2.2a1 1 0 011-.24 11.4 11.4 0 003.6.58 1 1 0 011 1V20a1 1 0 01-1 1A17 17 0 013 4a1 1 0 011-1h3.5a1 1 0 011 1 11.4 11.4 0 00.58 3.6 1 1 0 01-.25 1z" fill="currentColor"/></svg>
+1 (310) 555-0148
</a>
</div>
<div class="client__meta">
<span class="metalabel">Booking</span>
<span class="metaval">#ML-4821</span>
</div>
</section>
<div class="rule" role="presentation"></div>
<!-- Schedule -->
<section class="grid" aria-label="Schedule">
<div class="field">
<span class="metalabel">Stylist</span>
<span class="metaval metaval--lead">Léa Moreau</span>
<span class="metasub">Senior Colourist · Chair 3</span>
</div>
<div class="field">
<span class="metalabel">Date</span>
<span class="metaval metaval--lead">Sat, Jun 14</span>
<span class="metasub">2:30 PM – 4:15 PM</span>
</div>
<div class="field">
<span class="metalabel">Duration</span>
<span class="metaval metaval--lead">1h 45m</span>
<span class="metasub">Studio Suite B</span>
</div>
</section>
<div class="rule" role="presentation"></div>
<!-- Services -->
<section class="services" aria-label="Services and add-ons">
<span class="section-cap">Service & Add-ons</span>
<ul class="lines" id="lines">
<li class="line">
<span class="line__name">
Balayage & Gloss
<span class="line__tag">Signature</span>
</span>
<span class="line__price">$185</span>
</li>
<li class="line">
<span class="line__name">Bond-Repair Treatment <span class="line__addon">Add-on</span></span>
<span class="line__price">$45</span>
</li>
<li class="line">
<span class="line__name">Blow-out & Style <span class="line__addon">Add-on</span></span>
<span class="line__price">$55</span>
</li>
<li class="line">
<span class="line__name">Scalp Ritual Massage <span class="line__addon">Add-on</span></span>
<span class="line__price">$30</span>
</li>
</ul>
</section>
<!-- Notes -->
<section class="notes" aria-label="Stylist notes">
<svg class="notes__icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M5 3h11l3 3v15H5z" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M8 9h8M8 13h8M8 17h5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<p class="notes__txt">Prefers ash-toned warmth, no heavy fringe. Allergic to ammonia — use the gentle line. Offer sparkling rosé on arrival.</p>
</section>
<!-- Footer / totals -->
<footer class="appt__foot">
<dl class="totals">
<div class="totals__row"><dt>Subtotal</dt><dd id="subtotal">$315</dd></div>
<div class="totals__row totals__row--muted"><dt>Atelier discount (10%)</dt><dd id="discount">−$31.50</dd></div>
<div class="totals__row totals__row--grand"><dt>Total due</dt><dd id="total">$283.50</dd></div>
</dl>
<div class="actions">
<button class="btn btn--primary" id="advance" type="button">Check in</button>
<button class="btn" id="reschedule" type="button">Reschedule</button>
<button class="btn" id="message" type="button">Message</button>
</div>
</footer>
</article>
</main>
<div class="toast-wrap" id="toasts" aria-live="polite" aria-atomic="false"></div>
<script src="script.js"></script>
</body>
</html>Appointment Detail Card
The single card a front-desk concierge keeps open through a guest’s visit at Maison Lumière. It opens with the client — avatar, name, a small gold VIP badge for Atelier members, and a tappable phone number — then lays out the schedule in a thin gold-ruled grid: stylist and chair, date and time window, duration and suite. Services and add-ons are itemised with line prices and quiet category tags, and a rose-tinted note carries the stylist’s standing preferences.
The footer does real work. A primary button steps the booking forward through its lifecycle — Confirmed → Checked-in → In service → Done — recolouring the status pill at each stage and surfacing a contextual toast, with the In-service state gently pulsing. Reschedule and Message are one tap away. Subtotals, the member discount and the grand total are derived live from the line items in vanilla JS, so editing a price keeps the maths honest.
Built with Cormorant Garamond for display and Inter for the interface, generous whitespace, hairline gold dividers and soft editorial shadows. It is keyboard-usable, AA-contrast, respects reduced-motion, and reflows cleanly down to roughly 360px — no frameworks, no build step.