Clinic — Video Consult Room
A telemedicine video consult room with a full-bleed main stage, a draggable-feel self-view PiP, circular call controls, side chat / notes / info tabs, a live call timer, and a graceful end-call flow.
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;
}
/* ── Room shell ── */
.room {
height: 100vh;
display: grid;
grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr;
grid-template-areas:
"bar bar"
"main side";
}
/* ── Call bar ── */
.callbar {
grid-area: bar;
background: var(--ink);
color: #fff;
padding: 12px 22px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
z-index: 5;
}
.cb-left {
display: flex;
align-items: center;
gap: 12px;
}
.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;
flex-shrink: 0;
}
.cb-meta {
line-height: 1.25;
}
.cb-title {
font-size: 0.95rem;
font-weight: 700;
letter-spacing: -0.01em;
}
.cb-sub {
font-size: 0.78rem;
color: rgba(255, 255, 255, 0.6);
}
.cb-center {
display: inline-flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 999px;
padding: 6px 14px;
}
.live-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--coral);
box-shadow: 0 0 0 3px rgba(255, 122, 102, 0.25);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.cb-timer {
font-variant-numeric: tabular-nums;
font-weight: 700;
font-size: 0.92rem;
letter-spacing: 0.02em;
}
.cb-right {
display: flex;
align-items: center;
}
.enc-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.76rem;
font-weight: 600;
color: #9be8c6;
background: rgba(47, 158, 111, 0.18);
border: 1px solid rgba(47, 158, 111, 0.3);
border-radius: 999px;
padding: 5px 11px;
}
/* ── Stage ── */
.stage-wrap {
grid-area: main;
background: var(--ink);
display: flex;
flex-direction: column;
padding: 18px 18px 22px;
min-height: 0;
}
.stage {
position: relative;
flex: 1;
border-radius: var(--r-lg);
overflow: hidden;
min-height: 0;
}
.stage-tile {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: radial-gradient(circle at 30% 25%, rgba(18, 156, 147, 0.45), transparent 55%),
linear-gradient(150deg, #123c39, #0a2724 70%);
}
.big-avatar {
width: 132px;
height: 132px;
border-radius: 50%;
display: grid;
place-items: center;
background: linear-gradient(150deg, var(--teal), var(--teal-700));
color: #fff;
font-size: 2.8rem;
font-weight: 800;
letter-spacing: 0.02em;
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.06), var(--shadow-2);
}
.remote-label {
position: absolute;
left: 18px;
bottom: 18px;
display: flex;
flex-direction: column;
background: rgba(10, 39, 36, 0.6);
backdrop-filter: blur(6px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--r-sm);
padding: 8px 14px;
}
.rl-name {
color: #fff;
font-weight: 700;
font-size: 0.95rem;
}
.rl-role {
color: rgba(255, 255, 255, 0.65);
font-size: 0.76rem;
}
.quality {
position: absolute;
top: 16px;
right: 16px;
display: flex;
align-items: flex-end;
gap: 3px;
height: 22px;
background: rgba(10, 39, 36, 0.55);
border-radius: var(--r-sm);
padding: 5px 8px;
}
.q-bar {
width: 4px;
border-radius: 2px;
background: var(--ok);
}
.q-bar:nth-child(1) {
height: 6px;
}
.q-bar:nth-child(2) {
height: 10px;
}
.q-bar:nth-child(3) {
height: 14px;
}
.q-bar:nth-child(4) {
height: 18px;
}
.q-dim {
background: rgba(255, 255, 255, 0.22);
}
.quality.is-weak .q-bar {
background: var(--warn);
}
.quality.is-weak .q-bar:nth-child(3),
.quality.is-weak .q-bar:nth-child(4) {
background: rgba(255, 255, 255, 0.22);
}
/* ── Self-view PiP ── */
.selfview {
position: absolute;
right: 18px;
bottom: 18px;
width: 168px;
height: 116px;
border-radius: var(--r-md);
overflow: hidden;
display: grid;
place-items: center;
background: radial-gradient(circle at 70% 30%, rgba(255, 122, 102, 0.4), transparent 55%),
linear-gradient(150deg, #1c4a45, #0d322e);
border: 2px solid rgba(255, 255, 255, 0.14);
box-shadow: var(--shadow-2);
}
.sv-avatar {
width: 52px;
height: 52px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--coral);
color: #fff;
font-weight: 800;
font-size: 1.1rem;
}
.sv-label {
position: absolute;
left: 8px;
bottom: 7px;
color: #fff;
font-size: 0.72rem;
font-weight: 600;
background: rgba(10, 39, 36, 0.55);
border-radius: 6px;
padding: 2px 7px;
}
.sv-mute {
position: absolute;
right: 8px;
bottom: 7px;
font-size: 0.9rem;
background: rgba(212, 80, 62, 0.9);
border-radius: 6px;
padding: 2px 6px;
}
.sv-camoff {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
background: #0d322e;
color: rgba(255, 255, 255, 0.7);
font-size: 0.74rem;
font-weight: 600;
}
.sv-camoff span:first-child {
font-size: 1.4rem;
opacity: 0.7;
}
/* ── Ended overlay ── */
.ended-overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
background: rgba(10, 39, 36, 0.82);
backdrop-filter: blur(4px);
z-index: 3;
}
.eo-card {
text-align: center;
color: #fff;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.eo-icon {
width: 64px;
height: 64px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(47, 158, 111, 0.22);
border: 1px solid rgba(47, 158, 111, 0.45);
color: #9be8c6;
font-size: 1.7rem;
margin-bottom: 6px;
}
.eo-title {
font-size: 1.25rem;
font-weight: 800;
}
.eo-dur {
color: rgba(255, 255, 255, 0.7);
font-size: 0.9rem;
}
.eo-btn {
margin-top: 14px;
border: none;
background: var(--teal);
color: #fff;
font: inherit;
font-weight: 600;
font-size: 0.9rem;
border-radius: 11px;
padding: 11px 22px;
cursor: pointer;
transition: background 0.15s;
}
.eo-btn:hover {
background: var(--teal-d);
}
/* ── Controls ── */
.controls {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
padding-top: 18px;
}
.ctl {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.78);
font: inherit;
cursor: pointer;
}
.ctl-ic {
width: 56px;
height: 56px;
display: grid;
place-items: center;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.14);
font-size: 1.3rem;
transition: background 0.15s, transform 0.12s, border-color 0.15s;
}
.ctl:hover .ctl-ic {
background: rgba(255, 255, 255, 0.18);
}
.ctl:active .ctl-ic {
transform: translateY(1px);
}
.ctl-lbl {
font-size: 0.74rem;
font-weight: 600;
}
.ctl.is-off .ctl-ic {
background: rgba(212, 80, 62, 0.92);
border-color: rgba(212, 80, 62, 0.92);
color: #fff;
}
.ctl.is-active .ctl-ic {
background: var(--teal);
border-color: var(--teal);
color: #fff;
}
.ctl-end .ctl-ic {
background: var(--danger);
border-color: var(--danger);
color: #fff;
width: 64px;
height: 64px;
transform: rotate(135deg);
}
.ctl-end .ctl-lbl {
color: #ff9f93;
}
.ctl-end:hover .ctl-ic {
background: #c0432f;
}
/* ── Side panel ── */
.side {
grid-area: side;
background: var(--white);
border-left: 1px solid var(--line);
display: flex;
flex-direction: column;
min-height: 0;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--line);
padding: 0 10px;
}
.tab {
flex: 1;
border: none;
background: transparent;
font: inherit;
font-weight: 600;
font-size: 0.88rem;
color: var(--muted);
padding: 14px 0;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: color 0.15s, border-color 0.15s;
}
.tab:hover {
color: var(--ink-2);
}
.tab.is-active {
color: var(--teal-d);
border-bottom-color: var(--teal);
}
.tabpanel {
display: none;
flex: 1;
min-height: 0;
flex-direction: column;
}
.tabpanel.is-active {
display: flex;
}
/* ── Chat ── */
.thread {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.msg {
display: flex;
flex-direction: column;
max-width: 84%;
}
.msg.in {
align-self: flex-start;
}
.msg.out {
align-self: flex-end;
align-items: flex-end;
}
.msg-who {
font-size: 0.7rem;
font-weight: 600;
color: var(--muted);
margin: 0 4px 3px;
}
.bubble {
padding: 9px 13px;
border-radius: 14px;
font-size: 0.88rem;
line-height: 1.4;
}
.msg.in .bubble {
background: var(--teal-50);
color: var(--ink);
border-bottom-left-radius: 4px;
}
.msg.out .bubble {
background: var(--teal-d);
color: #fff;
border-bottom-right-radius: 4px;
}
.typing {
display: flex;
align-items: center;
gap: 8px;
padding: 0 16px 8px;
font-size: 0.76rem;
color: var(--muted);
}
.t-dots {
display: inline-flex;
gap: 3px;
}
.t-dots i {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--muted);
animation: blink 1.2s infinite ease-in-out;
}
.t-dots i:nth-child(2) {
animation-delay: 0.2s;
}
.t-dots i:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0%,
60%,
100% {
opacity: 0.25;
}
30% {
opacity: 1;
}
}
.composer {
display: flex;
gap: 8px;
padding: 12px 14px;
border-top: 1px solid var(--line);
}
.composer-input {
flex: 1;
border: 1px solid var(--line-2);
border-radius: 11px;
padding: 10px 13px;
font: inherit;
font-size: 0.88rem;
color: var(--ink);
outline: none;
transition: border-color 0.15s;
}
.composer-input:focus {
border-color: var(--teal);
}
.send-btn {
width: 42px;
border: none;
border-radius: 11px;
background: var(--teal);
color: #fff;
font-size: 1rem;
cursor: pointer;
transition: background 0.15s;
}
.send-btn:hover {
background: var(--teal-d);
}
/* ── Notes ── */
#panel-notes {
padding: 16px;
gap: 8px;
}
.notes-label {
font-size: 0.8rem;
font-weight: 700;
color: var(--ink-2);
}
.notes-area {
flex: 1;
resize: none;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 12px;
font: inherit;
font-size: 0.88rem;
color: var(--ink);
outline: none;
line-height: 1.5;
transition: border-color 0.15s;
}
.notes-area:focus {
border-color: var(--teal);
}
.notes-hint {
font-size: 0.74rem;
color: var(--muted);
}
/* ── Info ── */
#panel-info {
padding: 16px;
gap: 16px;
overflow-y: auto;
}
.info-head {
display: flex;
align-items: center;
gap: 12px;
}
.info-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--coral);
color: #fff;
font-weight: 800;
font-size: 1rem;
}
.info-name {
font-size: 1rem;
font-weight: 700;
}
.info-meta {
font-size: 0.8rem;
color: var(--muted);
}
.info-list {
display: flex;
flex-direction: column;
}
.info-row {
padding: 11px 0;
border-bottom: 1px solid var(--line);
}
.info-row:last-child {
border-bottom: none;
}
.info-row dt {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
color: var(--muted);
margin-bottom: 3px;
}
.info-row dd {
font-size: 0.9rem;
font-weight: 500;
color: var(--ink);
}
.chip {
display: inline-block;
font-size: 0.76rem;
font-weight: 700;
padding: 3px 10px;
border-radius: 999px;
}
.chip.danger {
background: rgba(212, 80, 62, 0.14);
color: var(--danger);
}
/* ── 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) {
.room {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr 300px;
grid-template-areas:
"bar"
"main"
"side";
}
.side {
border-left: none;
border-top: 1px solid var(--line);
}
.selfview {
width: 128px;
height: 90px;
}
}
/* Visibility guard: honor the [hidden] attribute over base display */
.sv-camoff[hidden],
.ended-overlay[hidden],
.typing[hidden],
.tabpanel[hidden] {
display: none;
}// ── Toast ──────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2600);
}
// ── Call timer (counts up mm:ss) ─────────────────────────────────────────────
const timerEl = document.getElementById("timer");
let seconds = 0;
let callLive = true;
function fmt(total) {
const m = Math.floor(total / 60);
const s = total % 60;
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
const timerId = setInterval(() => {
if (!callLive) return;
seconds += 1;
timerEl.textContent = fmt(seconds);
}, 1000);
// ── Control toggles ──────────────────────────────────────────────────────────
const micBtn = document.getElementById("micBtn");
const camBtn = document.getElementById("camBtn");
const shareBtn = document.getElementById("shareBtn");
const handBtn = document.getElementById("handBtn");
const svMute = document.getElementById("svMute");
const svCamOff = document.getElementById("svCamOff");
micBtn.addEventListener("click", () => {
const on = micBtn.dataset.on === "true";
micBtn.dataset.on = on ? "false" : "true";
micBtn.classList.toggle("is-off", on);
micBtn.setAttribute("aria-pressed", String(on));
micBtn.setAttribute("aria-label", on ? "Unmute microphone" : "Mute microphone");
micBtn.querySelector(".ctl-lbl").textContent = on ? "Unmute" : "Mute";
svMute.hidden = !on;
showToast(on ? "Microphone muted" : "Microphone on");
});
camBtn.addEventListener("click", () => {
const on = camBtn.dataset.on === "true";
camBtn.dataset.on = on ? "false" : "true";
camBtn.classList.toggle("is-off", on);
camBtn.setAttribute("aria-pressed", String(on));
camBtn.setAttribute("aria-label", on ? "Turn camera on" : "Turn camera off");
svCamOff.hidden = !on;
showToast(on ? "Camera turned off" : "Camera turned on");
});
shareBtn.addEventListener("click", () => {
const active = shareBtn.classList.toggle("is-active");
shareBtn.setAttribute("aria-pressed", String(active));
showToast(active ? "You started sharing your screen" : "You stopped sharing");
});
handBtn.addEventListener("click", () => {
const active = handBtn.classList.toggle("is-active");
handBtn.setAttribute("aria-pressed", String(active));
showToast(active ? "Hand raised ✋" : "Hand lowered");
});
// ── Side tabs ────────────────────────────────────────────────────────────────
const tabs = document.querySelectorAll(".tab");
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
const name = tab.dataset.tab;
tabs.forEach((t) => {
const on = t === tab;
t.classList.toggle("is-active", on);
t.setAttribute("aria-selected", String(on));
});
document.querySelectorAll(".tabpanel").forEach((p) => {
const on = p.id === `panel-${name}`;
p.classList.toggle("is-active", on);
p.hidden = !on;
});
});
});
// ── Chat: send + simulated reply with typing indicator ───────────────────────
const thread = document.getElementById("thread");
const typing = document.getElementById("typing");
const composer = document.getElementById("composer");
const chatInput = document.getElementById("chatInput");
const REPLIES = [
"Thanks, that's helpful.",
"Understood — let's keep monitoring that.",
"Good. I'll add a note to your chart.",
"Sounds reasonable. Any other symptoms?",
];
let replyIdx = 0;
function addMessage(text, dir, who) {
const msg = document.createElement("div");
msg.className = `msg ${dir}`;
msg.innerHTML = `<span class="msg-who">${who}</span><p class="bubble"></p>`;
msg.querySelector(".bubble").textContent = text;
thread.appendChild(msg);
thread.scrollTop = thread.scrollHeight;
}
composer.addEventListener("submit", (e) => {
e.preventDefault();
const text = chatInput.value.trim();
if (!text || !callLive) return;
addMessage(text, "out", "You");
chatInput.value = "";
typing.hidden = false;
thread.scrollTop = thread.scrollHeight;
setTimeout(() => {
typing.hidden = true;
addMessage(REPLIES[replyIdx % REPLIES.length], "in", "Dr. Okafor");
replyIdx += 1;
}, 1800);
});
// ── End call ─────────────────────────────────────────────────────────────────
const endBtn = document.getElementById("endBtn");
const endedOverlay = document.getElementById("endedOverlay");
const endedDuration = document.getElementById("endedDuration");
const rejoinBtn = document.getElementById("rejoinBtn");
endBtn.addEventListener("click", () => {
if (!callLive) return;
callLive = false;
clearInterval(timerId);
endedDuration.textContent = `Duration ${fmt(seconds)}`;
endedOverlay.hidden = false;
chatInput.disabled = true;
showToast("Call ended");
});
rejoinBtn.addEventListener("click", () => {
showToast("Returning to the patient portal…");
});<!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>Video Consult · Northbridge Clinic</title>
</head>
<body>
<div class="room">
<header class="callbar">
<div class="cb-left">
<span class="brand-mark" aria-hidden="true">✚</span>
<div class="cb-meta">
<p class="cb-title">Consult · Amara Mensah</p>
<p class="cb-sub">Cardiology follow-up</p>
</div>
</div>
<div class="cb-center">
<span class="live-dot" aria-hidden="true"></span>
<span class="cb-timer" id="timer" role="timer" aria-label="Call duration">00:00</span>
</div>
<div class="cb-right">
<span class="enc-badge" title="End-to-end encrypted">
<span aria-hidden="true">🔒</span> Encrypted
</span>
</div>
</header>
<main class="stage-wrap">
<section class="stage" aria-label="Remote participant video">
<div class="stage-tile">
<div class="big-avatar" aria-hidden="true">LO</div>
<div class="remote-label">
<span class="rl-name">Dr. Lena Okafor</span>
<span class="rl-role">Cardiology</span>
</div>
<div class="quality" id="quality" title="Connection quality: good">
<span class="q-bar"></span>
<span class="q-bar"></span>
<span class="q-bar"></span>
<span class="q-bar q-dim"></span>
</div>
</div>
<div class="selfview" id="selfview">
<div class="sv-avatar" aria-hidden="true">AM</div>
<span class="sv-label">You</span>
<span class="sv-mute" id="svMute" aria-hidden="true" hidden>🔇</span>
<div class="sv-camoff" id="svCamOff" hidden>
<span aria-hidden="true">📷</span>
<span>Camera off</span>
</div>
</div>
<div class="ended-overlay" id="endedOverlay" hidden>
<div class="eo-card">
<span class="eo-icon" aria-hidden="true">✓</span>
<p class="eo-title">Call ended</p>
<p class="eo-dur" id="endedDuration">Duration 00:00</p>
<button class="eo-btn" id="rejoinBtn">Return to portal</button>
</div>
</div>
</section>
<div class="controls" role="toolbar" aria-label="Call controls">
<button class="ctl" id="micBtn" data-on="true" aria-pressed="false" aria-label="Mute microphone">
<span class="ctl-ic" aria-hidden="true">🎙️</span>
<span class="ctl-lbl">Mute</span>
</button>
<button class="ctl" id="camBtn" data-on="true" aria-pressed="false" aria-label="Turn camera off">
<span class="ctl-ic" aria-hidden="true">📹</span>
<span class="ctl-lbl">Camera</span>
</button>
<button class="ctl" id="shareBtn" aria-pressed="false" aria-label="Share screen">
<span class="ctl-ic" aria-hidden="true">🖥️</span>
<span class="ctl-lbl">Share</span>
</button>
<button class="ctl" id="handBtn" aria-pressed="false" aria-label="Raise hand">
<span class="ctl-ic" aria-hidden="true">✋</span>
<span class="ctl-lbl">Raise</span>
</button>
<button class="ctl ctl-end" id="endBtn" aria-label="End call">
<span class="ctl-ic" aria-hidden="true">📞</span>
<span class="ctl-lbl">End</span>
</button>
</div>
</main>
<aside class="side">
<nav class="tabs" role="tablist" aria-label="Consult panel">
<button class="tab is-active" id="tab-chat" role="tab" aria-selected="true" data-tab="chat">Chat</button>
<button class="tab" id="tab-notes" role="tab" aria-selected="false" data-tab="notes">Notes</button>
<button class="tab" id="tab-info" role="tab" aria-selected="false" data-tab="info">Info</button>
</nav>
<section class="tabpanel is-active" id="panel-chat" role="tabpanel" aria-labelledby="tab-chat">
<div class="thread" id="thread">
<div class="msg in">
<span class="msg-who">Dr. Okafor</span>
<p class="bubble">Hi Amara, can you hear me clearly?</p>
</div>
<div class="msg in">
<span class="msg-who">Dr. Okafor</span>
<p class="bubble">Great — let's review your recent readings.</p>
</div>
</div>
<div class="typing" id="typing" hidden aria-live="polite">
<span class="t-who">Dr. Okafor is typing</span>
<span class="t-dots"><i></i><i></i><i></i></span>
</div>
<form class="composer" id="composer">
<input
class="composer-input"
id="chatInput"
type="text"
placeholder="Type a message…"
autocomplete="off"
aria-label="Message"
/>
<button class="send-btn" type="submit" aria-label="Send message">➤</button>
</form>
</section>
<section class="tabpanel" id="panel-notes" role="tabpanel" aria-labelledby="tab-notes" hidden>
<label class="notes-label" for="notesArea">Consult notes</label>
<textarea
class="notes-area"
id="notesArea"
placeholder="Jot quick notes during the consult…"
></textarea>
<p class="notes-hint">Notes are private to this session.</p>
</section>
<section class="tabpanel" id="panel-info" role="tabpanel" aria-labelledby="tab-info" hidden>
<div class="info-head">
<span class="info-avatar" aria-hidden="true">AM</span>
<div>
<p class="info-name">Amara Mensah</p>
<p class="info-meta">34 · Female · MRN 48211</p>
</div>
</div>
<dl class="info-list">
<div class="info-row">
<dt>Reason for visit</dt>
<dd>Cardiology follow-up · palpitations</dd>
</div>
<div class="info-row">
<dt>Blood pressure</dt>
<dd>128 / 82 mmHg</dd>
</div>
<div class="info-row">
<dt>Heart rate</dt>
<dd>74 bpm</dd>
</div>
<div class="info-row">
<dt>Allergies</dt>
<dd><span class="chip danger">Penicillin</span></dd>
</div>
<div class="info-row">
<dt>Medications</dt>
<dd>Atorvastatin 20mg · Metformin 500mg</dd>
</div>
</dl>
</section>
</aside>
</div>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Video Consult Room
A focused telemedicine call surface. The dark stage centres the remote clinician with a simulated video tile (gradient + initials), an encrypted badge and a live connection-quality meter, while a self-view picture-in-picture tile shows the patient and visibly reflects muted-mic and camera-off states. A circular control bar handles mute, camera, screen-share and raise-hand, with a prominent red end-call button. A light, clinical side panel carries Chat, Notes and Info tabs: messages send with a simulated reply and typing indicator, notes persist in a textarea, and Info shows a compact patient snapshot. A header timer counts the call up in mm:ss, and ending the call freezes the duration on an overlay. All interactions are vanilla JS.
Illustrative UI only — not intended for real medical use.