Museum — Fine Art Museum Landing
A stately marketing landing page for a fictional fine art museum, built in refined gallery-white, deep charcoal, and gold. It pairs a featured masterwork hero with a filterable current-and-upcoming exhibitions strip, savable collection highlights, an interactive admission ticket builder with live totals, visit hours, and tiered membership plans. Vanilla HTML, CSS, and JavaScript with a Cormorant Garamond serif display, accessible dialogs, toasts, and a fully responsive layout down to 360px.
MCP
Code
:root {
--paper: #f6f4ef;
--wall: #ffffff;
--charcoal: #1b1a18;
--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: 0 1px 2px rgba(28, 27, 25, 0.05), 0 12px 32px -16px rgba(28, 27, 25, 0.22);
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
font-size: 16px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { font-family: var(--serif); font-weight: 600; color: var(--charcoal); line-height: 1.1; margin: 0; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
.wrap { width: min(1160px, 100% - 3rem); margin-inline: auto; }
.skip {
position: absolute; left: -999px; top: 0; z-index: 100;
background: var(--charcoal); color: #fff; padding: 0.6rem 1rem; border-radius: 0 0 var(--r-sm) 0;
}
.skip:focus { left: 0; }
:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; border-radius: 3px; }
.eyebrow {
font-family: var(--sans);
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 0.72rem;
font-weight: 600;
color: var(--gold-d);
margin: 0 0 0.6rem;
}
/* ---------- buttons ---------- */
.btn {
font-family: var(--sans);
font-weight: 600;
font-size: 0.92rem;
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 0.7rem 1.3rem;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: transform 0.15s ease, background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
background: none;
}
.btn:active { transform: translateY(1px); }
.btn--solid { background: var(--charcoal); color: #fff; }
.btn--solid:hover { background: #000; box-shadow: 0 8px 22px -10px rgba(0,0,0,0.5); }
.btn--line { border-color: var(--line-2); color: var(--ink); }
.btn--line:hover { border-color: var(--charcoal); background: rgba(28,27,25,0.03); }
.btn--ghost { color: var(--ink-2); padding: 0.5rem 0.7rem; }
.btn--ghost:hover { color: var(--charcoal); }
.btn--lg { padding: 0.95rem 1.7rem; font-size: 1rem; }
.btn--block { width: 100%; }
.link {
font-family: var(--sans);
font-weight: 600;
font-size: 0.9rem;
color: var(--gold-d);
background: none; border: 0; cursor: pointer; padding: 0;
transition: color 0.18s ease, letter-spacing 0.18s ease;
}
.link:hover { color: var(--charcoal); letter-spacing: 0.01em; }
.link--lg { font-size: 0.98rem; }
/* ---------- topbar ---------- */
.topbar {
position: sticky; top: 0; z-index: 40;
background: rgba(255,255,255,0.85);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar__inner { display: flex; align-items: center; gap: 1.5rem; height: 74px; }
.brand { display: flex; align-items: center; gap: 0.7rem; margin-right: auto; }
.brand__mark {
width: 40px; height: 40px; flex: none;
display: grid; place-items: center;
border: 1px solid var(--charcoal);
font-family: var(--serif); font-weight: 700; font-size: 1.4rem;
color: var(--charcoal); background: var(--wall);
}
.brand__text { display: flex; flex-direction: column; line-height: 1.05; }
.brand__text strong { font-family: var(--serif); font-size: 1.2rem; font-weight: 600; color: var(--charcoal); }
.brand__text small { font-size: 0.68rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); }
.nav { display: flex; gap: 1.6rem; }
.nav a {
font-size: 0.9rem; font-weight: 500; color: var(--ink-2);
position: relative; padding: 0.2rem 0;
}
.nav a::after {
content: ""; position: absolute; left: 0; bottom: -2px; height: 1px; width: 0;
background: var(--gold); transition: width 0.25s ease;
}
.nav a:hover { color: var(--charcoal); }
.nav a:hover::after { width: 100%; }
.topbar__actions { display: flex; align-items: center; gap: 0.6rem; }
.hamburger { display: none; flex-direction: column; gap: 4px; background: none; border: 0; cursor: pointer; padding: 6px; }
.hamburger span { width: 22px; height: 2px; background: var(--charcoal); transition: 0.25s; }
/* ---------- hero ---------- */
.hero { padding: clamp(2.5rem, 6vw, 5.5rem) 0 clamp(3rem, 6vw, 6rem); }
.hero__grid { display: grid; grid-template-columns: 1.05fr 0.95fr; gap: clamp(2rem, 5vw, 5rem); align-items: center; }
.hero__copy h1 { font-size: clamp(2.6rem, 6vw, 4.6rem); font-weight: 600; letter-spacing: -0.01em; margin-bottom: 1.4rem; }
.hero__copy h1 em { color: var(--gold); font-style: italic; }
.lede { font-size: 1.12rem; color: var(--ink-2); max-width: 38ch; margin: 0 0 2rem; }
.hero__cta { display: flex; gap: 0.8rem; flex-wrap: wrap; margin-bottom: 2.4rem; }
.hero__stats { display: flex; gap: 2.4rem; margin: 0; padding-top: 1.6rem; border-top: 1px solid var(--line); }
.hero__stats dt { font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.2rem; }
.hero__stats dd { margin: 0; font-family: var(--serif); font-size: 1.7rem; font-weight: 600; color: var(--charcoal); }
.hero__art { margin: 0; outline: none; }
.frame {
position: relative;
background: var(--wall);
padding: 14px;
border: 1px solid var(--line-2);
box-shadow: var(--shadow);
}
.frame--hero { padding: 18px; border: 2px solid var(--charcoal); box-shadow: 0 30px 60px -30px rgba(28,27,25,0.5); }
.canvas { aspect-ratio: 4 / 3; background: linear-gradient(135deg, var(--c1, #888), var(--c2, #333)); }
.canvas--hero { aspect-ratio: 4 / 5; overflow: hidden; }
.canvas--hero svg { width: 100%; height: 100%; }
.hero__art:hover .frame--hero { transform: translateY(-4px); transition: transform 0.35s ease; }
.cap { display: flex; flex-direction: column; gap: 0.15rem; margin-top: 1rem; padding-left: 0.2rem; }
.cap__title { font-family: var(--serif); font-size: 1.25rem; font-weight: 600; color: var(--charcoal); }
.cap__meta { font-size: 0.86rem; color: var(--ink-2); }
.cap__no { font-size: 0.74rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
/* ---------- section heads ---------- */
.sec-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 1.5rem; margin-bottom: 2.4rem; flex-wrap: wrap; }
.sec-head h2 { font-size: clamp(2rem, 4vw, 3rem); }
.sec-head--center { justify-content: center; text-align: center; }
.sec-sub { color: var(--ink-2); max-width: 46ch; margin: 0.7rem auto 0; }
/* ---------- exhibitions ---------- */
.exhibitions { padding: clamp(3rem, 6vw, 5rem) 0; background: var(--wall); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.filterbar { display: flex; gap: 0.5rem; }
.chip {
font-family: var(--sans); font-size: 0.84rem; font-weight: 500;
border: 1px solid var(--line-2); background: transparent; color: var(--ink-2);
padding: 0.45rem 1rem; border-radius: 999px; cursor: pointer; transition: 0.2s;
}
.chip:hover { border-color: var(--charcoal); color: var(--charcoal); }
.chip.is-active { background: var(--charcoal); color: #fff; border-color: var(--charcoal); }
.exh-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.8rem; }
.exh {
display: grid; grid-template-columns: 200px 1fr; gap: 1.4rem;
align-items: stretch; padding-bottom: 1.6rem; border-bottom: 1px solid var(--line);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.exh .frame { padding: 8px; align-self: start; }
.exh .canvas { aspect-ratio: 3 / 4; }
.badge {
position: absolute; top: 14px; left: 14px;
font-size: 0.68rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
padding: 0.25rem 0.6rem; border-radius: var(--r-sm); color: #fff;
}
.badge--ok { background: var(--ok); }
.badge--soon { background: var(--warn); }
.exh__body h3 { font-size: 1.55rem; margin-bottom: 0.4rem; }
.exh__dates { font-size: 0.82rem; letter-spacing: 0.04em; color: var(--gold-d); font-weight: 600; margin: 0 0 0.6rem; }
.exh__desc { font-size: 0.95rem; color: var(--ink-2); margin: 0 0 0.9rem; }
.exh.is-hidden { display: none; }
/* ---------- collection ---------- */
.collection { padding: clamp(3rem, 6vw, 5.5rem) 0; }
.col-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.6rem; }
.work { margin: 0; position: relative; }
.frame--mat { padding: 12px; background: var(--wall); }
.frame--mat::after { content: ""; position: absolute; inset: 12px; border: 1px solid var(--line); pointer-events: none; }
.canvas--portrait { aspect-ratio: 3 / 4; }
.work figcaption { padding: 0.9rem 0.2rem 0; }
.work figcaption h3 { font-size: 1.18rem; margin-bottom: 0.2rem; }
.work figcaption p { margin: 0; font-size: 0.85rem; color: var(--ink-2); }
.work__med { color: var(--muted) !important; font-size: 0.78rem !important; font-style: italic; }
.work .frame { transition: transform 0.3s ease, box-shadow 0.3s ease; }
.work:hover .frame, .work:focus-within .frame { transform: translateY(-5px); box-shadow: 0 20px 40px -22px rgba(28,27,25,0.45); }
.save {
position: absolute; top: 22px; right: 22px;
width: 36px; height: 36px; border-radius: 999px;
border: 1px solid var(--line-2); background: rgba(255,255,255,0.9);
cursor: pointer; font-size: 1.1rem; line-height: 1; color: var(--ink-2);
display: grid; place-items: center; transition: 0.2s; opacity: 0;
}
.work:hover .save, .work:focus-within .save, .save[aria-pressed="true"] { opacity: 1; }
.save:hover { border-color: var(--gold); color: var(--gold); }
.save[aria-pressed="true"] { background: var(--gold); color: #fff; border-color: var(--gold); }
.saved-note { margin-top: 1.4rem; font-size: 0.9rem; color: var(--gold-d); font-weight: 500; min-height: 1.2em; }
/* ---------- visit ---------- */
.visit { padding: clamp(3rem, 6vw, 5.5rem) 0; background: var(--charcoal); color: var(--paper); }
.visit h2 { color: var(--wall); }
.visit .eyebrow { color: #d6b06a; }
.visit__grid { display: grid; grid-template-columns: 1fr 0.85fr; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
.hours { list-style: none; margin: 1.5rem 0; padding: 0; max-width: 360px; }
.hours li { display: flex; justify-content: space-between; padding: 0.7rem 0; border-bottom: 1px solid rgba(255,255,255,0.12); font-size: 0.96rem; }
.hours li span:first-child { color: rgba(246,244,239,0.75); }
.hours__closed span:last-child { color: #d68f82; }
.addr { font-style: normal; color: rgba(246,244,239,0.7); font-size: 0.92rem; line-height: 1.7; }
.ticket-card { background: var(--wall); color: var(--ink); border-radius: var(--r-lg); padding: 1.8rem; box-shadow: 0 30px 60px -30px rgba(0,0,0,0.6); }
.ticket-card h3 { font-size: 1.7rem; }
.ticket-card__sub { font-size: 0.88rem; color: var(--muted); margin: 0.2rem 0 1.3rem; }
.ticket-list { display: flex; flex-direction: column; gap: 0.6rem; }
.ticket {
display: flex; justify-content: space-between; align-items: center;
border: 1px solid var(--line-2); border-radius: var(--r-md); background: var(--wall);
padding: 0.85rem 1.1rem; cursor: pointer; font-family: var(--sans); transition: 0.18s;
}
.ticket:hover { border-color: var(--charcoal); }
.ticket.is-selected { border-color: var(--gold); background: var(--gold-50); box-shadow: inset 0 0 0 1px var(--gold); }
.ticket__name { font-weight: 500; font-size: 0.96rem; }
.ticket__price { font-family: var(--serif); font-size: 1.2rem; font-weight: 600; }
.qty { display: flex; align-items: center; gap: 0.4rem; margin: 1.2rem 0; }
.qty__btn { width: 38px; height: 38px; border: 1px solid var(--line-2); border-radius: var(--r-sm); background: var(--wall); font-size: 1.2rem; cursor: pointer; color: var(--ink); transition: 0.18s; }
.qty__btn:hover { border-color: var(--charcoal); }
.qty__num { min-width: 2.2rem; text-align: center; font-family: var(--serif); font-size: 1.3rem; font-weight: 600; }
.ticket-card__total { display: flex; justify-content: space-between; align-items: baseline; padding: 1rem 0; margin-bottom: 1rem; border-top: 1px solid var(--line); }
.ticket-card__total strong { font-family: var(--serif); font-size: 2rem; color: var(--charcoal); }
/* ---------- membership ---------- */
.membership { padding: clamp(3rem, 6vw, 5.5rem) 0; }
.plans { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.6rem; align-items: start; }
.plan { position: relative; background: var(--wall); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 2rem 1.6rem; transition: transform 0.25s ease, box-shadow 0.25s ease; }
.plan:hover { transform: translateY(-4px); box-shadow: var(--shadow); }
.plan--feature { border: 1.5px solid var(--gold); box-shadow: 0 24px 50px -28px rgba(169,129,64,0.5); }
.plan__tag { position: absolute; top: -12px; left: 50%; transform: translateX(-50%); background: var(--gold); color: #fff; font-size: 0.68rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.3rem 0.8rem; border-radius: 999px; }
.plan h3 { font-size: 1.7rem; margin-bottom: 0.4rem; }
.plan__price { font-family: var(--serif); font-size: 2.6rem; font-weight: 600; color: var(--charcoal); margin: 0 0 1.2rem; }
.plan__price span { font-family: var(--sans); font-size: 0.9rem; font-weight: 500; color: var(--muted); }
.plan ul { list-style: none; margin: 0 0 1.6rem; padding: 0; }
.plan li { padding: 0.5rem 0 0.5rem 1.5rem; position: relative; font-size: 0.92rem; color: var(--ink-2); border-bottom: 1px solid var(--line); }
.plan li::before { content: "—"; position: absolute; left: 0; color: var(--gold); }
.plan li:last-child { border-bottom: 0; }
/* ---------- footer ---------- */
.footer { background: var(--charcoal); color: var(--paper); padding: 3rem 0 1.5rem; }
.footer__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 2.5rem; align-items: start; padding-bottom: 2rem; border-bottom: 1px solid rgba(255,255,255,0.12); }
.footer__brand { display: flex; align-items: center; gap: 0.9rem; }
.footer .brand__mark { border-color: rgba(255,255,255,0.5); color: var(--paper); background: transparent; }
.footer__brand p { margin: 0; font-family: var(--serif); font-size: 1.15rem; }
.footer__brand small { font-family: var(--sans); font-size: 0.78rem; color: rgba(246,244,239,0.6); }
.news label { display: block; font-family: var(--serif); font-size: 1.3rem; margin-bottom: 0.7rem; }
.news__row { display: flex; gap: 0.5rem; }
.news__row input { flex: 1; background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.2); border-radius: var(--r-sm); padding: 0.7rem 0.9rem; color: #fff; font-family: var(--sans); font-size: 0.95rem; }
.news__row input::placeholder { color: rgba(246,244,239,0.5); }
.news__row input:focus { border-color: var(--gold); outline: none; }
.news__hint { font-size: 0.8rem; color: rgba(246,244,239,0.55); margin: 0.6rem 0 0; }
.footer__fine { text-align: center; font-size: 0.78rem; color: rgba(246,244,239,0.5); margin: 1.5rem 0 0; }
/* ---------- modal ---------- */
.modal { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 1.5rem; background: rgba(28,27,25,0.55); backdrop-filter: blur(3px); animation: fade 0.2s ease; }
.modal[hidden] { display: none; }
.modal__panel { background: var(--wall); border-radius: var(--r-lg); padding: 2.2rem; max-width: 460px; width: 100%; box-shadow: 0 40px 80px -30px rgba(0,0,0,0.5); position: relative; animation: rise 0.25s ease; }
.modal__panel h3 { font-size: 2rem; margin-bottom: 0.8rem; }
.modal__panel p#modalBody { color: var(--ink-2); margin: 0 0 1.6rem; }
.modal__close { position: absolute; top: 1rem; right: 1rem; width: 34px; height: 34px; border-radius: 999px; border: 1px solid var(--line-2); background: var(--wall); cursor: pointer; font-size: 0.9rem; color: var(--ink-2); transition: 0.18s; }
.modal__close:hover { background: var(--charcoal); color: #fff; border-color: var(--charcoal); }
/* ---------- toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 2rem; transform: translate(-50%, 1.5rem);
background: var(--charcoal); color: #fff; padding: 0.85rem 1.4rem; border-radius: var(--r-md);
font-size: 0.92rem; font-weight: 500; box-shadow: 0 16px 40px -16px rgba(0,0,0,0.6);
opacity: 0; pointer-events: none; transition: opacity 0.25s ease, transform 0.25s ease; z-index: 80;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
.toast::before { content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 999px; background: var(--gold); margin-right: 0.6rem; vertical-align: middle; }
@keyframes fade { from { opacity: 0; } }
@keyframes rise { from { opacity: 0; transform: translateY(10px); } }
/* ---------- responsive ---------- */
@media (max-width: 920px) {
.hero__grid { grid-template-columns: 1fr; }
.hero__art { max-width: 440px; order: -1; }
.visit__grid { grid-template-columns: 1fr; }
.col-grid { grid-template-columns: repeat(2, 1fr); }
.plans { grid-template-columns: 1fr; max-width: 460px; margin-inline: auto; }
}
@media (max-width: 760px) {
.nav { display: none; }
.nav.is-open {
display: flex; flex-direction: column; gap: 0;
position: absolute; top: 74px; left: 0; right: 0;
background: var(--wall); border-bottom: 1px solid var(--line); padding: 0.5rem 0;
}
.nav.is-open a { padding: 0.9rem 1.5rem; border-top: 1px solid var(--line); }
.hamburger { display: flex; }
.exh-grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.wrap { width: 100% - 2rem; width: calc(100% - 2rem); }
body { font-size: 15px; }
.topbar__inner { gap: 0.8rem; }
.brand__text small { display: none; }
#searchBtn { display: none; }
.exh { grid-template-columns: 110px 1fr; gap: 1rem; }
.exh__body h3 { font-size: 1.3rem; }
.col-grid { grid-template-columns: 1fr 1fr; gap: 1rem; }
.footer__grid { grid-template-columns: 1fr; }
.hero__stats { gap: 1.4rem; }
.hero__stats dd { font-size: 1.4rem; }
.modal__panel { padding: 1.6rem; }
}(function () {
"use strict";
/* ---------- toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-on");
}, 2600);
}
/* ---------- mobile menu ---------- */
var menuBtn = document.getElementById("menuBtn");
var nav = document.querySelector(".nav");
if (menuBtn && nav) {
menuBtn.addEventListener("click", function () {
var open = nav.classList.toggle("is-open");
menuBtn.setAttribute("aria-expanded", String(open));
});
nav.addEventListener("click", function (e) {
if (e.target.tagName === "A") {
nav.classList.remove("is-open");
menuBtn.setAttribute("aria-expanded", "false");
}
});
}
/* ---------- search (stub) ---------- */
var searchBtn = document.getElementById("searchBtn");
if (searchBtn) {
searchBtn.addEventListener("click", function () {
toast("Collection search is offline in this demo.");
});
}
/* ---------- exhibition filter ---------- */
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var exhCards = Array.prototype.slice.call(document.querySelectorAll(".exh"));
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-selected", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
var filter = chip.getAttribute("data-filter");
exhCards.forEach(function (card) {
var show = filter === "all" || card.getAttribute("data-state") === filter;
card.classList.toggle("is-hidden", !show);
});
});
});
/* ---------- exhibition detail modal ---------- */
var details = {
northern: {
kicker: "On view · Gallery II",
title: "Light & Pigment: The Northern Tradition",
body: "Sixty works trace how painters of the Lowlands and Baltic coast learned to render daylight itself — from candle-lit interiors to the silver flatness of a sea fog. Includes loans from four private collections shown publicly for the first time."
},
marchetti: {
kicker: "On view · Gallery V",
title: "Marchetti: A Life in Red",
body: "Forty canvases by Hélène Marchetti (1849–1921), reunited for the first time since her studio sale. The retrospective follows her restless use of vermilion across portraiture, still life, and the late atelier scenes."
},
gold: {
kicker: "Opens 03 Oct · Gallery I",
title: "Gold Ground: Devotion in the Trecento",
body: "Gilded panels and altarpieces from fourteenth-century Italy, displayed together for the first time in a generation. A rare chance to see how light, gold leaf, and tempera conspired to make the sacred visible."
},
quiet: {
kicker: "Opens 14 Nov · Gallery III",
title: "The Quiet Room: Interiors 1850–1920",
body: "An exhibition about stillness — thresholds, half-open windows, and the patient light of empty rooms. Seventy works survey domestic space as a subject worthy of devotion."
}
};
var modal = document.getElementById("modal");
var modalClose = document.getElementById("modalClose");
var lastFocus = null;
function openModal(key) {
var d = details[key];
if (!d || !modal) return;
document.getElementById("modalKicker").textContent = d.kicker;
document.getElementById("modalTitle").textContent = d.title;
document.getElementById("modalBody").textContent = d.body;
lastFocus = document.activeElement;
modal.hidden = false;
modalClose.focus();
}
function closeModal() {
if (!modal) return;
modal.hidden = true;
if (lastFocus) lastFocus.focus();
}
document.querySelectorAll("[data-detail]").forEach(function (btn) {
btn.addEventListener("click", function () {
openModal(btn.getAttribute("data-detail"));
});
});
if (modalClose) modalClose.addEventListener("click", closeModal);
if (modal) {
modal.addEventListener("click", function (e) {
if (e.target === modal) closeModal();
});
document.getElementById("modalCta").addEventListener("click", closeModal);
}
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && modal && !modal.hidden) closeModal();
});
/* ---------- save / favourite works ---------- */
var savedCount = 0;
var savedNote = document.getElementById("savedNote");
document.querySelectorAll(".save").forEach(function (btn) {
btn.addEventListener("click", function () {
var on = btn.getAttribute("aria-pressed") === "true";
btn.setAttribute("aria-pressed", String(!on));
btn.textContent = on ? "♡" : "♥";
savedCount += on ? -1 : 1;
if (savedCount < 0) savedCount = 0;
if (savedNote) {
savedNote.textContent = savedCount === 0
? ""
: savedCount + (savedCount === 1 ? " work saved to your tour." : " works saved to your tour.");
}
toast(on ? "Removed from your tour." : "Saved to your tour.");
});
});
/* ---------- ticket reservation ---------- */
var ticketBtns = Array.prototype.slice.call(document.querySelectorAll(".ticket"));
var qtyNum = document.getElementById("qtyNum");
var qtyMinus = document.getElementById("qtyMinus");
var qtyPlus = document.getElementById("qtyPlus");
var totalEl = document.getElementById("ticketTotal");
var checkoutBtn = document.getElementById("checkoutBtn");
var state = { price: 0, name: null, qty: 1 };
function renderTotal() {
if (totalEl) totalEl.textContent = "$" + state.price * state.qty;
if (qtyNum) qtyNum.textContent = String(state.qty);
}
ticketBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
ticketBtns.forEach(function (b) { b.classList.remove("is-selected"); });
btn.classList.add("is-selected");
state.price = parseInt(btn.getAttribute("data-price"), 10);
state.name = btn.getAttribute("data-name");
renderTotal();
});
});
if (qtyMinus) qtyMinus.addEventListener("click", function () {
state.qty = Math.max(1, state.qty - 1);
renderTotal();
});
if (qtyPlus) qtyPlus.addEventListener("click", function () {
state.qty = Math.min(12, state.qty + 1);
renderTotal();
});
if (checkoutBtn) checkoutBtn.addEventListener("click", function () {
if (!state.name) {
toast("Please choose a ticket type first.");
return;
}
toast(state.qty + " × " + state.name + " reserved — $" + state.price * state.qty + " held.");
});
/* ---------- membership ctas ---------- */
document.querySelectorAll("[data-cta^='join-']").forEach(function (btn) {
btn.addEventListener("click", function () {
var tier = btn.getAttribute("data-cta").replace("join-", "");
toast("Joining as " + tier.charAt(0).toUpperCase() + tier.slice(1) + " — welcome.");
});
});
/* ---------- newsletter ---------- */
var newsForm = document.getElementById("newsForm");
if (newsForm) {
newsForm.addEventListener("submit", function (e) {
e.preventDefault();
var email = document.getElementById("newsEmail");
toast("Subscribed — the next dispatch is on its way.");
if (email) email.value = "";
});
}
/* ---------- generic ctas ---------- */
["header-tickets", "hero-tickets", "browse-all"].forEach(function (id) {
var el = document.querySelector("[data-cta='" + id + "']");
if (el && id === "browse-all") {
el.addEventListener("click", function (e) {
e.preventDefault();
toast("The full catalogue is offline in this demo.");
});
}
});
renderTotal();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>The Verraux Museum of Fine Art</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>
<a class="skip" href="#main">Skip to content</a>
<header class="topbar" id="top">
<div class="wrap topbar__inner">
<a class="brand" href="#top" aria-label="The Verraux Museum, home">
<span class="brand__mark" aria-hidden="true">V</span>
<span class="brand__text">
<strong>The Verraux</strong>
<small>Museum of Fine Art</small>
</span>
</a>
<nav class="nav" aria-label="Primary">
<a href="#exhibitions">Exhibitions</a>
<a href="#collection">Collection</a>
<a href="#visit">Visit</a>
<a href="#membership">Membership</a>
</nav>
<div class="topbar__actions">
<button class="btn btn--ghost" id="searchBtn" aria-haspopup="dialog">Search</button>
<a class="btn btn--solid" href="#tickets" data-cta="header-tickets">Buy Tickets</a>
<button class="hamburger" id="menuBtn" aria-label="Open menu" aria-expanded="false">
<span></span><span></span><span></span>
</button>
</div>
</div>
</header>
<main id="main">
<!-- HERO -->
<section class="hero" aria-labelledby="hero-title">
<div class="wrap hero__grid">
<div class="hero__copy">
<p class="eyebrow">Now on view · Through 14 September</p>
<h1 id="hero-title">Where the<br /><em>silence</em> of centuries<br />still speaks.</h1>
<p class="lede">
Seven galleries. Four thousand works. One continuous conversation
between the painters of the past and the eyes of the present.
</p>
<div class="hero__cta">
<a class="btn btn--solid btn--lg" href="#tickets" data-cta="hero-tickets">Plan Your Visit</a>
<a class="btn btn--line btn--lg" href="#exhibitions">View Exhibitions</a>
</div>
<dl class="hero__stats">
<div><dt>Founded</dt><dd>1894</dd></div>
<div><dt>Works</dt><dd>4,120</dd></div>
<div><dt>Galleries</dt><dd>7</dd></div>
</dl>
</div>
<figure class="hero__art" tabindex="0" aria-label="Featured masterwork">
<div class="frame frame--hero">
<div class="canvas canvas--hero" role="img" aria-label="Painting: Procession at Dusk by Hélène Marchetti">
<svg viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#caa15e" />
<stop offset="0.55" stop-color="#8c5f33" />
<stop offset="1" stop-color="#2c211a" />
</linearGradient>
<radialGradient id="sun" cx="0.7" cy="0.3" r="0.5">
<stop offset="0" stop-color="#f5e3b8" />
<stop offset="1" stop-color="#f5e3b8" stop-opacity="0" />
</radialGradient>
</defs>
<rect width="400" height="500" fill="url(#sky)" />
<circle cx="280" cy="150" r="160" fill="url(#sun)" />
<path d="M0 360 L90 300 L180 350 L260 290 L340 340 L400 305 L400 500 L0 500 Z" fill="#1f1812" opacity="0.85" />
<path d="M0 410 L120 380 L230 415 L330 375 L400 405 L400 500 L0 500 Z" fill="#120d09" />
<g fill="#0c0805">
<ellipse cx="120" cy="430" rx="6" ry="16" />
<ellipse cx="150" cy="436" rx="6" ry="15" />
<ellipse cx="182" cy="430" rx="6" ry="17" />
<ellipse cx="214" cy="438" rx="6" ry="14" />
</g>
</svg>
</div>
</div>
<figcaption class="cap">
<span class="cap__title">Procession at Dusk</span>
<span class="cap__meta">Hélène Marchetti · 1881 · Oil on canvas</span>
<span class="cap__no">Acc. no. VM·1894·017</span>
</figcaption>
</figure>
</div>
</section>
<!-- TICKER / EXHIBITIONS STRIP -->
<section class="exhibitions" id="exhibitions" aria-labelledby="exh-title">
<div class="wrap">
<div class="sec-head">
<div>
<p class="eyebrow">Current & Upcoming</p>
<h2 id="exh-title">Exhibitions</h2>
</div>
<div class="filterbar" role="tablist" aria-label="Filter exhibitions">
<button class="chip is-active" role="tab" aria-selected="true" data-filter="all">All</button>
<button class="chip" role="tab" aria-selected="false" data-filter="open">On view</button>
<button class="chip" role="tab" aria-selected="false" data-filter="soon">Upcoming</button>
</div>
</div>
<div class="exh-grid" id="exhGrid">
<article class="exh" data-state="open">
<div class="frame">
<div class="canvas" style="--c1:#3f5d52;--c2:#1d2c27"></div>
<span class="badge badge--ok">On view</span>
</div>
<div class="exh__body">
<h3>Light & Pigment: The Northern Tradition</h3>
<p class="exh__dates">12 Apr — 14 Sep 2026 · Gallery II</p>
<p class="exh__desc">Sixty works tracing the slow mastery of natural light from the Lowlands to the Baltic coast.</p>
<button class="link" data-detail="northern">Details →</button>
</div>
</article>
<article class="exh" data-state="open">
<div class="frame">
<div class="canvas" style="--c1:#7a3b3b;--c2:#2a1414"></div>
<span class="badge badge--ok">On view</span>
</div>
<div class="exh__body">
<h3>Marchetti: A Life in Red</h3>
<p class="exh__dates">01 Mar — 30 Jun 2026 · Gallery V</p>
<p class="exh__desc">The first full retrospective of Hélène Marchetti, reuniting forty canvases dispersed for a century.</p>
<button class="link" data-detail="marchetti">Details →</button>
</div>
</article>
<article class="exh" data-state="soon">
<div class="frame">
<div class="canvas" style="--c1:#caa15e;--c2:#4a3414"></div>
<span class="badge badge--soon">Opens 03 Oct</span>
</div>
<div class="exh__body">
<h3>Gold Ground: Devotion in the Trecento</h3>
<p class="exh__dates">03 Oct 2026 — 25 Jan 2027 · Gallery I</p>
<p class="exh__desc">Gilded panels and altarpieces from the dawn of the Italian Renaissance, rarely shown together.</p>
<button class="link" data-detail="gold">Details →</button>
</div>
</article>
<article class="exh" data-state="soon">
<div class="frame">
<div class="canvas" style="--c1:#4a4a5e;--c2:#181822"></div>
<span class="badge badge--soon">Opens 14 Nov</span>
</div>
<div class="exh__body">
<h3>The Quiet Room: Interiors 1850–1920</h3>
<p class="exh__dates">14 Nov 2026 — 08 Mar 2027 · Gallery III</p>
<p class="exh__desc">Domestic stillness as subject — windows, thresholds, and the light that falls between them.</p>
<button class="link" data-detail="quiet">Details →</button>
</div>
</article>
</div>
</div>
</section>
<!-- COLLECTION HIGHLIGHTS -->
<section class="collection" id="collection" aria-labelledby="col-title">
<div class="wrap">
<div class="sec-head">
<div>
<p class="eyebrow">From the Permanent Collection</p>
<h2 id="col-title">Collection Highlights</h2>
</div>
<a class="link link--lg" href="#collection" data-cta="browse-all">Browse all 4,120 works →</a>
</div>
<div class="col-grid" id="colGrid">
<figure class="work" tabindex="0">
<div class="frame frame--mat">
<div class="canvas canvas--portrait" style="--c1:#b08d57;--c2:#5a3d21"></div>
</div>
<figcaption>
<h3>The Astronomer's Daughter</h3>
<p>Cornelis Vanden Berghe · 1667</p>
<p class="work__med">Oil on oak panel</p>
</figcaption>
<button class="save" aria-pressed="false" aria-label="Save The Astronomer's Daughter">♡</button>
</figure>
<figure class="work" tabindex="0">
<div class="frame frame--mat">
<div class="canvas canvas--portrait" style="--c1:#5e7a6c;--c2:#23362e"></div>
</div>
<figcaption>
<h3>Harbour, Grey Morning</h3>
<p>Aldous Finn · 1903</p>
<p class="work__med">Oil on canvas</p>
</figcaption>
<button class="save" aria-pressed="false" aria-label="Save Harbour, Grey Morning">♡</button>
</figure>
<figure class="work" tabindex="0">
<div class="frame frame--mat">
<div class="canvas canvas--portrait" style="--c1:#8c5a8c;--c2:#36223a"></div>
</div>
<figcaption>
<h3>Study of Anemones</h3>
<p>Hélène Marchetti · 1879</p>
<p class="work__med">Watercolour on paper</p>
</figcaption>
<button class="save" aria-pressed="false" aria-label="Save Study of Anemones">♡</button>
</figure>
<figure class="work" tabindex="0">
<div class="frame frame--mat">
<div class="canvas canvas--portrait" style="--c1:#a85a3a;--c2:#3a1c10"></div>
</div>
<figcaption>
<h3>The Red Atelier</h3>
<p>Hélène Marchetti · 1888</p>
<p class="work__med">Oil on canvas</p>
</figcaption>
<button class="save" aria-pressed="false" aria-label="Save The Red Atelier">♡</button>
</figure>
</div>
<p class="saved-note" id="savedNote" aria-live="polite"></p>
</div>
</section>
<!-- VISIT -->
<section class="visit" id="visit" aria-labelledby="visit-title">
<div class="wrap visit__grid">
<div class="visit__info">
<p class="eyebrow">Plan Your Day</p>
<h2 id="visit-title">Visit the museum</h2>
<ul class="hours">
<li><span>Tue — Thu</span><span>10:00 — 17:00</span></li>
<li><span>Fri — Sat</span><span>10:00 — 21:00</span></li>
<li><span>Sunday</span><span>11:00 — 18:00</span></li>
<li class="hours__closed"><span>Monday</span><span>Closed</span></li>
</ul>
<address class="addr">
14 Verraux Square · Old Town<br />
Free entry every first Friday evening.
</address>
</div>
<div class="ticket-card" id="tickets">
<h3>Admission</h3>
<p class="ticket-card__sub">Select a ticket to begin.</p>
<div class="ticket-list" id="ticketList">
<button class="ticket" data-price="0" data-name="Member">
<span class="ticket__name">Member & Under 18</span>
<span class="ticket__price">Free</span>
</button>
<button class="ticket" data-price="14" data-name="Adult">
<span class="ticket__name">Adult</span>
<span class="ticket__price">$14</span>
</button>
<button class="ticket" data-price="9" data-name="Concession">
<span class="ticket__name">Student / Senior</span>
<span class="ticket__price">$9</span>
</button>
</div>
<div class="qty" role="group" aria-label="Quantity">
<button class="qty__btn" id="qtyMinus" aria-label="Decrease quantity">−</button>
<span class="qty__num" id="qtyNum" aria-live="polite">1</span>
<button class="qty__btn" id="qtyPlus" aria-label="Increase quantity">+</button>
</div>
<div class="ticket-card__total">
<span>Total</span>
<strong id="ticketTotal">$0</strong>
</div>
<button class="btn btn--solid btn--block" id="checkoutBtn" data-cta="checkout">Reserve & Continue</button>
</div>
</div>
</section>
<!-- MEMBERSHIP -->
<section class="membership" id="membership" aria-labelledby="mem-title">
<div class="wrap">
<div class="sec-head sec-head--center">
<div>
<p class="eyebrow">Become a Patron</p>
<h2 id="mem-title">Membership</h2>
<p class="sec-sub">Unlimited entry, members-only previews, and a hand in keeping the galleries open.</p>
</div>
</div>
<div class="plans">
<article class="plan">
<h3>Friend</h3>
<p class="plan__price">$60<span>/year</span></p>
<ul>
<li>Unlimited admission</li>
<li>10% in the museum shop</li>
<li>Quarterly journal</li>
</ul>
<button class="btn btn--line btn--block" data-cta="join-friend">Join</button>
</article>
<article class="plan plan--feature">
<span class="plan__tag">Most chosen</span>
<h3>Patron</h3>
<p class="plan__price">$140<span>/year</span></p>
<ul>
<li>Everything in Friend</li>
<li>Guest passes (4 / year)</li>
<li>Exhibition previews</li>
<li>Curator's evening invites</li>
</ul>
<button class="btn btn--solid btn--block" data-cta="join-patron">Join</button>
</article>
<article class="plan">
<h3>Benefactor</h3>
<p class="plan__price">$500<span>/year</span></p>
<ul>
<li>Everything in Patron</li>
<li>Private gallery tours</li>
<li>Name on the donor wall</li>
</ul>
<button class="btn btn--line btn--block" data-cta="join-benefactor">Join</button>
</article>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="wrap footer__grid">
<div class="footer__brand">
<span class="brand__mark" aria-hidden="true">V</span>
<p>The Verraux Museum of Fine Art<br /><small>14 Verraux Square · Old Town</small></p>
</div>
<form class="news" id="newsForm">
<label for="newsEmail">The monthly dispatch</label>
<div class="news__row">
<input type="email" id="newsEmail" placeholder="you@example.com" required />
<button class="btn btn--solid" type="submit">Subscribe</button>
</div>
<p class="news__hint">Exhibition openings and quiet announcements. No noise.</p>
</form>
</div>
<p class="footer__fine">© 1894–2026 The Verraux Museum · A fictional institution.</p>
</footer>
<!-- DETAIL DIALOG -->
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
<div class="modal__panel">
<button class="modal__close" id="modalClose" aria-label="Close">✕</button>
<p class="eyebrow" id="modalKicker">Exhibition</p>
<h3 id="modalTitle">Title</h3>
<p id="modalBody">Body</p>
<a class="btn btn--solid" href="#tickets" id="modalCta">Book this exhibition</a>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Fine Art Museum Landing
A timeless marketing landing for the fictional Verraux Museum of Fine Art. The layout leans on generous wall space, a Cormorant Garamond serif display, and a restrained gallery-white, charcoal, and gold palette. A stately hero frames a featured masterwork rendered as an inline SVG painting, complete with a museum-style caption and accession number, alongside founding-date and collection statistics.
Below the fold, a current-and-upcoming exhibitions strip is filterable by status (all, on view, upcoming) and each card opens an accessible detail dialog with curatorial copy. The collection highlights grid presents real-feeling artworks — titles, artists, dates, and mediums — each savable to a personal tour via a heart toggle that tracks a running count. A visit panel lists opening hours and an interactive admission ticket builder computes a live total from the selected ticket type and quantity.
Everything runs on vanilla JavaScript: a small toast() helper, a focus-managed modal with Escape-to-close, the exhibition filter, the favourites counter, the ticket calculator, membership join actions, and a newsletter sign-up. Hover and focus states, badges, soft shadows, and smooth micro-interactions keep the experience refined, and the whole page reflows cleanly down to 360px.
Illustrative UI only — demo data; not a real museum system.