Museum — Collection Browse
A curatorial collection-browse page for a fictional art museum. A masonry-style grid of artifact cards shows title, artist, date, medium, gallery, and catalog number, each framed like a hung work. A left filter rail refines by era, medium, gallery, and department with live result counts, while sort controls reorder by acquisition date or title. Clicking any object opens an accessible quick-view overlay with full tombstone details, all driven client-side over a seeded array of two dozen works.
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;
--shadow-sm: 0 1px 2px rgba(28, 27, 25, 0.06);
--shadow-md: 0 12px 34px -18px rgba(28, 27, 25, 0.4);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: var(--sans);
background: var(--paper);
color: var(--ink);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { margin: 0; font-weight: 600; }
a { color: inherit; }
button { font-family: inherit; }
:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ---------- Masthead ---------- */
.masthead {
background: var(--wall);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
}
.masthead-inner {
max-width: 1240px;
margin: 0 auto;
padding: 16px 28px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
font-size: 22px;
color: var(--gold);
line-height: 1;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.1; }
.brand-name {
font-family: var(--serif);
font-size: 22px;
font-weight: 700;
letter-spacing: 0.01em;
color: var(--charcoal);
}
.brand-sub {
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
}
.masthead-nav { display: flex; gap: 26px; }
.masthead-nav a {
text-decoration: none;
font-size: 14px;
color: var(--ink-2);
padding-bottom: 2px;
border-bottom: 1.5px solid transparent;
transition: color 0.15s, border-color 0.15s;
}
.masthead-nav a:hover { color: var(--charcoal); }
.masthead-nav a.is-active {
color: var(--charcoal);
border-color: var(--gold);
}
/* ---------- Hero ---------- */
.hero {
background: linear-gradient(180deg, var(--wall), var(--paper));
border-bottom: 1px solid var(--line);
}
.hero-inner {
max-width: 1240px;
margin: 0 auto;
padding: 52px 28px 44px;
}
.eyebrow {
margin: 0 0 14px;
font-size: 12px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--gold-d);
}
.hero h1 {
font-family: var(--serif);
font-size: clamp(34px, 5vw, 52px);
font-weight: 600;
letter-spacing: 0.005em;
color: var(--charcoal);
max-width: 16ch;
}
.hero-lede {
margin: 16px 0 0;
max-width: 60ch;
color: var(--ink-2);
font-size: 16px;
}
/* ---------- Layout ---------- */
.layout {
max-width: 1240px;
margin: 0 auto;
padding: 32px 28px 60px;
display: grid;
grid-template-columns: 248px 1fr;
gap: 40px;
align-items: start;
}
/* ---------- Filter rail ---------- */
.rail {
position: sticky;
top: 84px;
align-self: start;
}
.rail-head {
display: flex;
align-items: baseline;
justify-content: space-between;
padding-bottom: 14px;
margin-bottom: 6px;
border-bottom: 1px solid var(--line);
}
.rail-head h2 {
font-family: var(--serif);
font-size: 22px;
font-weight: 600;
color: var(--charcoal);
}
.link-btn {
background: none;
border: none;
color: var(--gold-d);
font-size: 13px;
cursor: pointer;
padding: 2px 0;
text-decoration: underline;
text-underline-offset: 2px;
}
.link-btn:hover { color: var(--charcoal); }
.filter-group { padding: 16px 0; border-bottom: 1px solid var(--line); }
.filter-title {
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 12px;
}
.filter-list { display: flex; flex-direction: column; gap: 3px; }
.facet {
display: flex;
align-items: center;
gap: 10px;
padding: 5px 8px;
border-radius: var(--r-sm);
cursor: pointer;
font-size: 14px;
color: var(--ink-2);
transition: background 0.12s, color 0.12s;
user-select: none;
}
.facet:hover { background: var(--gold-50); color: var(--charcoal); }
.facet input { position: absolute; opacity: 0; pointer-events: none; }
.facet .box {
width: 15px;
height: 15px;
border: 1.5px solid var(--line-2);
border-radius: 4px;
display: grid;
place-items: center;
flex: 0 0 auto;
transition: background 0.12s, border-color 0.12s;
}
.facet .box::after {
content: "";
width: 8px;
height: 8px;
border-radius: 2px;
background: var(--gold);
transform: scale(0);
transition: transform 0.12s ease;
}
.facet input:checked + .box { border-color: var(--gold); }
.facet input:checked + .box::after { transform: scale(1); }
.facet input:checked ~ .facet-name { color: var(--charcoal); font-weight: 500; }
.facet input:focus-visible + .box { outline: 2px solid var(--gold); outline-offset: 2px; }
.facet-name { flex: 1; }
.facet-count { color: var(--muted); font-size: 12px; }
/* ---------- Results ---------- */
.results { min-width: 0; }
.results-bar {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
padding-bottom: 18px;
margin-bottom: 22px;
border-bottom: 1px solid var(--line);
}
.result-count {
margin: 0;
font-family: var(--serif);
font-size: 22px;
font-weight: 600;
color: var(--charcoal);
align-self: center;
}
.result-count span { color: var(--gold-d); }
.results-tools { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 4px 6px 4px 11px;
background: var(--wall);
border: 1px solid var(--line-2);
border-radius: 999px;
font-size: 12.5px;
color: var(--ink);
}
.chip button {
border: none;
background: none;
cursor: pointer;
color: var(--muted);
font-size: 15px;
line-height: 1;
padding: 0 2px;
border-radius: 50%;
}
.chip button:hover { color: var(--danger); }
.sort { display: inline-flex; align-items: center; gap: 8px; }
.sort-label {
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
#sortSelect {
font-family: var(--sans);
font-size: 14px;
color: var(--ink);
background: var(--wall);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 7px 10px;
cursor: pointer;
}
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
gap: 28px 24px;
}
.card {
background: transparent;
border: none;
text-align: left;
padding: 0;
cursor: pointer;
display: flex;
flex-direction: column;
font-family: inherit;
animation: rise 0.4s ease both;
}
@keyframes rise {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.card-frame {
position: relative;
padding: 14px;
background: var(--wall);
border: 1px solid var(--line);
border-radius: var(--r-sm);
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s, transform 0.2s, border-color 0.2s;
}
.card:hover .card-frame {
box-shadow: var(--shadow-md);
transform: translateY(-3px);
border-color: var(--line-2);
}
.card:focus-visible { outline: none; }
.card:focus-visible .card-frame { outline: 2px solid var(--gold); outline-offset: 3px; }
.card-art {
aspect-ratio: 4 / 5;
border-radius: 3px;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(28, 27, 25, 0.08);
}
.card-art .sig {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: var(--serif);
font-size: 44px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
mix-blend-mode: overlay;
}
.dept-tag {
position: absolute;
top: 22px;
left: 22px;
z-index: 2;
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
background: rgba(255, 255, 255, 0.92);
color: var(--ink-2);
padding: 3px 8px;
border-radius: 999px;
box-shadow: var(--shadow-sm);
}
.card-info { padding: 14px 2px 0; }
.card-title {
font-family: var(--serif);
font-size: 19px;
font-weight: 600;
color: var(--charcoal);
line-height: 1.2;
}
.card-artist { font-size: 13.5px; color: var(--ink-2); margin-top: 3px; }
.card-meta {
margin-top: 8px;
font-size: 12.5px;
color: var(--muted);
display: flex;
flex-wrap: wrap;
gap: 4px 8px;
}
.card-meta .dot { color: var(--line-2); }
.card-cat {
margin-top: 9px;
font-size: 11px;
letter-spacing: 0.06em;
color: var(--gold-d);
font-variant-numeric: tabular-nums;
}
/* ---------- Empty ---------- */
.empty {
text-align: center;
padding: 70px 20px;
color: var(--ink-2);
}
.empty-mark { font-size: 46px; color: var(--line-2); margin: 0 0 6px; }
.empty h3 { font-family: var(--serif); font-size: 26px; color: var(--charcoal); }
.empty p { margin: 8px 0 18px; }
/* ---------- Buttons ---------- */
.btn {
font-family: var(--sans);
font-size: 14px;
font-weight: 500;
color: var(--wall);
background: var(--charcoal);
border: 1px solid var(--charcoal);
border-radius: var(--r-sm);
padding: 10px 18px;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.btn:hover { background: #000; }
.btn:active { transform: translateY(1px); }
.btn.ghost { background: transparent; color: var(--ink); border-color: var(--line-2); }
.btn.ghost:hover { background: var(--gold-50); }
/* ---------- Overlay / Quick view ---------- */
.overlay {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: 24px;
}
.overlay-backdrop {
position: absolute;
inset: 0;
background: rgba(28, 27, 25, 0.5);
backdrop-filter: blur(2px);
animation: fade 0.2s ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.quickview {
position: relative;
background: var(--wall);
border-radius: var(--r-lg);
max-width: 880px;
width: 100%;
max-height: 90vh;
overflow: auto;
display: grid;
grid-template-columns: 1fr 1fr;
box-shadow: 0 40px 80px -30px rgba(28, 27, 25, 0.6);
animation: pop 0.24s cubic-bezier(0.2, 0.8, 0.3, 1) both;
}
@keyframes pop { from { opacity: 0; transform: scale(0.97) translateY(8px); } to { opacity: 1; transform: none; } }
.qv-close {
position: absolute;
top: 12px;
right: 14px;
z-index: 3;
width: 34px;
height: 34px;
border-radius: 50%;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.9);
font-size: 22px;
line-height: 1;
color: var(--ink-2);
cursor: pointer;
}
.qv-close:hover { color: var(--danger); }
.qv-art {
position: relative;
min-height: 360px;
border-radius: var(--r-lg) 0 0 var(--r-lg);
overflow: hidden;
}
.qv-art .sig {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-family: var(--serif);
font-size: 86px;
font-weight: 600;
color: rgba(255, 255, 255, 0.7);
mix-blend-mode: overlay;
}
.qv-body { padding: 36px 34px; }
.qv-cat {
margin: 0 0 8px;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold-d);
font-variant-numeric: tabular-nums;
}
.qv-title {
font-family: var(--serif);
font-size: 30px;
font-weight: 600;
color: var(--charcoal);
line-height: 1.15;
}
.qv-artist { margin: 6px 0 18px; color: var(--ink-2); font-size: 15px; }
.qv-meta {
margin: 0 0 18px;
display: grid;
grid-template-columns: auto 1fr;
gap: 7px 16px;
border-top: 1px solid var(--line);
padding-top: 16px;
}
.qv-meta dt {
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
align-self: center;
}
.qv-meta dd { margin: 0; font-size: 14px; color: var(--ink); }
.qv-desc { font-size: 14.5px; color: var(--ink-2); margin: 0 0 22px; }
.qv-actions { display: flex; gap: 10px; flex-wrap: wrap; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
background: var(--charcoal);
color: var(--wall);
font-size: 14px;
padding: 11px 20px;
border-radius: 999px;
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 60;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Footer ---------- */
.footer {
border-top: 1px solid var(--line);
text-align: center;
padding: 30px 20px;
font-size: 13px;
color: var(--ink-2);
}
.footer .muted { color: var(--muted); margin-top: 4px; }
.muted { color: var(--muted); }
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; gap: 26px; }
.rail { position: static; }
.filter-group { padding: 12px 0; }
.quickview { grid-template-columns: 1fr; }
.qv-art { border-radius: var(--r-lg) var(--r-lg) 0 0; min-height: 260px; }
}
@media (max-width: 520px) {
.masthead-inner { padding: 14px 18px; flex-wrap: wrap; gap: 12px; }
.masthead-nav { gap: 18px; font-size: 13px; }
.hero-inner { padding: 36px 18px 30px; }
.layout { padding: 24px 18px 48px; }
.grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 22px 16px; }
.card-title { font-size: 17px; }
.results-bar { flex-direction: column; }
.results-tools { width: 100%; justify-content: space-between; }
.qv-body { padding: 24px 20px; }
.qv-title { font-size: 25px; }
.footer { padding: 24px 16px; }
}/* Meridian Museum — Collection Browse (vanilla JS, client-side) */
(function () {
"use strict";
// Gradient palettes used as artwork "images"
function art(a, b, angle) {
return "linear-gradient(" + angle + "deg, " + a + ", " + b + ")";
}
// Seeded collection — fictional objects
var OBJECTS = [
{ id: 1, title: "Harbour at First Light", artist: "Eulalie Verne", year: 1873, acq: 1924, era: "19th Century", medium: "Painting", gallery: "Gallery 4", department: "European Paintings", catalog: "MM.1924.041", g: art("#3a5a78", "#9fb6c4", 145), sig: "EV", desc: "Oil on canvas. A study of dawn over the northern docks, prized for its restrained palette and the cold luminosity of its sky." },
{ id: 2, title: "Bronze Figure, Reclining", artist: "Tomás Aldebrand", year: 1961, acq: 1998, era: "Modern", medium: "Sculpture", gallery: "Gallery 9", department: "Modern & Contemporary", catalog: "MM.1998.207", g: art("#6b5a3e", "#3a2f1f", 160), sig: "TA", desc: "Cast bronze with a deep umber patina. Aldebrand's reclining form distills the human body into three continuous gestures." },
{ id: 3, title: "Loom Fragment, Saffron Border", artist: "Unknown (Kesh region)", year: 1480, acq: 1907, era: "Pre-1600", medium: "Textile", gallery: "Gallery 1", department: "Textiles & Costume", catalog: "MM.1907.012", g: art("#c79a3a", "#7a4f1d", 135), sig: "✦", desc: "Silk and gold-wrapped thread. A surviving border fragment whose saffron field has held its intensity for five centuries." },
{ id: 4, title: "The Cartographer's Table", artist: "Iris Mwangi", year: 2014, acq: 2016, era: "Contemporary", medium: "Painting", gallery: "Gallery 11", department: "Modern & Contemporary", catalog: "MM.2016.318", g: art("#7a3b52", "#2a1822", 150), sig: "IM", desc: "Acrylic and graphite on panel. Mwangi layers route lines and erasures into a meditation on mapping and forgetting." },
{ id: 5, title: "Study of Hands", artist: "Eulalie Verne", year: 1869, acq: 1924, era: "19th Century", medium: "Works on Paper", gallery: "Gallery 4", department: "Prints & Drawings", catalog: "MM.1924.039", g: art("#d8cdb6", "#a89b7f", 120), sig: "EV", desc: "Red chalk on laid paper. Six positions of a sitter's hands, drawn in a single afternoon according to the artist's notes." },
{ id: 6, title: "Amphora with Dancers", artist: "Workshop of Halios", year: -430, acq: 1889, era: "Antiquity", medium: "Ceramic", gallery: "Gallery 2", department: "Ancient Art", catalog: "MM.1889.004", g: art("#b1633a", "#2a1410", 140), sig: "⚱", desc: "Terracotta with black-figure decoration. A procession of dancers circles the body of this storage vessel." },
{ id: 7, title: "Nocturne in Indigo", artist: "Søren Haugaard", year: 1908, acq: 1951, era: "20th Century", medium: "Painting", gallery: "Gallery 6", department: "European Paintings", catalog: "MM.1951.122", g: art("#1f2a4a", "#3f5a8a", 155), sig: "SH", desc: "Oil on canvas. A near-monochrome evening interior in which a single lamp anchors the composition." },
{ id: 8, title: "Pendant, Coiled Serpent", artist: "Unknown (Volterra)", year: 1290, acq: 1912, era: "Pre-1600", medium: "Decorative Arts", gallery: "Gallery 1", department: "Decorative Arts", catalog: "MM.1912.058", g: art("#c9a227", "#6e4f12", 130), sig: "❖", desc: "Gold and enamel. A devotional pendant whose coiled serpent motif suggests guild ownership rather than personal piety." },
{ id: 9, title: "Field Recordings (Triptych)", artist: "Priya Sundaram", year: 2019, acq: 2021, era: "Contemporary", medium: "Photography", gallery: "Gallery 12", department: "Photography", catalog: "MM.2021.402", g: art("#4a6b4a", "#1c2a1c", 150), sig: "PS", desc: "Archival pigment prints. Three views of the same wetland at successive dawns, printed at large scale." },
{ id: 10, title: "Marble Head of a Youth", artist: "Roman copy after Greek original", year: 120, acq: 1894, era: "Antiquity", medium: "Sculpture", gallery: "Gallery 2", department: "Ancient Art", catalog: "MM.1894.019", g: art("#d6d2c6", "#9b9588", 125), sig: "⚜", desc: "Marble. A serene youth, the surface lightly weathered, likely once part of a full standing figure." },
{ id: 11, title: "Quilt, Lantern Court", artist: "Hannah Fells", year: 1851, acq: 1977, era: "19th Century", medium: "Textile", gallery: "Gallery 3", department: "Textiles & Costume", catalog: "MM.1977.165", g: art("#b4493a", "#6b2a22", 135), sig: "HF", desc: "Pieced cotton. A pattern of interlocking lanterns assembled from dress scraps over an entire winter." },
{ id: 12, title: "Self-Portrait at the Window", artist: "Søren Haugaard", year: 1921, acq: 1951, era: "20th Century", medium: "Painting", gallery: "Gallery 6", department: "European Paintings", catalog: "MM.1951.130", g: art("#5a4a3a", "#2a2018", 145), sig: "SH", desc: "Oil on canvas. The artist regards the viewer beside a pane of leaded glass, the only one in his late work." },
{ id: 13, title: "Vessel, Folded Form", artist: "Mei-Ling Chao", year: 1987, acq: 2003, era: "Modern", medium: "Ceramic", gallery: "Gallery 9", department: "Decorative Arts", catalog: "MM.2003.261", g: art("#7a7468", "#3a3631", 160), sig: "MC", desc: "Stoneware with ash glaze. The walls appear pressed inward, a signature of Chao's mature studio period." },
{ id: 14, title: "Migration Map No. 3", artist: "Iris Mwangi", year: 2017, acq: 2018, era: "Contemporary", medium: "Works on Paper", gallery: "Gallery 11", department: "Prints & Drawings", catalog: "MM.2018.355", g: art("#3a6b78", "#163038", 150), sig: "IM", desc: "Screenprint and ink. Overlapping flight paths rendered in successive translucent layers." },
{ id: 15, title: "The Orchard Keeper", artist: "Beatrix Lund", year: 1894, acq: 1939, era: "19th Century", medium: "Painting", gallery: "Gallery 5", department: "European Paintings", catalog: "MM.1939.088", g: art("#6b7a3a", "#2f3a18", 140), sig: "BL", desc: "Oil on canvas. A figure pauses among low fruit trees, the light filtered to a dusty gold." },
{ id: 16, title: "Ceremonial Mantle", artist: "Unknown (Andean)", year: 600, acq: 1921, era: "Antiquity", medium: "Textile", gallery: "Gallery 2", department: "Ancient Art", catalog: "MM.1921.073", g: art("#a8442f", "#3a1813", 130), sig: "✦", desc: "Camelid fiber. A mantle whose stepped motifs retain remarkable saturation given its age." },
{ id: 17, title: "Untitled (Red Threshold)", artist: "Caleb Ferro", year: 1972, acq: 1990, era: "Modern", medium: "Painting", gallery: "Gallery 10", department: "Modern & Contemporary", catalog: "MM.1990.194", g: art("#b4493a", "#7a2018", 155), sig: "CF", desc: "Oil and wax on linen. A single vertical band divides a field of layered crimson." },
{ id: 18, title: "Letter Press, Type Specimen", artist: "Atelier Brun", year: 1788, acq: 1955, era: "Pre-1800", medium: "Works on Paper", gallery: "Gallery 7", department: "Prints & Drawings", catalog: "MM.1955.141", g: art("#cfc7b3", "#8c8470", 120), sig: "AB", desc: "Letterpress on rag paper. A full specimen sheet from a short-lived provincial foundry." },
{ id: 19, title: "Standing Crane, Lacquer", artist: "Workshop of Sano", year: 1690, acq: 1933, era: "Pre-1800", medium: "Decorative Arts", gallery: "Gallery 8", department: "Decorative Arts", catalog: "MM.1933.077", g: art("#1c1b19", "#4a4036", 150), sig: "❖", desc: "Lacquered wood with gold maki-e. A crane in profile, wings half-raised, on a black ground." },
{ id: 20, title: "Coastline, Fog", artist: "Priya Sundaram", year: 2020, acq: 2022, era: "Contemporary", medium: "Photography", gallery: "Gallery 12", department: "Photography", catalog: "MM.2022.418", g: art("#8c95a0", "#3a4048", 150), sig: "PS", desc: "Archival pigment print. The horizon dissolves entirely, leaving a graded field of grey." },
{ id: 21, title: "Reliquary Casket", artist: "Unknown (Limoges)", year: 1230, acq: 1901, era: "Pre-1600", medium: "Decorative Arts", gallery: "Gallery 1", department: "Decorative Arts", catalog: "MM.1901.006", g: art("#3a6b78", "#c9a227", 130), sig: "❖", desc: "Champlevé enamel on copper. Saints in procession across the lid, the blues still vivid." },
{ id: 22, title: "Two Figures, Argument", artist: "Tomás Aldebrand", year: 1958, acq: 1998, era: "Modern", medium: "Works on Paper", gallery: "Gallery 9", department: "Prints & Drawings", catalog: "MM.1998.211", g: art("#4a4640", "#1c1b19", 145), sig: "TA", desc: "Charcoal on paper. A preparatory sketch for the artist's later bronze of the same name." },
{ id: 23, title: "Garden Wall, Summer", artist: "Beatrix Lund", year: 1901, acq: 1939, era: "20th Century", medium: "Painting", gallery: "Gallery 5", department: "European Paintings", catalog: "MM.1939.094", g: art("#c2a86b", "#7a6238", 140), sig: "BL", desc: "Oil on board. A sun-warmed garden wall, the brushwork loosening toward the upper edge." },
{ id: 24, title: "Suspended (Glass)", artist: "Mei-Ling Chao", year: 2009, acq: 2012, era: "Contemporary", medium: "Sculpture", gallery: "Gallery 12", department: "Modern & Contemporary", catalog: "MM.2012.297", g: art("#9fb6c4", "#3a5a78", 160), sig: "MC", desc: "Blown and cut glass. Twelve forms hung at varied heights, refracting the gallery's daylight." }
];
var FACETS = ["era", "medium", "gallery", "department"];
// Preserve a sensible chronological order for eras
var ERA_ORDER = ["Antiquity", "Pre-1600", "Pre-1800", "19th Century", "20th Century", "Modern", "Contemporary"];
var state = { era: new Set(), medium: new Set(), gallery: new Set(), department: new Set(), sort: "newest" };
var saved = new Set();
var grid = document.getElementById("grid");
var empty = document.getElementById("empty");
var resultCount = document.getElementById("resultCount");
var activeChips = document.getElementById("activeChips");
var sortSelect = document.getElementById("sortSelect");
var labels = {
era: "Era", medium: "Medium", gallery: "Gallery", department: "Department"
};
function uniqueValues(facet) {
var counts = {};
OBJECTS.forEach(function (o) {
counts[o[facet]] = (counts[o[facet]] || 0) + 1;
});
var keys = Object.keys(counts);
if (facet === "era") {
keys.sort(function (a, b) { return ERA_ORDER.indexOf(a) - ERA_ORDER.indexOf(b); });
} else {
keys.sort(function (a, b) {
// natural-ish sort so Gallery 2 < Gallery 11
return a.localeCompare(b, undefined, { numeric: true });
});
}
return keys.map(function (k) { return { value: k, count: counts[k] }; });
}
function buildFacets() {
FACETS.forEach(function (facet) {
var container = document.querySelector('[data-facet="' + facet + '"] .filter-list');
container.innerHTML = "";
uniqueValues(facet).forEach(function (item) {
var id = facet + "-" + item.value.replace(/[^a-z0-9]+/gi, "-");
var label = document.createElement("label");
label.className = "facet";
label.innerHTML =
'<input type="checkbox" value="' + escapeAttr(item.value) + '" id="' + id + '">' +
'<span class="box" aria-hidden="true"></span>' +
'<span class="facet-name">' + escapeHtml(item.value) + "</span>" +
'<span class="facet-count">' + item.count + "</span>";
var input = label.querySelector("input");
input.addEventListener("change", function () {
if (input.checked) state[facet].add(item.value);
else state[facet].delete(item.value);
render();
});
container.appendChild(label);
});
});
}
function matches(o) {
return FACETS.every(function (facet) {
var sel = state[facet];
return sel.size === 0 || sel.has(o[facet]);
});
}
function sortObjects(list) {
var s = state.sort;
return list.slice().sort(function (a, b) {
if (s === "newest") return b.acq - a.acq || a.title.localeCompare(b.title);
if (s === "oldest") return a.acq - b.acq || a.title.localeCompare(b.title);
if (s === "az") return a.title.localeCompare(b.title);
if (s === "za") return b.title.localeCompare(a.title);
return 0;
});
}
function yearLabel(y) {
return y < 0 ? Math.abs(y) + " BCE" : String(y);
}
function render() {
var filtered = sortObjects(OBJECTS.filter(matches));
// Result count
var total = OBJECTS.length;
resultCount.innerHTML =
"<span>" + filtered.length + "</span> of " + total + " objects";
// Active chips
renderChips();
// Grid
grid.innerHTML = "";
if (filtered.length === 0) {
empty.hidden = false;
grid.hidden = true;
return;
}
empty.hidden = true;
grid.hidden = false;
filtered.forEach(function (o, i) {
var card = document.createElement("button");
card.type = "button";
card.className = "card";
card.style.animationDelay = Math.min(i * 22, 320) + "ms";
card.setAttribute("aria-label", o.title + " by " + o.artist + ", quick view");
card.innerHTML =
'<div class="card-frame">' +
'<span class="dept-tag">' + escapeHtml(o.department) + "</span>" +
'<div class="card-art" style="background:' + o.g + '"><span class="sig">' + escapeHtml(o.sig) + "</span></div>" +
"</div>" +
'<div class="card-info">' +
'<div class="card-title">' + escapeHtml(o.title) + "</div>" +
'<div class="card-artist">' + escapeHtml(o.artist) + "</div>" +
'<div class="card-meta">' +
"<span>" + yearLabel(o.year) + "</span>" +
'<span class="dot">·</span><span>' + escapeHtml(o.medium) + "</span>" +
'<span class="dot">·</span><span>' + escapeHtml(o.gallery) + "</span>" +
"</div>" +
'<div class="card-cat">' + escapeHtml(o.catalog) + "</div>" +
"</div>";
card.addEventListener("click", function () { openQuickView(o); });
grid.appendChild(card);
});
}
function renderChips() {
activeChips.innerHTML = "";
FACETS.forEach(function (facet) {
state[facet].forEach(function (val) {
var chip = document.createElement("span");
chip.className = "chip";
chip.innerHTML = escapeHtml(val) +
'<button type="button" aria-label="Remove filter ' + escapeAttr(val) + '">×</button>';
chip.querySelector("button").addEventListener("click", function () {
state[facet].delete(val);
var cb = document.querySelector('[data-facet="' + facet + '"] input[value="' + cssEscape(val) + '"]');
if (cb) cb.checked = false;
render();
});
activeChips.appendChild(chip);
});
});
}
/* ---------- Quick view ---------- */
var overlay = document.getElementById("overlay");
var qv = overlay.querySelector(".quickview");
var lastFocus = null;
function openQuickView(o) {
lastFocus = document.activeElement;
document.getElementById("qvArt").style.background = o.g;
document.getElementById("qvArt").innerHTML = '<span class="sig">' + escapeHtml(o.sig) + "</span>";
document.getElementById("qvCat").textContent = o.catalog;
document.getElementById("qvTitle").textContent = o.title;
document.getElementById("qvArtist").textContent = o.artist + ", " + yearLabel(o.year);
document.getElementById("qvDesc").textContent = o.desc;
var meta = document.getElementById("qvMeta");
var rows = [
["Medium", o.medium],
["Department", o.department],
["Gallery", o.gallery],
["Era", o.era],
["Acquired", o.acq],
["Object no.", o.catalog]
];
meta.innerHTML = rows.map(function (r) {
return "<dt>" + escapeHtml(r[0]) + "</dt><dd>" + escapeHtml(String(r[1])) + "</dd>";
}).join("");
var saveBtn = document.getElementById("qvSave");
setSaveBtn(saveBtn, o);
saveBtn.onclick = function () {
if (saved.has(o.id)) { saved.delete(o.id); toast("Removed from your list"); }
else { saved.add(o.id); toast("Saved “" + o.title + "”"); }
setSaveBtn(saveBtn, o);
};
overlay.hidden = false;
document.body.style.overflow = "hidden";
qv.focus();
}
function setSaveBtn(btn, o) {
btn.textContent = saved.has(o.id) ? "✓ Saved to my list" : "Save to my list";
}
function closeQuickView() {
overlay.hidden = true;
document.body.style.overflow = "";
if (lastFocus) lastFocus.focus();
}
overlay.addEventListener("click", function (e) {
if (e.target.hasAttribute("data-close")) closeQuickView();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !overlay.hidden) closeQuickView();
});
/* ---------- Controls ---------- */
sortSelect.addEventListener("change", function () {
state.sort = sortSelect.value;
render();
});
document.getElementById("clearAll").addEventListener("click", clearAll);
document.getElementById("emptyReset").addEventListener("click", clearAll);
function clearAll() {
FACETS.forEach(function (f) { state[f].clear(); });
document.querySelectorAll('.filter-list input[type="checkbox"]').forEach(function (cb) { cb.checked = false; });
render();
toast("Filters cleared");
}
/* ---------- Toast ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
/* ---------- Helpers ---------- */
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function escapeAttr(s) { return escapeHtml(s); }
function cssEscape(s) { return s.replace(/(["\\])/g, "\\$1"); }
/* ---------- Init ---------- */
buildFacets();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Meridian Museum — Permanent Collection</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="masthead">
<div class="masthead-inner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◈</span>
<div class="brand-text">
<span class="brand-name">Meridian Museum</span>
<span class="brand-sub">Permanent Collection</span>
</div>
</div>
<nav class="masthead-nav" aria-label="Primary">
<a href="#" class="is-active">Browse</a>
<a href="#">Exhibitions</a>
<a href="#">Visit</a>
<a href="#">Research</a>
</nav>
</div>
</header>
<div class="hero">
<div class="hero-inner">
<p class="eyebrow">Catalogue · 1847 objects on view</p>
<h1>Browse the permanent collection</h1>
<p class="hero-lede">Paintings, sculpture, textiles, and works on paper drawn from five centuries of gift and acquisition. Filter by era, medium, gallery, and department.</p>
</div>
</div>
<main class="layout" id="top">
<!-- FILTER RAIL -->
<aside class="rail" aria-label="Filters">
<div class="rail-head">
<h2>Refine</h2>
<button type="button" class="link-btn" id="clearAll">Clear all</button>
</div>
<section class="filter-group" data-facet="era">
<h3 class="filter-title">Era</h3>
<div class="filter-list" role="group" aria-label="Era"></div>
</section>
<section class="filter-group" data-facet="medium">
<h3 class="filter-title">Medium</h3>
<div class="filter-list" role="group" aria-label="Medium"></div>
</section>
<section class="filter-group" data-facet="gallery">
<h3 class="filter-title">Gallery</h3>
<div class="filter-list" role="group" aria-label="Gallery"></div>
</section>
<section class="filter-group" data-facet="department">
<h3 class="filter-title">Department</h3>
<div class="filter-list" role="group" aria-label="Department"></div>
</section>
</aside>
<!-- RESULTS -->
<section class="results" aria-label="Collection objects">
<div class="results-bar">
<p class="result-count" id="resultCount" aria-live="polite">Loading…</p>
<div class="results-tools">
<div class="chips" id="activeChips" aria-label="Active filters"></div>
<label class="sort">
<span class="sort-label">Sort</span>
<select id="sortSelect" aria-label="Sort objects">
<option value="newest">Newest acquisitions</option>
<option value="oldest">Oldest acquisitions</option>
<option value="az">Title A–Z</option>
<option value="za">Title Z–A</option>
</select>
</label>
</div>
</div>
<div class="grid" id="grid"></div>
<div class="empty" id="empty" hidden>
<p class="empty-mark" aria-hidden="true">⌗</p>
<h3>No objects match these filters</h3>
<p>Try removing a refinement to widen the search.</p>
<button type="button" class="btn" id="emptyReset">Reset filters</button>
</div>
</section>
</main>
<!-- QUICK VIEW -->
<div class="overlay" id="overlay" hidden>
<div class="overlay-backdrop" data-close></div>
<div class="quickview" role="dialog" aria-modal="true" aria-labelledby="qvTitle" tabindex="-1">
<button type="button" class="qv-close" data-close aria-label="Close quick view">×</button>
<div class="qv-art" id="qvArt"></div>
<div class="qv-body">
<p class="qv-cat" id="qvCat"></p>
<h2 class="qv-title" id="qvTitle"></h2>
<p class="qv-artist" id="qvArtist"></p>
<dl class="qv-meta" id="qvMeta"></dl>
<p class="qv-desc" id="qvDesc"></p>
<div class="qv-actions">
<button type="button" class="btn" id="qvSave">Save to my list</button>
<button type="button" class="btn ghost" data-close>Close</button>
</div>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<footer class="footer">
<p>Meridian Museum of Art · 14 Lantern Court · Tues–Sun 10–6</p>
<p class="muted">Illustrative UI — demo data only.</p>
</footer>
<script src="script.js"></script>
</body>
</html>Collection Browse
A refined, gallery-like browse experience for a permanent museum collection. The page pairs a sticky left filter rail with a responsive grid of framed artifact cards, each presenting an object’s title in a serif display face alongside its artist, date, medium, gallery, and catalog number. Solid gradient blocks stand in for artwork imagery, matted inside thin white frames to evoke works hung on a wall.
The filter rail offers four faceted groups — era, medium, gallery, and department — each rendered with live per-value counts derived from the data. Toggling any facet filters the grid instantly, updates a running result count, and adds a removable chip to the results bar. A sort control reorders objects by newest or oldest acquisition, or by title A–Z and Z–A. When no objects match, an empty state offers a one-click reset.
Clicking a card opens a modal quick view with the object’s gradient enlarged beside a full tombstone: medium, department, gallery, era, acquisition year, and object number, plus a short curatorial note and a save-to-list toggle. The overlay traps focus, closes on Escape or backdrop click, and restores focus to the originating card. Everything runs in vanilla JavaScript over a seeded array of twenty-four fictional works, with a small toast helper confirming actions.
Illustrative UI only — demo data; not a real museum system.