Museum — Attendance & Ticketing Dashboard
A refined, gallery-toned operations dashboard for a fictional art museum that tracks visitor attendance and ticketing at a glance. Four KPI cards report today visitors, revenue, capacity used, and new members with up and down deltas; a CSS bar chart breaks admissions down by opening hour and flags the peak; a conic-gradient donut with a legend shows the ticket-type mix; and a table lists upcoming timed-entry slots with sold and capacity fill bars and status badges. A date-range selector rewrites every figure, with toasts confirming each change.
MCP
Code
:root {
--paper: #f6f4ef;
--wall: #ffffff;
--charcoal: #1c1b19;
--ink: #2a2825;
--ink-2: #4a4640;
--muted: #8c857a;
--gold: #a98140;
--gold-d: #876631;
--gold-50: #f3ecdd;
--line: rgba(28, 27, 25, 0.12);
--line-2: rgba(28, 27, 25, 0.2);
--ok: #3f7d56;
--warn: #b8842c;
--danger: #b4493a;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--shadow-sm: 0 1px 2px rgba(28, 27, 25, 0.05), 0 1px 3px rgba(28, 27, 25, 0.04);
--shadow-md: 0 6px 24px rgba(28, 27, 25, 0.07);
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: var(--sans);
background: var(--paper);
color: var(--ink);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app { max-width: 1180px; margin: 0 auto; padding: 0 24px 64px; }
/* Topbar */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 18px 0;
background: color-mix(in srgb, var(--paper) 86%, transparent);
backdrop-filter: saturate(120%) blur(8px);
border-bottom: 1px solid var(--line);
flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark {
width: 42px; height: 42px;
display: grid; place-items: center;
border: 1px solid var(--gold);
color: var(--gold-d);
border-radius: var(--r-sm);
font-size: 20px;
background: var(--gold-50);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.25; }
.brand-name { font-family: var(--serif); font-weight: 600; font-size: 1.32rem; color: var(--charcoal); }
.brand-sub { font-size: 0.78rem; color: var(--muted); letter-spacing: 0.02em; }
.topbar-right { display: flex; align-items: center; gap: 16px; }
.range {
display: inline-flex;
background: var(--wall);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px;
box-shadow: var(--shadow-sm);
}
.range-btn {
font: inherit;
font-size: 0.82rem;
font-weight: 500;
color: var(--ink-2);
background: transparent;
border: 0;
padding: 6px 14px;
border-radius: 999px;
cursor: pointer;
transition: background 0.18s, color 0.18s;
}
.range-btn:hover { color: var(--charcoal); }
.range-btn[aria-pressed="true"] {
background: var(--charcoal);
color: var(--paper);
}
.range-btn:focus-visible,
.ghost-btn:focus-visible,
.export-row:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.who-avatar {
width: 38px; height: 38px;
display: grid; place-items: center;
border-radius: 50%;
background: var(--charcoal);
color: var(--paper);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.04em;
}
/* Board head */
.board-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
margin: 34px 0 22px;
flex-wrap: wrap;
}
.board-title {
font-family: var(--serif);
font-weight: 600;
font-size: 2.3rem;
margin: 0;
color: var(--charcoal);
letter-spacing: -0.01em;
}
.board-meta { margin: 4px 0 0; color: var(--muted); font-size: 0.88rem; }
.pill {
display: inline-flex; align-items: center; gap: 8px;
font-size: 0.8rem; font-weight: 500;
padding: 7px 14px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--wall);
}
.pill .dot { width: 8px; height: 8px; border-radius: 50%; }
.pill-ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 30%, var(--line)); }
.pill-ok .dot { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 18%, transparent); }
.pill-warn { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 32%, var(--line)); }
.pill-warn .dot { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 18%, transparent); }
/* KPIs */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 22px;
}
.kpi {
background: var(--wall);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 20px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 7px;
transition: transform 0.18s, box-shadow 0.18s;
}
.kpi:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.kpi-label { font-size: 0.78rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
.kpi-value { font-family: var(--serif); font-weight: 600; font-size: 2.1rem; color: var(--charcoal); line-height: 1; }
.kpi-delta { font-size: 0.78rem; font-weight: 500; }
.kpi-delta.up { color: var(--ok); }
.kpi-delta.down { color: var(--danger); }
.kpi-bar {
margin-top: 4px;
height: 6px;
border-radius: 999px;
background: var(--gold-50);
overflow: hidden;
}
.kpi-bar span {
display: block; height: 100%;
background: linear-gradient(90deg, var(--gold), var(--gold-d));
border-radius: 999px;
transition: width 0.5s ease;
}
/* Grid */
.grid {
display: grid;
grid-template-columns: 1.55fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.card {
background: var(--wall);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px 24px;
box-shadow: var(--shadow-sm);
}
.card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 20px;
}
.card-title { font-family: var(--serif); font-weight: 600; font-size: 1.4rem; margin: 0; color: var(--charcoal); }
.card-sub { margin: 3px 0 0; font-size: 0.82rem; color: var(--muted); }
.tag {
font-size: 0.74rem;
font-weight: 500;
color: var(--gold-d);
background: var(--gold-50);
border: 1px solid color-mix(in srgb, var(--gold) 24%, var(--line));
padding: 4px 11px;
border-radius: 999px;
white-space: nowrap;
}
/* Bar chart */
.bars {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
align-items: end;
gap: 8px;
height: 220px;
padding-top: 8px;
border-bottom: 1px solid var(--line);
}
.bar-col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: 8px;
height: 100%;
}
.bar {
width: 100%;
max-width: 30px;
border-radius: 5px 5px 0 0;
background: linear-gradient(180deg, var(--gold), var(--gold-d));
position: relative;
transition: height 0.55s cubic-bezier(0.22, 1, 0.36, 1), background 0.18s;
cursor: default;
}
.bar.is-peak { background: linear-gradient(180deg, var(--charcoal), #3a3733); }
.bar:hover { filter: brightness(1.07); }
.bar:hover::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: var(--charcoal);
color: var(--paper);
font-size: 0.72rem;
padding: 4px 8px;
border-radius: var(--r-sm);
white-space: nowrap;
pointer-events: none;
z-index: 5;
}
.bar-label { font-size: 0.68rem; color: var(--muted); }
/* Donut */
.donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 22px; }
.donut {
width: 168px; height: 168px;
border-radius: 50%;
position: relative;
flex: none;
}
.donut-hole {
position: absolute;
inset: 26px;
background: var(--wall);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: inset 0 0 0 1px var(--line);
}
.donut-total { font-family: var(--serif); font-weight: 600; font-size: 1.7rem; color: var(--charcoal); line-height: 1; }
.donut-cap { font-size: 0.7rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.legend { list-style: none; margin: 0; padding: 0; width: 100%; display: grid; gap: 10px; }
.legend li {
display: grid;
grid-template-columns: 14px 1fr auto;
align-items: center;
gap: 10px;
font-size: 0.84rem;
}
.legend .sw { width: 12px; height: 12px; border-radius: 3px; }
.legend .lname { color: var(--ink-2); }
.legend .lval { color: var(--charcoal); font-weight: 500; font-variant-numeric: tabular-nums; }
.legend .lpct { color: var(--muted); font-size: 0.78rem; margin-left: 8px; }
/* Slots table */
.table-wrap { overflow-x: auto; }
.slots {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
min-width: 580px;
}
.slots thead th {
text-align: left;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--muted);
font-weight: 600;
padding: 0 14px 12px;
border-bottom: 1px solid var(--line);
}
.slots th.num, .slots td.num { text-align: right; font-variant-numeric: tabular-nums; }
.slots tbody td {
padding: 14px;
border-bottom: 1px solid var(--line);
color: var(--ink-2);
vertical-align: middle;
}
.slots tbody tr { transition: background 0.15s; }
.slots tbody tr:hover { background: var(--gold-50); }
.slots tbody tr:last-child td { border-bottom: 0; }
.slot-time { font-weight: 600; color: var(--charcoal); }
.slot-exh { color: var(--ink-2); }
.slot-exh small { display: block; color: var(--muted); font-size: 0.74rem; }
.fillbar {
width: 110px;
height: 7px;
border-radius: 999px;
background: var(--gold-50);
overflow: hidden;
}
.fillbar span {
display: block; height: 100%;
border-radius: 999px;
transition: width 0.5s ease;
}
.fillbar.lo span { background: var(--ok); }
.fillbar.mid span { background: var(--warn); }
.fillbar.hi span { background: var(--danger); }
.badge {
display: inline-flex; align-items: center; gap: 6px;
font-size: 0.74rem; font-weight: 500;
padding: 3px 10px;
border-radius: 999px;
border: 1px solid var(--line);
}
.badge.open { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 30%, var(--line)); background: color-mix(in srgb, var(--ok) 7%, var(--wall)); }
.badge.filling { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 32%, var(--line)); background: color-mix(in srgb, var(--warn) 8%, var(--wall)); }
.badge.soldout { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, var(--line)); background: color-mix(in srgb, var(--danger) 7%, var(--wall)); }
/* Foot */
.board-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-top: 22px;
padding-top: 18px;
border-top: 1px solid var(--line);
font-size: 0.8rem;
color: var(--muted);
flex-wrap: wrap;
}
.ghost-btn {
font: inherit;
font-size: 0.84rem;
font-weight: 500;
color: var(--charcoal);
background: var(--wall);
border: 1px solid var(--line-2);
padding: 9px 18px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.16s, border-color 0.16s;
}
.ghost-btn:hover { background: var(--gold-50); border-color: var(--gold); }
/* Toast */
.toast-wrap {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 60;
width: max-content;
max-width: 92vw;
}
.toast {
background: var(--charcoal);
color: var(--paper);
font-size: 0.86rem;
padding: 12px 18px;
border-radius: var(--r-md);
box-shadow: var(--shadow-md);
border-left: 3px solid var(--gold);
animation: toast-in 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.toast.out { animation: toast-out 0.26s forwards; }
@keyframes toast-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(12px); } }
/* Responsive */
@media (max-width: 900px) {
.grid { grid-template-columns: 1fr; }
.kpis { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.app { padding: 0 16px 48px; }
.board-title { font-size: 1.85rem; }
.kpis { grid-template-columns: 1fr 1fr; gap: 12px; }
.kpi { padding: 14px 16px; }
.kpi-value { font-size: 1.7rem; }
.card { padding: 18px 16px; }
.bars { height: 180px; gap: 5px; }
.bar-label { font-size: 0.6rem; }
.range { width: 100%; justify-content: space-between; }
.range-btn { flex: 1; padding: 7px 6px; text-align: center; }
.topbar-right { width: 100%; flex-direction: column; align-items: stretch; }
.who { display: none; }
.board-foot { flex-direction: column; align-items: flex-start; }
}(function () {
"use strict";
// ---- Toast helper ----
var toastWrap = document.getElementById("toastWrap");
function toast(msg) {
var el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 280);
}, 2600);
}
var fmtMoney = function (n) {
return "$" + Math.round(n).toLocaleString("en-US");
};
var fmtNum = function (n) {
return Math.round(n).toLocaleString("en-US");
};
// ---- Dataset per range ----
// Hours the museum is open: 10..17 (last entry 17:00)
var HOURS = [10, 11, 12, 13, 14, 15, 16, 17];
var TICKET_TYPES = [
{ key: "adult", name: "General Admission", color: "#1c1b19", share: 0.46 },
{ key: "member", name: "Members", color: "#a98140", share: 0.21 },
{ key: "student", name: "Student / Senior", color: "#876631", share: 0.17 },
{ key: "child", name: "Youth & Child", color: "#3f7d56", share: 0.1 },
{ key: "group", name: "Groups & Tours", color: "#8c857a", share: 0.06 }
];
var EXHIBITIONS = [
{ name: "Permanent Collection", sub: "Galleries 1–14" },
{ name: "Luminous Ground", sub: "Color Field, 1958–74" },
{ name: "Permanent Collection", sub: "Galleries 1–14" },
{ name: "Cast in Bronze", sub: "Rodin & After" },
{ name: "Luminous Ground", sub: "Color Field, 1958–74" },
{ name: "Permanent Collection", sub: "Galleries 1–14" }
];
var RANGES = {
today: {
meta: "Today · Saturday, June 15, 2026 · Open 10:00–18:00",
visitors: 3184,
revenue: 68420,
capacity: 71,
members: 42,
d: { visitors: 6.2, revenue: 4.1, members: -1.8 },
// visitor weight per hour (relative)
hourW: [0.55, 0.82, 1.0, 0.93, 1.12, 0.96, 0.74, 0.48],
slots: [
{ time: "14:30", sold: 162, cap: 180 },
{ time: "15:00", sold: 174, cap: 180 },
{ time: "15:30", sold: 180, cap: 180 },
{ time: "16:00", sold: 121, cap: 180 },
{ time: "16:30", sold: 88, cap: 180 },
{ time: "17:00", sold: 54, cap: 180 }
],
status: "open"
},
"7d": {
meta: "Last 7 days · Jun 9 – Jun 15, 2026 · Closed Mondays",
visitors: 18642,
revenue: 401350,
capacity: 64,
members: 287,
d: { visitors: 3.4, revenue: 5.6, members: 2.2 },
hourW: [0.62, 0.85, 1.04, 0.9, 1.05, 0.92, 0.7, 0.5],
slots: [
{ time: "Tue 11:00", sold: 168, cap: 180 },
{ time: "Wed 13:30", sold: 159, cap: 180 },
{ time: "Thu 15:00", sold: 177, cap: 180 },
{ time: "Fri 14:00", sold: 180, cap: 180 },
{ time: "Sat 12:30", sold: 174, cap: 180 },
{ time: "Sun 11:30", sold: 142, cap: 180 }
],
status: "open"
},
"30d": {
meta: "Last 30 days · May 17 – Jun 15, 2026",
visitors: 74310,
revenue: 1612780,
capacity: 58,
members: 1043,
d: { visitors: -0.8, revenue: 2.9, members: 4.7 },
hourW: [0.68, 0.88, 1.0, 0.86, 0.98, 0.9, 0.72, 0.55],
slots: [
{ time: "Avg 10:30", sold: 132, cap: 180 },
{ time: "Avg 12:00", sold: 156, cap: 180 },
{ time: "Avg 13:30", sold: 161, cap: 180 },
{ time: "Avg 15:00", sold: 149, cap: 180 },
{ time: "Avg 16:30", sold: 108, cap: 180 },
{ time: "Avg 17:00", sold: 71, cap: 180 }
],
status: "open"
},
qtr: {
meta: "Quarter to date · Apr 1 – Jun 15, 2026",
visitors: 214905,
revenue: 4738200,
capacity: 61,
members: 3119,
d: { visitors: 8.1, revenue: 9.4, members: 6.3 },
hourW: [0.66, 0.86, 1.0, 0.88, 1.02, 0.9, 0.71, 0.52],
slots: [
{ time: "Apr peak", sold: 171, cap: 180 },
{ time: "May peak", sold: 180, cap: 180 },
{ time: "Jun peak", sold: 178, cap: 180 },
{ time: "Weekday avg", sold: 138, cap: 180 },
{ time: "Weekend avg", sold: 166, cap: 180 },
{ time: "Late entry", sold: 64, cap: 180 }
],
status: "open"
}
};
// ---- Render: KPIs ----
function setDelta(el, pct) {
var up = pct >= 0;
el.className = "kpi-delta " + (up ? "up" : "down");
el.textContent = (up ? "▲ " : "▼ ") + Math.abs(pct).toFixed(1) + "% vs prior";
}
function renderKpis(r) {
document.getElementById("kpiVisitors").textContent = fmtNum(r.visitors);
document.getElementById("kpiRevenue").textContent = fmtMoney(r.revenue);
document.getElementById("kpiCapacity").textContent = r.capacity + "%";
document.getElementById("kpiCapacityBar").style.width = r.capacity + "%";
document.getElementById("kpiMembers").textContent = fmtNum(r.members);
setDelta(document.getElementById("kpiVisitorsDelta"), r.d.visitors);
setDelta(document.getElementById("kpiRevenueDelta"), r.d.revenue);
setDelta(document.getElementById("kpiMembersDelta"), r.d.members);
}
// ---- Render: hourly bar chart ----
function renderHours(r) {
var chart = document.getElementById("hoursChart");
chart.innerHTML = "";
var wSum = r.hourW.reduce(function (a, b) { return a + b; }, 0);
var maxW = Math.max.apply(null, r.hourW);
var peakIdx = r.hourW.indexOf(maxW);
HOURS.forEach(function (h, i) {
var w = r.hourW[i];
var count = Math.round((w / wSum) * r.visitors);
var pct = (w / maxW) * 100;
var col = document.createElement("div");
col.className = "bar-col";
var bar = document.createElement("div");
bar.className = "bar" + (i === peakIdx ? " is-peak" : "");
bar.style.height = "0%";
var label = (h < 10 ? "0" : "") + h + ":00";
bar.setAttribute("data-tip", label + " · " + fmtNum(count) + " visitors");
bar.setAttribute("role", "img");
bar.setAttribute("aria-label", label + ", " + fmtNum(count) + " visitors");
var lbl = document.createElement("span");
lbl.className = "bar-label";
lbl.textContent = (h % 12 === 0 ? 12 : h % 12) + (h < 12 ? "a" : "p");
col.appendChild(bar);
col.appendChild(lbl);
chart.appendChild(col);
// animate in
requestAnimationFrame(function () {
requestAnimationFrame(function () {
bar.style.height = pct + "%";
});
});
});
var peakH = HOURS[peakIdx];
document.getElementById("hoursPeak").textContent =
"Peak " + (peakH < 10 ? "0" : "") + peakH + ":00";
}
// ---- Render: donut + legend ----
function renderDonut(r) {
var donut = document.getElementById("donut");
document.getElementById("donutTotal").textContent = fmtNum(r.visitors);
var stops = [];
var legendEl = document.getElementById("legend");
legendEl.innerHTML = "";
var acc = 0;
TICKET_TYPES.forEach(function (t) {
var start = acc * 360;
acc += t.share;
var end = acc * 360;
stops.push(t.color + " " + start.toFixed(2) + "deg " + end.toFixed(2) + "deg");
var count = Math.round(t.share * r.visitors);
var li = document.createElement("li");
var sw = document.createElement("span");
sw.className = "sw";
sw.style.background = t.color;
var name = document.createElement("span");
name.className = "lname";
name.textContent = t.name;
var val = document.createElement("span");
val.className = "lval";
val.innerHTML = fmtNum(count) + '<span class="lpct">' + Math.round(t.share * 100) + "%</span>";
li.appendChild(sw);
li.appendChild(name);
li.appendChild(val);
legendEl.appendChild(li);
});
donut.style.background = "conic-gradient(" + stops.join(", ") + ")";
}
// ---- Render: slots table ----
function fillClass(pct) {
if (pct >= 95) return "hi";
if (pct >= 80) return "mid";
return "lo";
}
function renderSlots(r) {
var body = document.getElementById("slotsBody");
body.innerHTML = "";
document.getElementById("slotsCount").textContent = r.slots.length + " slots";
r.slots.forEach(function (s, i) {
var exh = EXHIBITIONS[i % EXHIBITIONS.length];
var pct = Math.round((s.sold / s.cap) * 100);
var fc = fillClass(pct);
var soldOut = s.sold >= s.cap;
var tr = document.createElement("tr");
tr.innerHTML =
'<td><span class="slot-time">' + s.time + "</span></td>" +
'<td class="slot-exh">' + exh.name + "<small>" + exh.sub + "</small></td>" +
'<td class="num">' + fmtNum(s.sold) + "</td>" +
'<td class="num">' + fmtNum(s.cap) + "</td>" +
"<td>" +
'<div class="fillbar ' + fc + '" role="img" aria-label="' + pct + ' percent full">' +
'<span style="width:0%"></span></div>' +
"</td>" +
"<td>" +
(soldOut
? '<span class="badge soldout">Sold out</span>'
: pct >= 80
? '<span class="badge filling">Filling · ' + (s.cap - s.sold) + " left</span>"
: '<span class="badge open">Open · ' + (s.cap - s.sold) + " left</span>") +
"</td>";
body.appendChild(tr);
var bar = tr.querySelector(".fillbar span");
requestAnimationFrame(function () {
requestAnimationFrame(function () {
bar.style.width = pct + "%";
});
});
});
}
// ---- Status pill ----
function renderStatus(r) {
var pill = document.getElementById("statusPill");
pill.className = "pill " + (r.status === "open" ? "pill-ok" : "pill-warn");
pill.innerHTML =
'<span class="dot"></span> ' + (r.status === "open" ? "Galleries open" : "After hours");
}
function pad(n) { return n < 10 ? "0" + n : "" + n; }
function nowStr() {
var d = new Date();
return pad(d.getHours()) + ":" + pad(d.getMinutes());
}
// ---- Master render ----
function render(key) {
var r = RANGES[key];
document.getElementById("rangeMeta").textContent = r.meta;
renderStatus(r);
renderKpis(r);
renderHours(r);
renderDonut(r);
renderSlots(r);
document.getElementById("syncTime").textContent = nowStr();
}
// ---- Range buttons ----
var rangeBtns = document.querySelectorAll(".range-btn");
var rangeLabels = { today: "Today", "7d": "the last 7 days", "30d": "the last 30 days", qtr: "this quarter" };
rangeBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
var key = btn.getAttribute("data-range");
if (btn.getAttribute("aria-pressed") === "true") return;
rangeBtns.forEach(function (b) { b.setAttribute("aria-pressed", "false"); });
btn.setAttribute("aria-pressed", "true");
render(key);
toast("Showing figures for " + rangeLabels[key] + ".");
});
});
// ---- Export ----
document.getElementById("exportBtn").addEventListener("click", function () {
var active = document.querySelector('.range-btn[aria-pressed="true"]');
var key = active ? active.getAttribute("data-range") : "today";
var r = RANGES[key];
toast(
"Report queued — " + fmtNum(r.visitors) + " visitors, " + fmtMoney(r.revenue) + " revenue."
);
document.getElementById("syncTime").textContent = nowStr();
});
// ---- Init ----
render("today");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Meridian Museum — Attendance & Ticketing</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&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◈</span>
<div class="brand-text">
<span class="brand-name">Meridian Museum of Art</span>
<span class="brand-sub">Attendance & Ticketing — Operations</span>
</div>
</div>
<div class="topbar-right">
<div class="range" role="group" aria-label="Date range">
<button class="range-btn" data-range="today" type="button" aria-pressed="true">Today</button>
<button class="range-btn" data-range="7d" type="button" aria-pressed="false">7 days</button>
<button class="range-btn" data-range="30d" type="button" aria-pressed="false">30 days</button>
<button class="range-btn" data-range="qtr" type="button" aria-pressed="false">Quarter</button>
</div>
<div class="who" aria-hidden="true">
<span class="who-avatar">AV</span>
</div>
</div>
</header>
<main class="board">
<div class="board-head">
<div>
<h1 class="board-title">Visitor Operations</h1>
<p class="board-meta" id="rangeMeta">Today · Saturday, June 15, 2026 · Open 10:00–18:00</p>
</div>
<span class="pill pill-ok" id="statusPill"><span class="dot"></span> Galleries open</span>
</div>
<section class="kpis" aria-label="Key figures">
<article class="kpi">
<span class="kpi-label">Visitors</span>
<strong class="kpi-value" id="kpiVisitors">3,184</strong>
<span class="kpi-delta up" id="kpiVisitorsDelta">▲ 6.2% vs prior</span>
</article>
<article class="kpi">
<span class="kpi-label">Revenue</span>
<strong class="kpi-value" id="kpiRevenue">$68,420</strong>
<span class="kpi-delta up" id="kpiRevenueDelta">▲ 4.1% vs prior</span>
</article>
<article class="kpi">
<span class="kpi-label">Capacity used</span>
<strong class="kpi-value" id="kpiCapacity">71%</strong>
<div class="kpi-bar"><span id="kpiCapacityBar" style="width:71%"></span></div>
</article>
<article class="kpi">
<span class="kpi-label">New members</span>
<strong class="kpi-value" id="kpiMembers">42</strong>
<span class="kpi-delta down" id="kpiMembersDelta">▼ 1.8% vs prior</span>
</article>
</section>
<div class="grid">
<section class="card chart-card" aria-labelledby="hoursTitle">
<div class="card-head">
<div>
<h2 class="card-title" id="hoursTitle">Visitors by hour</h2>
<p class="card-sub" id="hoursSub">Timed-entry admissions across opening hours</p>
</div>
<span class="tag" id="hoursPeak">Peak 14:00</span>
</div>
<div class="bars" id="hoursChart" role="img" aria-label="Bar chart of visitors by hour"></div>
</section>
<section class="card donut-card" aria-labelledby="mixTitle">
<div class="card-head">
<h2 class="card-title" id="mixTitle">Ticket-type mix</h2>
</div>
<div class="donut-wrap">
<div class="donut" id="donut" role="img" aria-label="Donut chart of ticket type mix">
<div class="donut-hole">
<span class="donut-total" id="donutTotal">3,184</span>
<span class="donut-cap">admissions</span>
</div>
</div>
<ul class="legend" id="legend"></ul>
</div>
</section>
</div>
<section class="card slots-card" aria-labelledby="slotsTitle">
<div class="card-head">
<div>
<h2 class="card-title" id="slotsTitle">Upcoming timed-entry slots</h2>
<p class="card-sub">Permanent collection & <em>Luminous Ground: Color Field Painting, 1958–1974</em></p>
</div>
<span class="tag" id="slotsCount">6 slots</span>
</div>
<div class="table-wrap">
<table class="slots">
<thead>
<tr>
<th scope="col">Entry time</th>
<th scope="col">Exhibition</th>
<th scope="col" class="num">Sold</th>
<th scope="col" class="num">Capacity</th>
<th scope="col">Fill</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="slotsBody"></tbody>
</table>
</div>
</section>
<footer class="board-foot">
<span>Catalog Ref. ATT-2026-0615 · Last sync <span id="syncTime">just now</span></span>
<button class="ghost-btn" type="button" id="exportBtn">Export day report</button>
</footer>
</main>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Attendance & Ticketing Dashboard
An operations console for the fictional Meridian Museum of Art, framing live admission to the permanent collection and the special exhibition Luminous Ground: Color Field Painting, 1958–1974. The layout keeps generous wall space, a Cormorant Garamond and Inter pairing, and a quiet gold-and-charcoal palette so it reads like a cultural institution. Four KPI cards report today’s visitors, revenue, capacity used, and new members — each with a directional delta or a progress bar.
Below the figures, a pure-CSS bar chart plots admissions across opening hours and highlights the peak slot, with a tooltip on each bar. A conic-gradient donut and labelled legend break out the ticket-type mix — general admission, members, student and senior, youth and child, and groups — and a table lists the upcoming timed-entry slots, each with sold-versus-capacity counts, an animated fill bar, and an open, filling, or sold-out badge.
The date-range selector in the top bar switches between today, seven days, thirty days, and the quarter; choosing a range recomputes every card, both charts, and the slot table, animates the bars and fill widths back in, and raises a toast. An export action queues a day report and stamps the sync time. Everything is keyboard-usable with visible focus, meets AA contrast, and reflows cleanly down to 360px.
Illustrative UI only — demo data; not a real museum system.