Dental — Patient Portal
A trust-first dental patient portal dashboard with a next-appointment card, animated treatment-history timeline, an outstanding-balance summary with a pay action, a downloadable documents list, and quick actions for rescheduling or messaging the clinic — all built as a clean clinical light interface with rounded cards, soft shadows, badges, live toasts, and a keyboard-accessible reschedule flow.
MCP
Code
:root {
--blue: #2563eb;
--blue-d: #1d4ed8;
--blue-50: #eff5ff;
--mint: #34d399;
--mint-50: #e8fbf3;
--ink: #0f2740;
--ink-2: #3a5169;
--muted: #6b7c90;
--bg: #f3f8ff;
--white: #fff;
--line: rgba(15, 39, 64, 0.1);
--line-2: rgba(15, 39, 64, 0.18);
--ok: #22b07d;
--warn: #d98a2b;
--danger: #e05252;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--shadow: 0 1px 2px rgba(15, 39, 64, 0.04), 0 8px 24px rgba(15, 39, 64, 0.06);
--shadow-lg: 0 20px 60px rgba(15, 39, 64, 0.16);
}
* { 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 600px at 100% -10%, var(--blue-50), transparent 60%),
radial-gradient(900px 500px at -10% 110%, var(--mint-50), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.shell {
max-width: 1100px;
margin: 0 auto;
padding: 22px 20px 56px;
}
/* Top bar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
margin-bottom: 22px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 12px;
color: var(--white);
background: linear-gradient(135deg, var(--blue), var(--blue-d));
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.35);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { font-size: 15px; font-weight: 700; }
.brand-text small { color: var(--muted); font-size: 12px; }
.topbar-right { display: flex; align-items: center; gap: 14px; }
.user { display: flex; align-items: center; gap: 10px; }
.avatar {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
font-size: 13px;
font-weight: 700;
color: var(--blue-d);
background: var(--blue-50);
border: 1px solid var(--line);
}
.user-text { display: flex; flex-direction: column; line-height: 1.15; }
.user-text strong { font-size: 13px; }
.user-text small { color: var(--muted); font-size: 11px; }
.icon-btn {
position: relative;
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 12px;
border: 1px solid var(--line);
background: var(--white);
color: var(--ink-2);
cursor: pointer;
transition: background 0.15s, transform 0.1s, border-color 0.15s;
}
.icon-btn:hover { background: var(--blue-50); border-color: var(--line-2); }
.icon-btn:active { transform: scale(0.94); }
.icon-btn .dot {
position: absolute;
top: 9px;
right: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--danger);
border: 2px solid var(--white);
}
/* Grid */
.grid {
display: grid;
grid-template-columns: 1.55fr 1fr;
gap: 20px;
align-items: start;
}
.col { display: flex; flex-direction: column; gap: 20px; }
/* Welcome */
.welcome {
padding: 20px 24px;
border-radius: var(--r-lg);
background: linear-gradient(120deg, var(--blue), var(--blue-d));
color: var(--white);
box-shadow: 0 16px 40px rgba(37, 99, 235, 0.28);
}
.welcome .eyebrow {
margin: 0 0 4px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.75);
}
.welcome h1 { margin: 0 0 6px; font-size: 22px; font-weight: 800; letter-spacing: -0.02em; }
.welcome .sub { margin: 0; font-size: 14px; color: rgba(255, 255, 255, 0.9); }
/* Cards */
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 20px 22px;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.card-head h2 { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
/* Badges */
.badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 600;
white-space: nowrap;
}
.badge-blue { color: var(--blue-d); background: var(--blue-50); }
.badge-ok { color: var(--ok); background: var(--mint-50); }
.badge-warn { color: var(--warn); background: rgba(217, 138, 43, 0.12); }
.badge-paid { color: var(--ok); background: var(--mint-50); }
/* Appointment card */
.appt-card { position: relative; overflow: hidden; }
.appt-card::before {
content: "";
position: absolute;
inset: 0 0 auto auto;
width: 220px;
height: 220px;
background: radial-gradient(circle at top right, var(--mint-50), transparent 70%);
pointer-events: none;
}
.appt-body { display: flex; gap: 20px; align-items: center; }
.appt-date {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 92px;
padding: 14px 10px;
border-radius: var(--r-md);
background: linear-gradient(160deg, var(--blue-50), #fff);
border: 1px solid var(--line);
}
.appt-month { font-size: 12px; font-weight: 700; letter-spacing: 0.12em; color: var(--blue-d); }
.appt-day { font-size: 38px; font-weight: 800; line-height: 1; color: var(--ink); }
.appt-year { font-size: 12px; color: var(--muted); }
.appt-detail { flex: 1; }
.appt-type { margin: 0 0 10px; font-size: 16px; font-weight: 700; }
.appt-meta { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px; }
.appt-meta li { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--ink-2); }
.appt-meta svg { color: var(--blue); flex: none; }
.appt-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
padding-top: 16px;
border-top: 1px dashed var(--line-2);
}
/* Buttons */
.btn {
appearance: none;
border: 1px solid transparent;
border-radius: 999px;
padding: 9px 16px;
font: inherit;
font-size: 13.5px;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s, background 0.15s, box-shadow 0.15s, border-color 0.15s;
}
.btn:active { transform: translateY(1px); }
.btn-primary { color: var(--white); background: var(--blue); box-shadow: 0 8px 18px rgba(37, 99, 235, 0.28); }
.btn-primary:hover { background: var(--blue-d); }
.btn-ghost { color: var(--ink-2); background: var(--white); border-color: var(--line-2); }
.btn-ghost:hover { background: var(--blue-50); color: var(--blue-d); }
.btn-block { width: 100%; margin-top: 14px; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }
.link-btn {
background: none;
border: none;
color: var(--blue);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
padding: 4px 6px;
border-radius: 8px;
}
.link-btn:hover { background: var(--blue-50); }
/* Timeline */
.timeline { list-style: none; margin: 0; padding: 0; position: relative; }
.timeline::before {
content: "";
position: absolute;
left: 7px;
top: 6px;
bottom: 6px;
width: 2px;
background: var(--line);
}
.tl-item { position: relative; padding: 0 0 18px 30px; }
.tl-item:last-child { padding-bottom: 0; }
.tl-item.is-hidden { display: none; }
.tl-dot {
position: absolute;
left: 0;
top: 4px;
width: 16px;
height: 16px;
border-radius: 50%;
border: 3px solid var(--white);
box-shadow: 0 0 0 1px var(--line);
}
.tl-ok { background: var(--ok); }
.tl-warn { background: var(--warn); }
.tl-body { display: flex; flex-direction: column; gap: 2px; }
.tl-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.tl-row strong { font-size: 14px; }
.tl-body small { color: var(--muted); font-size: 12px; }
.tl-body p { margin: 4px 0 0; font-size: 13px; color: var(--ink-2); }
/* Balance */
.balance-card {
background: linear-gradient(165deg, #fff, var(--blue-50));
}
.balance-amount { margin: 4px 0 2px; font-size: 34px; font-weight: 800; letter-spacing: -0.02em; }
.balance-note { margin: 0; font-size: 12.5px; color: var(--muted); }
.balance-bar {
margin-top: 14px;
height: 8px;
border-radius: 999px;
background: var(--line);
overflow: hidden;
}
.balance-bar span {
display: block;
height: 100%;
width: 62%;
border-radius: 999px;
background: linear-gradient(90deg, var(--warn), #f0a94a);
transition: width 0.6s ease, background 0.4s ease;
}
/* Quick actions */
.quick-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.quick {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 8px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--white);
color: var(--ink);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: transform 0.12s, box-shadow 0.15s, border-color 0.15s, background 0.15s;
}
.quick:hover { transform: translateY(-2px); border-color: var(--blue); background: var(--blue-50); box-shadow: var(--shadow); }
.quick:active { transform: translateY(0); }
.quick-ic { font-size: 22px; }
/* Documents */
.docs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.doc {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: var(--r-md);
border: 1px solid var(--line);
transition: background 0.15s, border-color 0.15s;
}
.doc:hover { background: var(--blue-50); border-color: var(--line-2); }
.doc-ic {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 10px;
font-size: 18px;
background: var(--blue-50);
flex: none;
}
.doc-text { flex: 1; display: flex; flex-direction: column; line-height: 1.2; }
.doc-text strong { font-size: 13.5px; }
.doc-text small { color: var(--muted); font-size: 12px; }
.doc.is-new { position: relative; }
.doc.is-new::after {
content: "New";
position: absolute;
top: 8px;
right: 62px;
font-size: 10px;
font-weight: 700;
color: var(--mint);
color: var(--ok);
}
.doc-btn {
border: 1px solid var(--line-2);
background: var(--white);
color: var(--blue-d);
border-radius: 999px;
padding: 6px 14px;
font: inherit;
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.doc-btn:hover { background: var(--blue); color: var(--white); border-color: var(--blue); }
.doc-btn:active { transform: scale(0.95); }
.doc-btn.is-viewed { background: var(--mint-50); color: var(--ok); border-color: transparent; cursor: default; }
/* Modals */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(15, 39, 64, 0.45);
backdrop-filter: blur(4px);
display: grid;
place-items: center;
padding: 20px;
z-index: 50;
animation: fade 0.2s ease;
}
.modal-overlay[hidden] { display: none; }
.modal {
width: min(460px, 100%);
background: var(--white);
border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
padding: 22px 24px;
animation: pop 0.22s cubic-bezier(0.2, 0.8, 0.3, 1.1);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
.modal-head h2 { margin: 0; font-size: 17px; font-weight: 700; }
.modal-sub { margin: 2px 0 16px; font-size: 13px; color: var(--muted); }
.slots { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 18px; }
.slot {
display: flex;
flex-direction: column;
gap: 2px;
padding: 12px;
border-radius: var(--r-md);
border: 1.5px solid var(--line);
background: var(--white);
color: var(--ink);
font: inherit;
text-align: left;
cursor: pointer;
transition: border-color 0.15s, background 0.15s, transform 0.1s;
}
.slot strong { font-size: 13.5px; }
.slot small { font-size: 12px; color: var(--muted); }
.slot:hover { border-color: var(--blue); background: var(--blue-50); }
.slot[aria-selected="true"] { border-color: var(--blue); background: var(--blue-50); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
.field-label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
.textarea {
width: 100%;
border: 1.5px solid var(--line);
border-radius: var(--r-md);
padding: 12px;
font: inherit;
font-size: 14px;
resize: vertical;
margin-bottom: 18px;
color: var(--ink);
}
.textarea:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); }
/* Toast */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 80;
width: max-content;
max-width: 92vw;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-radius: 999px;
background: var(--ink);
color: var(--white);
font-size: 13.5px;
font-weight: 500;
box-shadow: var(--shadow-lg);
animation: toastIn 0.28s cubic-bezier(0.2, 0.8, 0.3, 1.1);
}
.toast.out { animation: toastOut 0.25s ease forwards; }
.toast .tick {
display: grid;
place-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--mint);
color: var(--ink);
font-size: 12px;
font-weight: 800;
flex: none;
}
/* Focus */
:focus-visible {
outline: 3px solid rgba(37, 99, 235, 0.45);
outline-offset: 2px;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes pop { from { opacity: 0; transform: translateY(12px) scale(0.97); } to { opacity: 1; transform: none; } }
@keyframes toastIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateY(16px); } }
/* Responsive */
@media (max-width: 860px) {
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.shell { padding: 14px 12px 44px; }
.topbar { flex-direction: column; align-items: stretch; gap: 12px; border-radius: var(--r-md); }
.topbar-right { justify-content: space-between; }
.welcome h1 { font-size: 19px; }
.appt-body { flex-direction: column; align-items: stretch; gap: 14px; }
.appt-date { flex-direction: row; gap: 8px; justify-content: center; min-width: 0; }
.appt-day { font-size: 26px; }
.appt-actions .btn { flex: 1; }
.slots { grid-template-columns: 1fr; }
.balance-amount { font-size: 28px; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastWrap = document.getElementById("toastWrap");
function toast(msg, opts) {
opts = opts || {};
var el = document.createElement("div");
el.className = "toast";
el.setAttribute("role", "status");
if (opts.tick !== false) {
var t = document.createElement("span");
t.className = "tick";
t.textContent = "✓";
el.appendChild(t);
}
var span = document.createElement("span");
span.textContent = msg;
el.appendChild(span);
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 260);
}, opts.duration || 2600);
}
/* ---------- Focus trap utility ---------- */
function trapFocus(container) {
var focusables = container.querySelectorAll(
'button, [href], input, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusables.length) return function () {};
var first = focusables[0];
var last = focusables[focusables.length - 1];
function onKey(e) {
if (e.key !== "Tab") return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
container.addEventListener("keydown", onKey);
return function () {
container.removeEventListener("keydown", onKey);
};
}
/* ---------- Reschedule modal ---------- */
var modal = document.getElementById("modal");
var slots = document.getElementById("slots");
var confirmBtn = document.getElementById("modalConfirm");
var selectedSlot = null;
var lastFocused = null;
var releaseTrap = null;
function openModal() {
lastFocused = document.activeElement;
modal.hidden = false;
selectedSlot = null;
confirmBtn.disabled = true;
Array.prototype.forEach.call(slots.children, function (s) {
s.setAttribute("aria-selected", "false");
});
releaseTrap = trapFocus(modal);
document.getElementById("modalClose").focus();
document.addEventListener("keydown", escClose);
}
function closeModal() {
modal.hidden = true;
if (releaseTrap) releaseTrap();
document.removeEventListener("keydown", escClose);
if (lastFocused) lastFocused.focus();
}
function escClose(e) {
if (e.key === "Escape") closeModal();
}
Array.prototype.forEach.call(slots.children, function (slot) {
slot.addEventListener("click", function () {
Array.prototype.forEach.call(slots.children, function (s) {
s.setAttribute("aria-selected", "false");
});
slot.setAttribute("aria-selected", "true");
selectedSlot = slot;
confirmBtn.disabled = false;
});
});
confirmBtn.addEventListener("click", function () {
if (!selectedSlot) return;
var day = selectedSlot.getAttribute("data-day");
var label = selectedSlot.getAttribute("data-label");
var parts = label.split(" · ");
document.getElementById("apptDay").textContent = day;
document.getElementById("apptTime").textContent = parts[1] + " · 45 min";
var status = document.getElementById("apptStatus");
status.textContent = "Rescheduled";
status.className = "badge badge-warn";
closeModal();
toast("Appointment moved to " + label);
});
document.getElementById("rescheduleBtn").addEventListener("click", openModal);
document.getElementById("modalClose").addEventListener("click", closeModal);
document.getElementById("modalCancel").addEventListener("click", closeModal);
modal.addEventListener("click", function (e) {
if (e.target === modal) closeModal();
});
/* ---------- Message modal ---------- */
var msgModal = document.getElementById("msgModal");
var msgText = document.getElementById("msgText");
var releaseMsgTrap = null;
var msgLastFocused = null;
function openMsg() {
msgLastFocused = document.activeElement;
msgModal.hidden = false;
releaseMsgTrap = trapFocus(msgModal);
msgText.focus();
document.addEventListener("keydown", escMsg);
}
function closeMsg() {
msgModal.hidden = true;
if (releaseMsgTrap) releaseMsgTrap();
document.removeEventListener("keydown", escMsg);
if (msgLastFocused) msgLastFocused.focus();
}
function escMsg(e) {
if (e.key === "Escape") closeMsg();
}
document.getElementById("messageBtn").addEventListener("click", openMsg);
document.getElementById("msgClose").addEventListener("click", closeMsg);
document.getElementById("msgCancel").addEventListener("click", closeMsg);
msgModal.addEventListener("click", function (e) {
if (e.target === msgModal) closeMsg();
});
document.getElementById("msgSend").addEventListener("click", function () {
var val = msgText.value.trim();
if (!val) {
toast("Please write a message first", { tick: false });
msgText.focus();
return;
}
msgText.value = "";
closeMsg();
toast("Message sent to the care team");
});
/* ---------- Pay balance ---------- */
var payBtn = document.getElementById("payBtn");
var balAmount = document.getElementById("balAmount");
var balBadge = document.getElementById("balBadge");
var balFill = document.getElementById("balFill");
var balNote = document.getElementById("balNote");
var paid = false;
payBtn.addEventListener("click", function () {
if (paid) return;
paid = true;
payBtn.disabled = true;
var start = 184;
var current = start;
var step = start / 28;
var timer = setInterval(function () {
current -= step;
if (current <= 0) {
current = 0;
clearInterval(timer);
balBadge.textContent = "Paid";
balBadge.className = "badge badge-paid";
balNote.textContent = "Paid in full on " + formatToday() + " · Thank you!";
payBtn.textContent = "Balance cleared";
toast("Payment of $184.00 received");
}
balAmount.textContent = "$" + current.toFixed(2);
}, 22);
balFill.style.width = "0%";
balFill.style.background = "linear-gradient(90deg, #22b07d, #34d399)";
});
function formatToday() {
var d = new Date();
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
return months[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear();
}
/* ---------- Timeline expand ---------- */
var expandBtn = document.getElementById("expandHistory");
var expanded = false;
expandBtn.addEventListener("click", function () {
expanded = !expanded;
var hidden = document.querySelectorAll("#timeline .tl-item");
Array.prototype.forEach.call(hidden, function (item) {
if (item.classList.contains("is-hidden") || item.dataset.wasHidden) {
item.style.display = expanded ? "" : "none";
if (expanded) {
item.classList.remove("is-hidden");
item.dataset.wasHidden = "1";
} else {
item.classList.add("is-hidden");
}
}
});
expandBtn.textContent = expanded ? "Show less" : "Show all";
});
/* ---------- Documents view ---------- */
var docCount = document.getElementById("docCount");
var newDocs = 2;
Array.prototype.forEach.call(document.querySelectorAll(".doc-btn"), function (btn) {
btn.addEventListener("click", function () {
if (btn.classList.contains("is-viewed")) return;
var doc = btn.closest(".doc");
btn.textContent = "Viewed";
btn.classList.add("is-viewed");
if (doc.classList.contains("is-new")) {
doc.classList.remove("is-new");
newDocs = Math.max(0, newDocs - 1);
if (newDocs === 0) {
docCount.textContent = "All read";
docCount.className = "badge badge-ok";
} else {
docCount.textContent = newDocs + " new";
}
}
var name = doc.querySelector("strong").textContent;
toast("Opened “" + name + "”");
});
});
/* ---------- Quick actions ---------- */
Array.prototype.forEach.call(document.querySelectorAll(".quick"), function (q) {
q.addEventListener("click", function () {
var action = q.getAttribute("data-action");
if (action === "reschedule") return openModal();
if (action === "message") return openMsg();
if (action === "records") return toast("Opening your dental records");
if (action === "refill") return toast("Refill request sent to Dr. Nadeem");
});
});
/* ---------- Misc top-bar ---------- */
document.getElementById("calendarBtn").addEventListener("click", function () {
toast("Added to your calendar");
this.querySelector ? null : null;
});
document.getElementById("bellBtn").addEventListener("click", function () {
this.querySelector(".dot").style.display = "none";
toast("You're all caught up", { tick: false });
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northlake Dental — Patient Portal</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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="shell">
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5.5c-1.6-1.6-4.3-1.7-6-.2C4.2 6.9 4 9.7 5 12c.6 1.4.7 3 .9 4.6.2 1.4.4 2.9 1.3 2.9.8 0 1-1.2 1.2-2.5.2-1.3.4-2.5 1.6-2.5s1.4 1.2 1.6 2.5c.2 1.3.4 2.5 1.2 2.5.9 0 1.1-1.5 1.3-2.9.2-1.6.3-3.2.9-4.6 1-2.3.8-5.1-1-6.7-1.7-1.5-4.4-1.4-6 .2Z"/></svg>
</span>
<div class="brand-text">
<strong>Northlake Dental</strong>
<small>Patient Portal</small>
</div>
</div>
<div class="topbar-right">
<button class="icon-btn" id="bellBtn" aria-label="Notifications">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>
<span class="dot" aria-hidden="true"></span>
</button>
<div class="user">
<span class="avatar" aria-hidden="true">ME</span>
<div class="user-text">
<strong>Mara Ellison</strong>
<small>ID · NL-40912</small>
</div>
</div>
</div>
</header>
<main class="grid">
<!-- LEFT COLUMN -->
<div class="col">
<!-- Welcome -->
<section class="welcome" aria-labelledby="welcomeTitle">
<div>
<p class="eyebrow">Good afternoon</p>
<h1 id="welcomeTitle">Hi Mara, your smile is on track ✨</h1>
<p class="sub">You have 1 upcoming visit and 2 documents ready to review.</p>
</div>
</section>
<!-- Next appointment -->
<section class="card appt-card" aria-labelledby="apptTitle">
<div class="card-head">
<h2 id="apptTitle">Next appointment</h2>
<span class="badge badge-blue" id="apptStatus">Confirmed</span>
</div>
<div class="appt-body">
<div class="appt-date">
<span class="appt-month">JUL</span>
<span class="appt-day" id="apptDay">14</span>
<span class="appt-year">2026</span>
</div>
<div class="appt-detail">
<p class="appt-type" id="apptType">Routine cleaning & check-up</p>
<ul class="appt-meta">
<li>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
<span id="apptTime">9:30 AM · 45 min</span>
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>
<span>Northlake Clinic · Chair 3</span>
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>
<span>Dr. Priya Nadeem, DDS</span>
</li>
</ul>
</div>
</div>
<div class="appt-actions">
<button class="btn btn-primary" id="rescheduleBtn">Reschedule</button>
<button class="btn btn-ghost" id="calendarBtn">Add to calendar</button>
<button class="btn btn-ghost" id="messageBtn">Message clinic</button>
</div>
</section>
<!-- Treatment history -->
<section class="card" aria-labelledby="histTitle">
<div class="card-head">
<h2 id="histTitle">Treatment history</h2>
<button class="link-btn" id="expandHistory">Show all</button>
</div>
<ol class="timeline" id="timeline">
<li class="tl-item">
<span class="tl-dot tl-ok" aria-hidden="true"></span>
<div class="tl-body">
<div class="tl-row">
<strong>Composite filling · Tooth #19</strong>
<span class="badge badge-ok">Completed</span>
</div>
<small>Mar 3, 2026 · Dr. Priya Nadeem</small>
<p>Small occlusal cavity restored. No follow-up needed.</p>
</div>
</li>
<li class="tl-item">
<span class="tl-dot tl-ok" aria-hidden="true"></span>
<div class="tl-body">
<div class="tl-row">
<strong>Scaling & polish</strong>
<span class="badge badge-ok">Completed</span>
</div>
<small>Jan 8, 2026 · Hygienist L. Ortega</small>
<p>Routine hygiene visit. Mild gingival inflammation noted.</p>
</div>
</li>
<li class="tl-item is-hidden">
<span class="tl-dot tl-warn" aria-hidden="true"></span>
<div class="tl-body">
<div class="tl-row">
<strong>Whitening consult</strong>
<span class="badge badge-warn">Advised</span>
</div>
<small>Oct 21, 2025 · Dr. J. Fenwick</small>
<p>Discussed at-home trays. Awaiting patient decision.</p>
</div>
</li>
<li class="tl-item is-hidden">
<span class="tl-dot tl-ok" aria-hidden="true"></span>
<div class="tl-body">
<div class="tl-row">
<strong>Panoramic X-ray</strong>
<span class="badge badge-ok">Completed</span>
</div>
<small>Oct 21, 2025 · Radiology</small>
<p>Full-mouth imaging. Wisdom teeth monitoring ongoing.</p>
</div>
</li>
</ol>
</section>
</div>
<!-- RIGHT COLUMN -->
<div class="col">
<!-- Balance -->
<section class="card balance-card" aria-labelledby="balTitle">
<div class="card-head">
<h2 id="balTitle">Outstanding balance</h2>
<span class="badge badge-warn" id="balBadge">Due Jul 20</span>
</div>
<p class="balance-amount" id="balAmount">$184.00</p>
<p class="balance-note" id="balNote">Statement issued Jun 22, 2026 · Insurance applied</p>
<div class="balance-bar" aria-hidden="true"><span id="balFill"></span></div>
<button class="btn btn-primary btn-block" id="payBtn">Pay balance</button>
</section>
<!-- Quick actions -->
<section class="card" aria-labelledby="qaTitle">
<div class="card-head"><h2 id="qaTitle">Quick actions</h2></div>
<div class="quick-grid">
<button class="quick" data-action="reschedule">
<span class="quick-ic" aria-hidden="true">📅</span>
<span>Reschedule</span>
</button>
<button class="quick" data-action="message">
<span class="quick-ic" aria-hidden="true">💬</span>
<span>Message</span>
</button>
<button class="quick" data-action="records">
<span class="quick-ic" aria-hidden="true">📁</span>
<span>Records</span>
</button>
<button class="quick" data-action="refill">
<span class="quick-ic" aria-hidden="true">💊</span>
<span>Refill Rx</span>
</button>
</div>
</section>
<!-- Documents -->
<section class="card" aria-labelledby="docTitle">
<div class="card-head">
<h2 id="docTitle">Documents</h2>
<span class="badge badge-blue" id="docCount">2 new</span>
</div>
<ul class="docs" id="docs">
<li class="doc is-new">
<span class="doc-ic" aria-hidden="true">🧾</span>
<div class="doc-text">
<strong>Statement · June 2026</strong>
<small>PDF · 84 KB</small>
</div>
<button class="doc-btn">View</button>
</li>
<li class="doc is-new">
<span class="doc-ic" aria-hidden="true">🩻</span>
<div class="doc-text">
<strong>Panoramic X-ray report</strong>
<small>PDF · 1.2 MB</small>
</div>
<button class="doc-btn">View</button>
</li>
<li class="doc">
<span class="doc-ic" aria-hidden="true">📄</span>
<div class="doc-text">
<strong>Treatment plan · Whitening</strong>
<small>PDF · 210 KB</small>
</div>
<button class="doc-btn">View</button>
</li>
</ul>
</section>
</div>
</main>
</div>
<!-- Reschedule modal -->
<div class="modal-overlay" id="modal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal-head">
<h2 id="modalTitle">Reschedule appointment</h2>
<button class="icon-btn" id="modalClose" aria-label="Close dialog">✕</button>
</div>
<p class="modal-sub">Pick a new time with Dr. Priya Nadeem. Current: <strong>Jul 14, 9:30 AM</strong>.</p>
<div class="slots" id="slots" role="listbox" aria-label="Available time slots">
<button class="slot" role="option" data-day="16" data-label="Jul 16 · 11:00 AM">
<strong>Wed, Jul 16</strong><small>11:00 AM</small>
</button>
<button class="slot" role="option" data-day="18" data-label="Jul 18 · 2:15 PM">
<strong>Fri, Jul 18</strong><small>2:15 PM</small>
</button>
<button class="slot" role="option" data-day="22" data-label="Jul 22 · 8:45 AM">
<strong>Tue, Jul 22</strong><small>8:45 AM</small>
</button>
<button class="slot" role="option" data-day="24" data-label="Jul 24 · 4:30 PM">
<strong>Thu, Jul 24</strong><small>4:30 PM</small>
</button>
</div>
<div class="modal-actions">
<button class="btn btn-ghost" id="modalCancel">Cancel</button>
<button class="btn btn-primary" id="modalConfirm" disabled>Confirm new time</button>
</div>
</div>
</div>
<!-- Message composer -->
<div class="modal-overlay" id="msgModal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="msgTitle">
<div class="modal-head">
<h2 id="msgTitle">Message the clinic</h2>
<button class="icon-btn" id="msgClose" aria-label="Close dialog">✕</button>
</div>
<p class="modal-sub">Our care team usually replies within one business day.</p>
<label class="field-label" for="msgText">Your message</label>
<textarea id="msgText" class="textarea" rows="4" placeholder="e.g. I've had some sensitivity on my lower left side…"></textarea>
<div class="modal-actions">
<button class="btn btn-ghost" id="msgCancel">Cancel</button>
<button class="btn btn-primary" id="msgSend">Send message</button>
</div>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>The Patient Portal gives a fictional patient, Mara Ellison, a calm single-screen overview of their care at Northlake Dental. A prominent next-appointment card leads with the date, provider, and chair, backed by quick actions to reschedule, add the visit to a calendar, or message the front desk. A vertical treatment-history timeline records past cleanings, fillings, and consults with status badges, while a balance card summarizes the outstanding amount and last statement.
Interactions are handled entirely in vanilla JavaScript. The reschedule button opens an accessible modal with keyboard focus trapping and selectable time slots; confirming a new slot updates the appointment card in place and fires a toast. The Pay balance action animates the amount down to zero and flips the badge to Paid, documents can be marked as viewed, and a message composer sends a simulated note to the care team. Every control is keyboard-operable with visible focus rings and ARIA labelling.
The layout is responsive down to roughly 360px, collapsing the two-column grid into a single stack, and uses only CSS gradient blocks and system-safe imagery so nothing is hotlinked. Data is realistic but clearly fictional.
Illustrative UI only — not intended for real medical use.