Museum — Gallery Wall / Salon Hang
A curatorial salon-hang gallery wall where framed works on paper sit in a balanced, spanning CSS grid against quiet wall space. Hovering lifts a single frame and dims its neighbours; clicking opens a typeset museum label with catalogue number, medium, dimensions and credit line. A layout toggle swaps the dense salon massing for an eye-level single line hung to a common centre rail, with refined serif typography and soft gallery lighting.
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;
--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;
}
button { font-family: inherit; cursor: pointer; }
:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ---------- Top bar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
background: rgba(246, 244, 239, 0.88);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar__inner {
max-width: 1180px;
margin: 0 auto;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand__mark {
font-size: 28px;
color: var(--gold);
line-height: 1;
transform: translateY(1px);
}
.brand__text { display: flex; flex-direction: column; }
.brand__name {
font-family: var(--serif);
font-weight: 600;
font-size: 22px;
letter-spacing: 0.2px;
color: var(--charcoal);
}
.brand__sub {
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.layout-switch { display: flex; align-items: center; gap: 8px; }
.layout-switch__label {
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
margin-right: 2px;
}
.seg {
background: var(--wall);
border: 1px solid var(--line-2);
color: var(--ink-2);
font-size: 13px;
font-weight: 500;
padding: 8px 14px;
border-radius: 999px;
transition: all 0.18s ease;
}
.seg:hover { border-color: var(--gold); color: var(--charcoal); }
.seg.is-active {
background: var(--charcoal);
border-color: var(--charcoal);
color: var(--paper);
}
/* ---------- Stage / Wall ---------- */
.stage {
max-width: 1180px;
margin: 0 auto;
padding: 56px 24px 80px;
}
.wall {
background:
radial-gradient(120% 90% at 50% 0%, #ffffff 0%, #faf9f6 60%, #f3f1ec 100%);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 46px;
box-shadow:
0 1px 0 rgba(255, 255, 255, 0.8) inset,
0 24px 60px -36px rgba(28, 27, 25, 0.35);
position: relative;
overflow: hidden;
}
/* faint picture-rail line near top */
.wall::before {
content: "";
position: absolute;
left: 0; right: 0; top: 22px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--line-2) 12%, var(--line-2) 88%, transparent);
opacity: 0.6;
pointer-events: none;
}
/* Salon hang: dense, spanning grid */
.wall[data-layout="salon"] {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-auto-rows: 26px;
gap: 18px;
align-items: stretch;
}
/* Eye-level line: even horizontal row, centred */
.wall[data-layout="line"] {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 34px;
padding-top: 70px;
padding-bottom: 70px;
}
.wall[data-layout="line"]::after {
content: "";
position: absolute;
left: 46px; right: 46px;
top: 50%;
height: 1px;
background: repeating-linear-gradient(90deg, var(--line-2) 0 6px, transparent 6px 12px);
opacity: 0.5;
pointer-events: none;
}
/* ---------- Frame ---------- */
.frame {
position: relative;
appearance: none;
border: none;
text-align: left;
padding: 0;
background: transparent;
display: block;
cursor: pointer;
transition: transform 0.2s ease, filter 0.25s ease, opacity 0.25s ease;
}
.wall[data-layout="line"] .frame {
/* fixed comfortable footprint, centred on the rail */
width: clamp(150px, 20vw, 230px);
}
.frame__mat {
background: var(--wall);
border: 7px solid #fbfaf7;
outline: 1px solid var(--line);
box-shadow:
0 2px 4px rgba(28, 27, 25, 0.06),
0 14px 30px -18px rgba(28, 27, 25, 0.4);
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
transition: box-shadow 0.25s ease, outline-color 0.25s ease;
}
.frame__plate {
position: relative;
flex: 1 1 auto;
min-height: 70px;
border: 1px solid var(--line);
border-radius: 2px;
overflow: hidden;
}
.frame__plate svg { display: block; width: 100%; height: 100%; }
.frame__cap {
margin-top: 10px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.frame__title {
font-family: var(--serif);
font-weight: 600;
font-size: 14px;
color: var(--charcoal);
font-style: italic;
line-height: 1.25;
}
.frame__cat {
font-size: 10px;
letter-spacing: 0.08em;
color: var(--muted);
white-space: nowrap;
flex: 0 0 auto;
}
.frame__artist {
font-size: 11px;
color: var(--ink-2);
margin-top: 2px;
}
/* hover/focus highlight + dim siblings */
.frame:hover, .frame:focus-visible { transform: translateY(-4px); z-index: 4; }
.frame:hover .frame__mat,
.frame:focus-visible .frame__mat {
box-shadow:
0 6px 10px rgba(28, 27, 25, 0.1),
0 28px 48px -22px rgba(28, 27, 25, 0.55);
outline-color: var(--gold);
}
.wall.is-hovering .frame:not(:hover):not(:focus-visible) {
opacity: 0.42;
filter: saturate(0.85);
}
/* medium badge corner */
.frame__badge {
position: absolute;
top: 8px; left: 8px;
font-size: 9px;
letter-spacing: 0.1em;
text-transform: uppercase;
background: rgba(28, 27, 25, 0.72);
color: var(--paper);
padding: 3px 7px;
border-radius: 999px;
backdrop-filter: blur(2px);
}
.frame__badge--star {
background: var(--gold);
color: #fff;
}
.rail-note {
text-align: center;
font-size: 12px;
color: var(--muted);
letter-spacing: 0.04em;
margin: 26px 0 0;
font-style: italic;
}
/* ---------- Label popover ---------- */
.label-pop {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
padding: 22px;
background: rgba(28, 27, 25, 0.42);
backdrop-filter: blur(3px);
animation: fade 0.18s ease;
}
.label-pop[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.label-pop__card {
background: var(--wall);
border: 1px solid var(--line);
border-radius: var(--r-md);
max-width: 440px;
width: 100%;
padding: 30px 30px 26px;
position: relative;
box-shadow: 0 30px 70px -30px rgba(28, 27, 25, 0.6);
animation: rise 0.22s ease;
}
@keyframes rise { from { transform: translateY(10px); opacity: 0; } to { transform: none; opacity: 1; } }
.label-pop__close {
position: absolute;
top: 14px; right: 14px;
border: 1px solid var(--line);
background: var(--paper);
width: 30px; height: 30px;
border-radius: 999px;
font-size: 18px;
line-height: 1;
color: var(--ink-2);
transition: all 0.15s ease;
}
.label-pop__close:hover { background: var(--charcoal); color: var(--paper); border-color: var(--charcoal); }
.label-pop__cat {
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--gold-d);
}
.label-pop__title {
font-family: var(--serif);
font-style: italic;
font-weight: 600;
font-size: 27px;
line-height: 1.18;
color: var(--charcoal);
margin: 6px 0 2px;
}
.label-pop__artist {
font-size: 14px;
color: var(--ink-2);
margin: 0 0 18px;
}
.label-pop__meta {
margin: 0 0 16px;
display: grid;
gap: 10px;
border-top: 1px solid var(--line);
padding-top: 16px;
}
.label-pop__meta > div {
display: grid;
grid-template-columns: 96px 1fr;
gap: 12px;
align-items: baseline;
}
.label-pop__meta dt {
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
margin: 0;
}
.label-pop__meta dd { margin: 0; font-size: 14px; color: var(--ink); }
.label-pop__note {
font-size: 13.5px;
color: var(--ink-2);
line-height: 1.6;
border-top: 1px solid var(--line);
padding-top: 14px;
margin: 0 0 14px;
}
.label-pop__tags { display: flex; flex-wrap: wrap; gap: 7px; }
.tag {
font-size: 11px;
background: var(--gold-50);
color: var(--gold-d);
border: 1px solid rgba(169, 129, 64, 0.28);
padding: 4px 10px;
border-radius: 999px;
letter-spacing: 0.03em;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translateX(-50%) translateY(20px);
background: var(--charcoal);
color: var(--paper);
font-size: 13px;
padding: 11px 18px;
border-radius: 999px;
box-shadow: 0 16px 34px -18px rgba(0, 0, 0, 0.6);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 80;
}
.toast.is-show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.wall[data-layout="salon"] { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 520px) {
.topbar__inner { padding: 14px 16px; }
.brand__name { font-size: 19px; }
.layout-switch { width: 100%; justify-content: space-between; }
.stage { padding: 32px 14px 60px; }
.wall { padding: 22px; }
.wall[data-layout="salon"] {
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 22px;
gap: 12px;
}
.wall[data-layout="line"] { gap: 22px; padding-top: 40px; padding-bottom: 40px; }
.wall[data-layout="line"] .frame { width: clamp(130px, 42vw, 200px); }
.label-pop__card { padding: 24px 20px 22px; }
.label-pop__title { font-size: 23px; }
.label-pop__meta > div { grid-template-columns: 78px 1fr; }
}(function () {
"use strict";
// --- Fictional collection data -------------------------------------------
// span = [colSpan, rowSpan] for the salon-hang grid (6 cols, 26px rows).
var WORKS = [
{
cat: "AC.1903.041", title: "Harbour at First Light", artist: "Élise Vaugirard",
date: "1903", medium: "Etching & aquatint", dims: "24 × 31 cm (plate)",
credit: "Bequest of M. Aldworth, 1947", span: [3, 7], star: true,
hue: 206, sat: 22, light: 64,
note: "A late printing from the Marseille suite, prized for its silvered tonal range across the morning water."
},
{
cat: "AC.1888.012", title: "Study of Folded Linen", artist: "Tomas Halvorsen",
date: "1888", medium: "Graphite on laid paper", dims: "19 × 14 cm",
credit: "Gift of the Halvorsen estate", span: [2, 5],
hue: 36, sat: 14, light: 76,
note: "A working drawing for the unrealised altarpiece at Bergen; the cross-hatching shows the artist's characteristic restraint."
},
{
cat: "AC.1921.207", title: "Nocturne, Pont Neuf", artist: "Renaud Sauvage",
date: "1921", medium: "Lithograph", dims: "28 × 39 cm",
credit: "Purchased with the Founders' Fund", span: [3, 6],
hue: 224, sat: 30, light: 32,
note: "Sauvage pulled only forty impressions before the stone was effaced. The river is rendered almost entirely in reserve."
},
{
cat: "AC.1875.003", title: "Pomegranate & Glass", artist: "Beatrix Lund",
date: "1875", medium: "Watercolour & gouache", dims: "17 × 22 cm",
credit: "Bequest of M. Aldworth, 1947", span: [2, 5],
hue: 4, sat: 46, light: 52,
note: "An early still life from Lund's Antwerp years, notable for the warm bloom on the fruit's skin."
},
{
cat: "AC.1910.118", title: "The Reading Room", artist: "Cosima Errante",
date: "1910", medium: "Mezzotint", dims: "26 × 20 cm",
credit: "Gift of the Errante circle", span: [2, 6],
hue: 30, sat: 10, light: 40,
note: "Errante's velvety blacks were achieved through repeated rocking of the plate, a technique she taught at the Brera."
},
{
cat: "AC.1896.054", title: "Cypress Road, Evening", artist: "Henri Delacourt",
date: "1896", medium: "Coloured woodcut", dims: "21 × 30 cm",
credit: "Purchased, 1962", span: [3, 5],
hue: 96, sat: 24, light: 44,
note: "Three blocks in olive, ochre and a thin grey key. A rare complete impression with the artist's red seal."
},
{
cat: "AC.1932.290", title: "Self-Portrait, Turned", artist: "Iris Vanmeer",
date: "1932", medium: "Drypoint", dims: "18 × 13 cm", star: true,
credit: "Aldworth acquisition, 1949", span: [2, 6],
hue: 18, sat: 18, light: 58,
note: "The burr is unusually rich in this early state, lending the shoulder a smoky softness later impressions lose."
},
{
cat: "AC.1869.009", title: "Marsh Birds at Dusk", artist: "Gustav Reinholt",
date: "1869", medium: "Steel engraving", dims: "15 × 24 cm",
credit: "Bequest of M. Aldworth, 1947", span: [3, 5],
hue: 168, sat: 16, light: 50,
note: "A topographical plate later hand-finished by the artist with touches of bodycolour in the reeds."
},
{
cat: "AC.1915.142", title: "Two Pears on a Sill", artist: "Beatrix Lund",
date: "1915", medium: "Pastel on tinted paper", dims: "20 × 26 cm",
credit: "Gift of an anonymous donor", span: [2, 5],
hue: 48, sat: 40, light: 60,
note: "A late return to still life; the chalky greens recall her Antwerp watercolours four decades earlier."
}
];
var GRAD = [
[165, 14, 88], [25, 8, 96]
];
// --- SVG art placeholder -------------------------------------------------
function hsl(h, s, l) { return "hsl(" + h + "," + s + "%," + l + "%)"; }
function artSVG(w, i) {
var h = w.hue, s = w.sat, l = w.light;
var id = "g" + i;
var motif = "";
// a few abstract motifs so frames read differently
if (i % 3 === 0) {
motif =
'<circle cx="50" cy="58" r="20" fill="' + hsl(h, s + 8, Math.max(20, l - 20)) + '" opacity="0.55"/>' +
'<rect x="0" y="74" width="100" height="26" fill="' + hsl(h, s, Math.max(16, l - 30)) + '" opacity="0.45"/>';
} else if (i % 3 === 1) {
motif =
'<path d="M0 70 L28 44 L48 60 L74 30 L100 52 L100 100 L0 100 Z" fill="' + hsl(h, s + 6, Math.max(18, l - 24)) + '" opacity="0.5"/>';
} else {
motif =
'<rect x="18" y="20" width="64" height="60" fill="none" stroke="' + hsl(h, s, Math.max(20, l - 26)) + '" stroke-width="3" opacity="0.55"/>' +
'<line x1="18" y1="50" x2="82" y2="50" stroke="' + hsl(h, s, Math.max(20, l - 26)) + '" stroke-width="2" opacity="0.4"/>';
}
return (
'<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Artwork plate">' +
'<defs><linearGradient id="' + id + '" x1="0" y1="0" x2="0" y2="1">' +
'<stop offset="0" stop-color="' + hsl(h, s, Math.min(92, l + 18)) + '"/>' +
'<stop offset="1" stop-color="' + hsl(h, s, l) + '"/>' +
'</linearGradient></defs>' +
'<rect width="100" height="100" fill="url(#' + id + ')"/>' +
motif +
'</svg>'
);
}
// --- Render frames -------------------------------------------------------
var wall = document.getElementById("wall");
var railNote = document.getElementById("railNote");
WORKS.forEach(function (w, i) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "frame";
btn.dataset.index = String(i);
btn.style.setProperty("--col", w.span[0]);
btn.style.setProperty("--row", w.span[1]);
btn.style.gridColumn = "span " + w.span[0];
btn.style.gridRow = "span " + w.span[1];
btn.setAttribute(
"aria-label",
w.title + " by " + w.artist + ", " + w.date + ". Open label."
);
var badge = w.star
? '<span class="frame__badge frame__badge--star">Highlight</span>'
: '<span class="frame__badge">' + w.medium.split(" ")[0] + '</span>';
btn.innerHTML =
'<span class="frame__mat">' +
'<span class="frame__plate">' + badge + artSVG(w, i) + '</span>' +
'<span class="frame__cap">' +
'<span class="frame__title">' + w.title + '</span>' +
'<span class="frame__cat">' + w.cat + '</span>' +
'</span>' +
'<span class="frame__artist">' + w.artist + ', ' + w.date + '</span>' +
'</span>';
btn.addEventListener("click", function () { openLabel(i); });
wall.appendChild(btn);
});
// dim siblings while hovering anywhere over the wall
wall.addEventListener("mouseover", function (e) {
if (e.target.closest(".frame")) wall.classList.add("is-hovering");
});
wall.addEventListener("mouseleave", function () {
wall.classList.remove("is-hovering");
});
wall.addEventListener("mouseout", function (e) {
if (!e.relatedTarget || !wall.contains(e.relatedTarget)) {
wall.classList.remove("is-hovering");
}
});
// --- Layout switch -------------------------------------------------------
var segs = Array.prototype.slice.call(document.querySelectorAll(".seg"));
segs.forEach(function (seg) {
seg.addEventListener("click", function () {
var layout = seg.dataset.layout;
if (wall.dataset.layout === layout) return;
wall.dataset.layout = layout;
segs.forEach(function (s) {
var on = s === seg;
s.classList.toggle("is-active", on);
s.setAttribute("aria-pressed", on ? "true" : "false");
});
if (layout === "salon") {
railNote.textContent =
"Salon hang — works are massed by tonal weight, not chronology.";
toast("Salon hang");
} else {
railNote.textContent =
"Single-line works are hung to a common centre line at 145 cm.";
toast("Eye-level line");
}
});
});
// --- Label popover -------------------------------------------------------
var pop = document.getElementById("labelPop");
var lpClose = document.getElementById("lpClose");
var lastFocus = null;
function openLabel(i) {
var w = WORKS[i];
lastFocus = document.activeElement;
document.getElementById("lpCat").textContent = "Cat. no. " + w.cat;
document.getElementById("lpTitle").textContent = w.title;
document.getElementById("lpArtist").textContent = w.artist;
document.getElementById("lpDate").textContent = w.date;
document.getElementById("lpMedium").textContent = w.medium;
document.getElementById("lpDims").textContent = w.dims;
document.getElementById("lpCredit").textContent = w.credit;
document.getElementById("lpNote").textContent = w.note;
var tags = document.getElementById("lpTags");
tags.innerHTML = "";
[w.medium, w.star ? "Gallery highlight" : "On view", "Works on paper"].forEach(function (t) {
var el = document.createElement("span");
el.className = "tag";
el.textContent = t;
tags.appendChild(el);
});
pop.hidden = false;
lpClose.focus();
}
function closeLabel() {
pop.hidden = true;
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
lpClose.addEventListener("click", closeLabel);
pop.addEventListener("click", function (e) {
if (e.target === pop) closeLabel();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !pop.hidden) closeLabel();
});
// --- Toast helper --------------------------------------------------------
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 1800);
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Gallery Wall — Salon Hang</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>
<header class="topbar">
<div class="topbar__inner">
<div class="brand">
<span class="brand__mark" aria-hidden="true">◳</span>
<div class="brand__text">
<span class="brand__name">The Aldworth Collection</span>
<span class="brand__sub">Gallery 4 — European Works on Paper</span>
</div>
</div>
<div class="layout-switch" role="group" aria-label="Wall layout">
<span class="layout-switch__label">Hang</span>
<button class="seg is-active" type="button" data-layout="salon" aria-pressed="true">Salon</button>
<button class="seg" type="button" data-layout="line" aria-pressed="false">Eye-level line</button>
</div>
</div>
</header>
<main class="stage">
<div class="wall" id="wall" data-layout="salon" aria-label="Gallery wall with framed artworks">
<!-- frames injected by script.js -->
</div>
<p class="rail-note" id="railNote">Single-line works are hung to a common centre line at 145 cm.</p>
</main>
<!-- Label popover -->
<div class="label-pop" id="labelPop" role="dialog" aria-modal="true" aria-labelledby="lpTitle" hidden>
<div class="label-pop__card">
<button class="label-pop__close" id="lpClose" type="button" aria-label="Close label">×</button>
<span class="label-pop__cat" id="lpCat">Cat. no. —</span>
<h2 class="label-pop__title" id="lpTitle">—</h2>
<p class="label-pop__artist" id="lpArtist">—</p>
<dl class="label-pop__meta">
<div><dt>Date</dt><dd id="lpDate">—</dd></div>
<div><dt>Medium</dt><dd id="lpMedium">—</dd></div>
<div><dt>Dimensions</dt><dd id="lpDims">—</dd></div>
<div><dt>Credit</dt><dd id="lpCredit">—</dd></div>
</dl>
<p class="label-pop__note" id="lpNote">—</p>
<div class="label-pop__tags" id="lpTags"></div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Gallery Wall / Salon Hang
A wall surface for displaying framed artworks the way a gallery actually hangs them. In the default salon hang, nine fictional works on paper — etchings, lithographs, a mezzotint, watercolours — are massed across a six-column CSS grid, each frame spanning a different number of columns and rows so the wall reads as a balanced composition rather than an even strip. Mats, thin keylines and soft drop shadows give each piece the feel of being framed and lit.
Hovering any frame lifts it forward and gently dims its neighbours, focusing the eye the way directional gallery lighting would. Clicking a frame opens a typeset museum label popover with the catalogue number, artist, date, medium, dimensions, credit line and a short curatorial note. The popover traps focus, closes on Escape or backdrop click, and returns focus to the frame you came from.
A header toggle switches between the salon massing and a single eye-level line, where the same works are re-hung to a common centre rail — the convention for a focused, museum-style row. Everything is vanilla HTML, CSS and JavaScript: refined serif display type for titles, a quiet sans for UI, inline SVG plates for the imagery, a small toast() helper, and a responsive layout that collapses gracefully down to ~360px.
Illustrative UI only — demo data; not a real museum system.