Clinic — Appointment Booking
A guided three-step appointment wizard — pick a specialty, choose a doctor with ratings and next-availability, then select a day and time slot — with a live summary, validation-gated navigation, and a final confirmation panel carrying a booking reference.
MCP
Код
:root {
--teal: #129c93;
--teal-d: #0c7a73;
--teal-700: #0a655f;
--teal-50: #e7f5f3;
--coral: #ff7a66;
--coral-soft: #ffe6df;
--ink: #16322f;
--ink-2: #3a534f;
--muted: #6b827e;
--bg: #f1f7f6;
--white: #ffffff;
--line: rgba(16, 50, 47, 0.1);
--line-2: rgba(16, 50, 47, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--pending: #6b7280;
--danger: #d4503e;
--font: "Inter", system-ui, -apple-system, sans-serif;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-1: 0 1px 2px rgba(16, 50, 47, 0.05), 0 4px 14px rgba(16, 50, 47, 0.06);
--shadow-2: 0 16px 40px rgba(12, 122, 115, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
/* ── App bar ── */
.appbar {
background: var(--white);
border-bottom: 1px solid var(--line);
padding: 14px 28px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--ink);
}
.brand-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: 10px;
background: linear-gradient(150deg, var(--teal), var(--teal-d));
color: #fff;
font-size: 1.1rem;
font-weight: 700;
}
.brand-name {
font-size: 1.1rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.brand-name em {
font-style: normal;
color: var(--teal-d);
}
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: var(--teal-d);
color: #fff;
font-weight: 700;
font-size: 0.82rem;
cursor: pointer;
}
/* ── Layout ── */
.booking {
max-width: 1020px;
margin: 0 auto;
padding: 28px 28px 56px;
}
.head {
margin-bottom: 18px;
}
.eyebrow {
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--teal-d);
}
.head h1 {
font-size: 1.75rem;
font-weight: 800;
letter-spacing: -0.02em;
margin-top: 2px;
}
/* ── Stepper ── */
.stepper {
list-style: none;
display: flex;
align-items: center;
gap: 8px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
padding: 10px 16px;
margin-bottom: 22px;
}
.step {
display: flex;
align-items: center;
gap: 9px;
flex: 1;
color: var(--muted);
font-size: 0.9rem;
font-weight: 600;
}
.step:not(:last-child)::after {
content: "";
flex: 1;
height: 2px;
border-radius: 2px;
background: var(--line);
margin-left: 8px;
}
.step-num {
width: 28px;
height: 28px;
flex: none;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--bg);
border: 1px solid var(--line-2);
color: var(--muted);
font-size: 0.84rem;
font-weight: 700;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.step.is-current {
color: var(--ink);
}
.step.is-current .step-num {
background: var(--teal-d);
border-color: var(--teal-d);
color: #fff;
}
.step.is-done {
color: var(--teal-d);
}
.step.is-done .step-num {
background: var(--teal-50);
border-color: var(--teal);
color: transparent;
font-size: 0;
}
.step.is-done .step-num::after {
content: "✓";
color: var(--teal-d);
font-size: 0.9rem;
}
.step.is-done:not(:last-child)::after {
background: var(--teal);
}
/* ── Two-column layout ── */
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 22px;
align-items: start;
}
.flow {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 22px 24px;
min-height: 320px;
}
.step-title {
font-size: 1.1rem;
font-weight: 700;
letter-spacing: -0.01em;
margin-bottom: 16px;
}
/* ── Step 1: specialties ── */
.spec-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.spec-card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 14px;
display: flex;
flex-direction: column;
align-items: center;
gap: 9px;
text-align: center;
font: inherit;
cursor: pointer;
transition: transform 0.14s, box-shadow 0.14s, border-color 0.14s;
}
.spec-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-1);
border-color: var(--teal);
}
.spec-card.is-selected {
border-color: var(--teal);
background: var(--teal-50);
box-shadow: 0 0 0 2px rgba(18, 156, 147, 0.3);
}
.spec-ic {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 12px;
background: var(--teal-50);
color: var(--teal-d);
font-size: 1.4rem;
}
.spec-card.is-selected .spec-ic {
background: var(--teal-d);
color: #fff;
}
.spec-name {
font-weight: 700;
font-size: 0.92rem;
color: var(--ink);
}
.spec-blurb {
font-size: 0.78rem;
color: var(--muted);
line-height: 1.35;
}
/* ── Step 2: doctors ── */
.doc-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
}
.doc-card {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 16px;
font: inherit;
text-align: left;
cursor: pointer;
transition: border-color 0.14s, box-shadow 0.14s, background 0.14s;
}
.doc-card:hover {
border-color: var(--teal);
box-shadow: var(--shadow-1);
}
.doc-card.is-selected {
border-color: var(--teal);
background: var(--teal-50);
box-shadow: 0 0 0 2px rgba(18, 156, 147, 0.3);
}
.doc-avatar {
width: 48px;
height: 48px;
flex: none;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--teal-d);
color: #fff;
font-weight: 700;
font-size: 0.92rem;
}
.doc-card:nth-child(2n) .doc-avatar {
background: #4f7cac;
}
.doc-card:nth-child(3n) .doc-avatar {
background: var(--coral);
}
.doc-body {
flex: 1;
min-width: 0;
}
.doc-name {
font-weight: 700;
font-size: 0.96rem;
}
.doc-sub {
font-size: 0.82rem;
color: var(--muted);
margin-top: 1px;
}
.doc-avail {
font-size: 0.8rem;
color: var(--teal-d);
font-weight: 600;
margin-top: 4px;
}
.doc-rating {
flex: none;
text-align: right;
font-size: 0.82rem;
color: var(--ink-2);
font-weight: 600;
}
.doc-rating .stars {
color: var(--warn);
letter-spacing: 1px;
}
/* ── Step 3: date strip + slots ── */
.date-strip {
display: flex;
gap: 8px;
overflow-x: auto;
padding-bottom: 4px;
margin-bottom: 18px;
}
.date-chip {
flex: none;
min-width: 64px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--white);
padding: 10px 6px;
text-align: center;
font: inherit;
cursor: pointer;
transition: border-color 0.14s, background 0.14s;
}
.date-chip:hover {
border-color: var(--teal);
}
.date-chip.is-selected {
border-color: var(--teal);
background: var(--teal-d);
color: #fff;
}
.dc-dow {
display: block;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
font-weight: 600;
}
.date-chip.is-selected .dc-dow {
color: rgba(255, 255, 255, 0.85);
}
.dc-day {
display: block;
font-size: 1.15rem;
font-weight: 800;
margin-top: 2px;
}
.slot-head {
font-size: 0.78rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ink-2);
margin: 14px 0 8px;
}
.slot-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.slot {
border: 1px solid var(--line);
border-radius: var(--r-sm);
background: var(--white);
padding: 10px 6px;
font: inherit;
font-weight: 600;
font-size: 0.86rem;
color: var(--ink);
cursor: pointer;
transition: border-color 0.14s, background 0.14s;
}
.slot:hover:not(:disabled) {
border-color: var(--teal);
background: var(--teal-50);
}
.slot.is-selected {
border-color: var(--teal);
background: var(--teal-d);
color: #fff;
}
.slot:disabled {
color: var(--muted);
background: var(--bg);
cursor: not-allowed;
text-decoration: line-through;
opacity: 0.6;
}
/* ── Success panel ── */
.panel-step.success {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 28px 16px;
gap: 10px;
}
.ok-mark {
width: 64px;
height: 64px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--teal-50);
color: var(--ok);
font-size: 2rem;
font-weight: 800;
margin-bottom: 6px;
}
.panel-step.success h2 {
font-size: 1.35rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.ok-sub {
color: var(--muted);
font-size: 0.92rem;
max-width: 360px;
}
.ok-ref {
margin-top: 6px;
background: var(--teal-50);
color: var(--teal-d);
border-radius: 999px;
padding: 8px 16px;
font-size: 0.88rem;
font-weight: 600;
}
.ok-actions {
display: flex;
gap: 10px;
margin-top: 12px;
}
/* ── Buttons ── */
.btn {
border: none;
border-radius: 11px;
padding: 11px 18px;
font: inherit;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
transition: transform 0.12s, background 0.15s, opacity 0.15s;
}
.btn:active {
transform: translateY(1px);
}
.btn.primary {
background: var(--coral);
color: #fff;
}
.btn.primary:hover {
background: #ff8e7c;
}
.btn.primary:disabled {
background: var(--coral-soft);
color: #fff;
cursor: not-allowed;
}
.btn.ghost {
background: var(--white);
border: 1px solid var(--line-2);
color: var(--ink-2);
}
.btn.ghost:hover {
background: var(--teal-50);
border-color: var(--teal);
}
/* ── Summary sidebar ── */
.summary {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 20px;
position: sticky;
top: 86px;
box-shadow: var(--shadow-1);
}
.sum-title {
font-size: 1rem;
font-weight: 700;
margin-bottom: 14px;
}
.sum-list {
display: flex;
flex-direction: column;
}
.sum-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid var(--line);
}
.sum-row dt {
font-size: 0.82rem;
color: var(--muted);
}
.sum-row dd {
font-size: 0.88rem;
font-weight: 600;
text-align: right;
}
.nav {
display: flex;
gap: 8px;
margin-top: 16px;
}
.nav .btn {
flex: 1;
}
.sum-fine {
margin-top: 12px;
font-size: 0.78rem;
color: var(--muted);
text-align: center;
}
/* ── Toast ── */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
background: var(--ink);
color: #fff;
padding: 13px 20px;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow-2);
z-index: 50;
max-width: 90vw;
}
@media (max-width: 820px) {
.layout {
grid-template-columns: 1fr;
}
.summary {
position: static;
}
.spec-grid {
grid-template-columns: repeat(2, 1fr);
}
.slot-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* Visibility guard: honor the [hidden] attribute over base display */
.panel-step[hidden],
.success[hidden] {
display: none;
}// ── Data ─────────────────────────────────────────────────────────────────────
const SPECIALTIES = [
{ id: "cardio", icon: "♥", name: "Cardiology", blurb: "Heart & vascular care" },
{ id: "derm", icon: "✦", name: "Dermatology", blurb: "Skin, hair & nails" },
{ id: "peds", icon: "☺", name: "Pediatrics", blurb: "Care for children" },
{ id: "gp", icon: "✚", name: "General Practice", blurb: "Everyday primary care" },
{ id: "ortho", icon: "⚙", name: "Orthopedics", blurb: "Bones & joints" },
{ id: "mind", icon: "◐", name: "Mental Health", blurb: "Therapy & wellbeing" },
];
const DOCTORS = {
cardio: [
{
name: "Dr. Lena Okafor",
sub: "Interventional cardiology",
rating: "4.9",
avail: "Tomorrow, 9:00 AM",
},
{ name: "Dr. Marcus Hale", sub: "Heart failure", rating: "4.7", avail: "Thu, 11:30 AM" },
],
derm: [
{ name: "Dr. Priya Nair", sub: "Medical dermatology", rating: "4.8", avail: "Today, 4:15 PM" },
{ name: "Dr. Owen Brandt", sub: "Cosmetic dermatology", rating: "4.6", avail: "Wed, 10:00 AM" },
],
peds: [
{
name: "Dr. Sara Nguyen",
sub: "General pediatrics",
rating: "5.0",
avail: "Tomorrow, 8:30 AM",
},
{ name: "Dr. Tomas Reyes", sub: "Pediatric pulmonology", rating: "4.8", avail: "Fri, 1:00 PM" },
],
gp: [
{ name: "Dr. Ravi Patel", sub: "Family medicine", rating: "4.9", avail: "Today, 3:00 PM" },
{ name: "Dr. Elise Moreau", sub: "Preventive care", rating: "4.7", avail: "Tomorrow, 2:00 PM" },
],
ortho: [
{ name: "Dr. Hannah Cole", sub: "Sports medicine", rating: "4.8", avail: "Wed, 9:45 AM" },
{ name: "Dr. Jonah Kim", sub: "Joint replacement", rating: "4.6", avail: "Thu, 4:30 PM" },
],
mind: [
{
name: "Dr. Amelia Frost",
sub: "Clinical psychology",
rating: "4.9",
avail: "Tomorrow, 5:00 PM",
},
{ name: "Dr. Noah Bauer", sub: "Psychiatry", rating: "4.7", avail: "Fri, 11:00 AM" },
],
};
const DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTH = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
// Fixed seed date: Mon 8 Jun 2026 (no Date.now / Math.random in the lab sandbox).
const BASE = { dow: 1, day: 8, month: 5 };
const MONTH_LEN = 30;
const AM_SLOTS = ["08:30", "09:00", "09:30", "10:00", "10:30", "11:00", "11:30", "12:00"];
const PM_SLOTS = ["13:30", "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", "17:00"];
// Deterministic "unavailable" pattern per day index.
const blocked = (dayIdx, slotIdx) => (dayIdx + slotIdx) % 4 === 0;
// ── State ────────────────────────────────────────────────────────────────────
const state = { step: 1, spec: null, doc: null, dayIdx: null, time: null };
const $ = (id) => document.getElementById(id);
const toast = $("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2600);
}
function dayMeta(i) {
const day = ((BASE.day - 1 + i) % MONTH_LEN) + 1;
const dow = DOW[(BASE.dow + i) % 7];
return { day, dow, label: `${dow}, ${day} ${MONTH[BASE.month]}` };
}
// ── Renderers ────────────────────────────────────────────────────────────────
function renderSpecialties() {
$("specGrid").innerHTML = SPECIALTIES.map(
(s) =>
`<button class="spec-card${state.spec === s.id ? " is-selected" : ""}" data-spec="${s.id}" aria-pressed="${state.spec === s.id}">` +
`<span class="spec-ic" aria-hidden="true">${s.icon}</span>` +
`<span class="spec-name">${s.name}</span><span class="spec-blurb">${s.blurb}</span></button>`
).join("");
}
function renderDoctors() {
const docs = DOCTORS[state.spec] || [];
$("docList").innerHTML = docs
.map((d, i) => {
const sel = state.doc === d.name ? " is-selected" : "";
const initials = d.name
.replace("Dr. ", "")
.split(" ")
.map((w) => w[0])
.join("");
return (
`<li><button class="doc-card${sel}" data-doc="${i}" aria-pressed="${!!sel}">` +
`<span class="doc-avatar" aria-hidden="true">${initials}</span>` +
`<span class="doc-body"><span class="doc-name">${d.name}</span>` +
`<span class="doc-sub">${d.sub}</span>` +
`<span class="doc-avail">Next available · ${d.avail}</span></span>` +
`<span class="doc-rating"><span class="stars" aria-hidden="true">★</span> ${d.rating}</span>` +
`</button></li>`
);
})
.join("");
}
function renderDates() {
let out = "";
for (let i = 1; i <= 7; i++) {
const m = dayMeta(i);
const sel = state.dayIdx === i ? " is-selected" : "";
out += `<button class="date-chip${sel}" data-day="${i}" aria-pressed="${state.dayIdx === i}"><span class="dc-dow">${m.dow}</span><span class="dc-day">${m.day}</span></button>`;
}
$("dateStrip").innerHTML = out;
}
function renderSlots() {
const di = state.dayIdx || 0;
const build = (list, offset) =>
list
.map((t, i) => {
const off = blocked(di, i + offset);
const sel = state.time === t ? " is-selected" : "";
return `<button class="slot${sel}" data-time="${t}"${off ? " disabled" : ""} aria-pressed="${state.time === t}">${t}</button>`;
})
.join("");
$("slotsAm").innerHTML = build(AM_SLOTS, 0);
$("slotsPm").innerHTML = build(PM_SLOTS, 8);
}
function renderSummary() {
const spec = SPECIALTIES.find((s) => s.id === state.spec);
$("sumSpec").textContent = spec ? spec.name : "—";
$("sumDoc").textContent = state.doc || "—";
$("sumDate").textContent = state.dayIdx ? dayMeta(state.dayIdx).label : "—";
$("sumTime").textContent = state.time || "—";
}
function renderStepper() {
document.querySelectorAll(".step").forEach((el) => {
const n = Number(el.dataset.step);
el.classList.toggle("is-current", n === state.step);
el.classList.toggle("is-done", n < state.step || state.step === 4);
});
}
// ── Validation ───────────────────────────────────────────────────────────────
function isValid() {
if (state.step === 1) return !!state.spec;
if (state.step === 2) return !!state.doc;
if (state.step === 3) return !!state.dayIdx && !!state.time;
return false;
}
function render() {
document.querySelectorAll(".panel-step").forEach((p) => (p.hidden = true));
if (state.step === 4) {
document.querySelector('[data-panel="done"]').hidden = false;
} else {
document.querySelector(`[data-panel="${state.step}"]`).hidden = false;
}
renderStepper();
renderSummary();
$("backBtn").hidden = state.step === 1 || state.step === 4;
$("contBtn").hidden = state.step === 4;
$("contBtn").disabled = !isValid();
$("contBtn").textContent = state.step === 3 ? "Confirm booking" : "Continue";
}
// ── Reference (deterministic, derived from selections) ────────────────────────
function makeRef() {
const specIdx = SPECIALTIES.findIndex((s) => s.id === state.spec) + 1;
const docs = DOCTORS[state.spec] || [];
const docIdx = docs.findIndex((d) => d.name === state.doc) + 1;
const slotIdx = [...AM_SLOTS, ...PM_SLOTS].indexOf(state.time) + 1;
const n = ((specIdx * 100000 + docIdx * 10000 + state.dayIdx * 100 + slotIdx) % 1000000) + 100000;
return "APT-" + String(n % 1000000).padStart(6, "0");
}
// ── Events ───────────────────────────────────────────────────────────────────
$("specGrid").addEventListener("click", (e) => {
const b = e.target.closest("[data-spec]");
if (!b) return;
state.spec = b.dataset.spec;
state.doc = null;
renderSpecialties();
render();
});
$("docList").addEventListener("click", (e) => {
const b = e.target.closest("[data-doc]");
if (!b) return;
state.doc = DOCTORS[state.spec][Number(b.dataset.doc)].name;
renderDoctors();
render();
});
$("dateStrip").addEventListener("click", (e) => {
const b = e.target.closest("[data-day]");
if (!b) return;
state.dayIdx = Number(b.dataset.day);
state.time = null;
renderDates();
renderSlots();
render();
});
document.querySelector(".flow").addEventListener("click", (e) => {
const b = e.target.closest(".slot");
if (!b || b.disabled) return;
state.time = b.dataset.time;
renderSlots();
render();
});
$("contBtn").addEventListener("click", () => {
if (!isValid()) return;
if (state.step === 3) {
$("bookingRef").textContent = makeRef();
state.step = 4;
render();
showToast("Appointment confirmed — see you soon!");
return;
}
state.step += 1;
if (state.step === 2) renderDoctors();
if (state.step === 3) {
renderDates();
renderSlots();
}
render();
});
$("backBtn").addEventListener("click", () => {
if (state.step > 1) state.step -= 1;
render();
});
$("calBtn").addEventListener("click", () => showToast("Added to your calendar."));
$("doneBtn").addEventListener("click", () => {
Object.assign(state, { step: 1, spec: null, doc: null, dayIdx: null, time: null });
renderSpecialties();
render();
});
// ── Init ─────────────────────────────────────────────────────────────────────
renderSpecialties();
render();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Book an Appointment · Northbridge Clinic</title>
</head>
<body>
<header class="appbar">
<a class="brand" href="#">
<span class="brand-mark" aria-hidden="true">✚</span>
<span class="brand-name">Northbridge <em>Health</em></span>
</a>
<button class="avatar" aria-label="Account">AM</button>
</header>
<main class="booking">
<div class="head">
<p class="eyebrow">New appointment</p>
<h1>Book a visit</h1>
</div>
<ol class="stepper" id="stepper">
<li class="step is-current" data-step="1">
<span class="step-num">1</span>
<span class="step-label">Specialty</span>
</li>
<li class="step" data-step="2">
<span class="step-num">2</span>
<span class="step-label">Doctor</span>
</li>
<li class="step" data-step="3">
<span class="step-num">3</span>
<span class="step-label">Date & time</span>
</li>
</ol>
<div class="layout">
<section class="flow">
<!-- Step 1: Specialty -->
<div class="panel-step" data-panel="1">
<h2 class="step-title">Choose a specialty</h2>
<div class="spec-grid" id="specGrid"></div>
</div>
<!-- Step 2: Doctor -->
<div class="panel-step" data-panel="2" hidden>
<h2 class="step-title">Choose a doctor</h2>
<ul class="doc-list" id="docList"></ul>
</div>
<!-- Step 3: Date & time -->
<div class="panel-step" data-panel="3" hidden>
<h2 class="step-title">Pick a date & time</h2>
<div class="date-strip" id="dateStrip" role="group" aria-label="Available days"></div>
<p class="slot-head">Morning</p>
<div class="slot-grid" id="slotsAm" role="group" aria-label="Morning slots"></div>
<p class="slot-head">Afternoon</p>
<div class="slot-grid" id="slotsPm" role="group" aria-label="Afternoon slots"></div>
</div>
<!-- Success -->
<div class="panel-step success" data-panel="done" hidden>
<div class="ok-mark" aria-hidden="true">✓</div>
<h2>Appointment confirmed</h2>
<p class="ok-sub">A confirmation has been sent to your patient portal.</p>
<p class="ok-ref">Reference <strong id="bookingRef">APT-000000</strong></p>
<div class="ok-actions">
<button class="btn primary" id="calBtn">Add to calendar</button>
<button class="btn ghost" id="doneBtn">Done</button>
</div>
</div>
</section>
<aside class="summary" aria-label="Booking summary">
<h3 class="sum-title">Your appointment</h3>
<dl class="sum-list">
<div class="sum-row">
<dt>Specialty</dt>
<dd id="sumSpec">—</dd>
</div>
<div class="sum-row">
<dt>Doctor</dt>
<dd id="sumDoc">—</dd>
</div>
<div class="sum-row">
<dt>Date</dt>
<dd id="sumDate">—</dd>
</div>
<div class="sum-row">
<dt>Time</dt>
<dd id="sumTime">—</dd>
</div>
</dl>
<div class="nav">
<button class="btn ghost" id="backBtn" hidden>Back</button>
<button class="btn primary" id="contBtn" disabled>Continue</button>
</div>
<p class="sum-fine">Free cancellation up to 24h before your visit.</p>
</aside>
</div>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Appointment Booking
A calm, clinical-white scheduling flow in teal and soft coral that walks a patient through booking a visit in three steps. A progress stepper tracks Specialty → Doctor → Date & time, marking completed steps with a check and highlighting the current one. Step one offers a grid of specialty cards; step two lists matching doctors with avatar initials, sub-specialty, a star rating and a next-available line; step three pairs a seven-day date strip with a morning/afternoon slot grid where some times are disabled. A persistent summary echoes every choice, Continue stays disabled until the current step is valid, and confirming swaps in a success panel with a deterministic booking reference and an add-to-calendar affordance. All vanilla JS.
Illustrative UI only — not intended for real medical use.