Museum — History / Heritage Museum Landing
A scholarly, archival landing page for a fictional heritage museum, built around a narrative valley history. It pairs an engraved hero and a scrolling era marquee with a filterable grid of permanent galleries, a keyboard-accessible timeline that reveals short accounts for each turning point, and an oral-history feature with a live archive search across letters, ledgers and recorded voices. Parchment-and-sepia palette, engraved serif type, generous wall space.
MCP
Code
:root {
/* History / parchment palette */
--parchment: #efe6d3;
--parchment-2: #e6dac1;
--wall: #faf5e9;
--sepia: #6b4f34;
--sepia-d: #4d3722;
--ink: #3a2c1d;
--ink-2: #6b5a44;
--muted: #8c7a5e;
--red: #8a2f28;
--red-d: #6b211b;
--red-50: #f2e3df;
--gold: #a98140;
--line: rgba(58, 44, 29, 0.16);
--line-2: rgba(58, 44, 29, 0.28);
--ok: #3f7d56;
--warn: #b8842c;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--shadow: 0 1px 2px rgba(58,44,29,0.06), 0 8px 24px rgba(58,44,29,0.08);
--shadow-lg: 0 12px 40px rgba(58,44,29,0.16);
--serif: "Cormorant", "EB Garamond", Georgia, serif;
--garamond: "EB Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--garamond);
background: var(--parchment);
color: var(--ink);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
img, svg { display: block; max-width: 100%; }
.wrap { width: min(1140px, 92vw); margin-inline: auto; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
position: absolute; left: -999px; top: 0; z-index: 100;
background: var(--ink); color: var(--parchment); padding: 10px 16px; border-radius: 0 0 var(--r-sm) 0;
}
.skip-link:focus { left: 0; }
a { color: inherit; }
:focus-visible {
outline: 2px solid var(--red);
outline-offset: 3px;
border-radius: 3px;
}
/* ---------- Buttons ---------- */
.btn {
--bg: transparent;
display: inline-flex; align-items: center; justify-content: center; gap: .5em;
font-family: var(--sans); font-weight: 600; font-size: .92rem; letter-spacing: .01em;
padding: .72em 1.3em; border-radius: 999px; border: 1px solid transparent;
cursor: pointer; text-decoration: none; transition: transform .15s ease, background .2s, box-shadow .2s, color .2s;
white-space: nowrap;
}
.btn:active { transform: translateY(1px); }
.btn--sm { padding: .5em 1em; font-size: .82rem; }
.btn--solid { background: var(--red); color: #fbf4e8; box-shadow: 0 2px 0 var(--red-d); }
.btn--solid:hover { background: var(--red-d); box-shadow: 0 4px 14px rgba(138,47,40,.28); }
.btn--ghost { background: transparent; color: var(--sepia); border-color: var(--line-2); }
.btn--ghost:hover { background: rgba(107,79,52,.08); border-color: var(--sepia); }
/* ---------- Topbar ---------- */
.topbar {
position: sticky; top: 0; z-index: 40;
background: rgba(239,230,211,.86);
backdrop-filter: saturate(140%) blur(10px);
border-bottom: 1px solid var(--line);
}
.topbar__inner { display: flex; align-items: center; gap: 1.4rem; padding: .7rem 0; }
.brand { display: flex; align-items: center; gap: .7rem; text-decoration: none; color: var(--ink); }
.brand__seal { color: var(--sepia); flex: none; }
.brand__name { display: flex; flex-direction: column; line-height: 1.05; }
.brand__name strong { font-family: var(--serif); font-weight: 700; font-size: 1.28rem; letter-spacing: .01em; }
.brand__name em { font-style: normal; font-family: var(--sans); font-size: .66rem; letter-spacing: .26em; text-transform: uppercase; color: var(--muted); }
.nav { display: flex; gap: 1.3rem; margin-left: auto; font-family: var(--sans); font-size: .9rem; font-weight: 500; }
.nav a { text-decoration: none; color: var(--ink-2); padding: .3em 0; position: relative; }
.nav a::after { content: ""; position: absolute; left: 0; bottom: -1px; width: 0; height: 1.5px; background: var(--red); transition: width .25s ease; }
.nav a:hover { color: var(--ink); }
.nav a:hover::after { width: 100%; }
.topbar__actions { display: flex; align-items: center; gap: .9rem; }
.open-flag {
font-family: var(--sans); font-size: .72rem; font-weight: 600; letter-spacing: .04em;
padding: .35em .75em; border-radius: 999px; border: 1px solid var(--line-2);
color: var(--muted); white-space: nowrap;
}
.open-flag.is-open { color: var(--ok); border-color: rgba(63,125,86,.4); background: rgba(63,125,86,.08); }
.open-flag.is-closed { color: var(--red); border-color: rgba(138,47,40,.35); background: var(--red-50); }
.menu-toggle { display: none; flex-direction: column; gap: 4px; background: none; border: 0; padding: 8px; cursor: pointer; }
.menu-toggle span { width: 22px; height: 2px; background: var(--ink); transition: .25s; }
.menu-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.menu-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.menu-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
.mobile-nav { display: flex; flex-direction: column; padding: .5rem 4vw 1rem; gap: .2rem; border-top: 1px solid var(--line); font-family: var(--sans); }
.mobile-nav a { padding: .7em .2em; text-decoration: none; color: var(--ink-2); border-bottom: 1px solid var(--line); }
/* ---------- Hero ---------- */
.hero {
display: grid; grid-template-columns: 1.05fr 1fr; gap: clamp(1.5rem, 5vw, 4rem);
width: min(1140px, 92vw); margin: clamp(1.5rem, 5vw, 3.5rem) auto clamp(2rem, 6vw, 4rem);
align-items: center;
}
.hero__plate { position: relative; }
.hero__engraving {
width: 100%; aspect-ratio: 5/4; border-radius: var(--r-md);
border: 1px solid var(--line-2);
box-shadow: var(--shadow-lg);
filter: sepia(.18) contrast(1.02);
}
.hero__mat {
position: absolute; inset: -14px; border-radius: var(--r-lg);
border: 1px solid var(--line); pointer-events: none; z-index: -1;
background: var(--wall);
box-shadow: inset 0 0 0 1px rgba(255,255,255,.5);
}
.eyebrow { font-family: var(--sans); font-size: .72rem; letter-spacing: .22em; text-transform: uppercase; color: var(--red); font-weight: 600; margin: 0 0 .8rem; }
.hero h1 { font-family: var(--serif); font-weight: 700; font-size: clamp(2.4rem, 6vw, 4rem); line-height: 1.02; margin: 0 0 1rem; letter-spacing: -.01em; }
.lede { font-size: clamp(1.05rem, 1.6vw, 1.22rem); color: var(--ink-2); max-width: 40ch; margin: 0 0 1.6rem; }
.lede strong { color: var(--ink); font-weight: 600; }
.hero__cta { display: flex; flex-wrap: wrap; gap: .8rem; margin-bottom: 2rem; }
.hero__stats { display: flex; gap: 2.2rem; margin: 0; padding-top: 1.4rem; border-top: 1px solid var(--line); flex-wrap: wrap; }
.hero__stats div { display: flex; flex-direction: column; }
.hero__stats dt { font-family: var(--serif); font-weight: 700; font-size: 1.7rem; color: var(--sepia); line-height: 1; }
.hero__stats dd { margin: .3rem 0 0; font-family: var(--sans); font-size: .74rem; letter-spacing: .03em; color: var(--muted); }
/* ---------- Eras marquee ---------- */
.eras { overflow: hidden; border-block: 1px solid var(--line); background: var(--sepia); color: var(--parchment); }
.eras__track { display: inline-flex; white-space: nowrap; font-family: var(--serif); font-size: 1.15rem; padding: .7rem 0; animation: scroll 32s linear infinite; }
.eras__track span { padding-inline: 1.4rem; opacity: .92; }
.eras:hover .eras__track { animation-play-state: paused; }
@keyframes scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .eras__track { animation: none; } }
/* ---------- Sections ---------- */
.section { padding: clamp(3rem, 8vw, 6rem) 0; }
.section--alt { background: var(--wall); border-block: 1px solid var(--line); }
.section__head { max-width: 56ch; margin-bottom: 2.6rem; }
.kicker { font-family: var(--sans); font-size: .72rem; letter-spacing: .2em; text-transform: uppercase; color: var(--red); font-weight: 600; margin: 0 0 .7rem; }
.section h2 { font-family: var(--serif); font-weight: 600; font-size: clamp(1.9rem, 4vw, 2.8rem); line-height: 1.08; margin: 0 0 .7rem; letter-spacing: -.01em; }
.section__sub { color: var(--ink-2); font-size: 1.1rem; margin: 0; }
/* ---------- Filters ---------- */
.filters { display: flex; flex-wrap: wrap; gap: .55rem; margin-bottom: 2rem; }
.chip {
font-family: var(--sans); font-size: .82rem; font-weight: 500; cursor: pointer;
padding: .5em 1.05em; border-radius: 999px; border: 1px solid var(--line-2);
background: transparent; color: var(--ink-2); transition: .2s;
}
.chip:hover { border-color: var(--sepia); color: var(--ink); }
.chip.is-active { background: var(--ink); color: var(--parchment); border-color: var(--ink); }
/* ---------- Gallery grid ---------- */
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.4rem; }
.gcard {
background: var(--wall); border: 1px solid var(--line); border-radius: var(--r-md);
overflow: hidden; box-shadow: var(--shadow); cursor: pointer;
transition: transform .25s ease, box-shadow .25s ease, border-color .25s;
display: flex; flex-direction: column;
}
.gcard:hover, .gcard:focus-visible { transform: translateY(-5px); box-shadow: var(--shadow-lg); border-color: var(--line-2); }
.gcard.is-hidden { display: none; }
.gcard__plate {
aspect-ratio: 16/10; display: grid; place-items: center;
background: linear-gradient(145deg, var(--c1), var(--c2));
border-bottom: 4px solid rgba(255,255,255,.12);
position: relative;
}
.gcard__plate svg { width: 46%; opacity: .92; }
.gcard__plate::after { content: ""; position: absolute; inset: 8px; border: 1px solid rgba(239,230,211,.3); border-radius: 4px; pointer-events: none; }
.gcard__body { padding: 1.1rem 1.2rem 1.3rem; display: flex; flex-direction: column; gap: .4rem; }
.badge {
align-self: flex-start; font-family: var(--sans); font-size: .64rem; font-weight: 600;
letter-spacing: .12em; text-transform: uppercase; color: var(--sepia);
background: var(--parchment-2); padding: .3em .7em; border-radius: 999px;
}
.badge--quiet { background: transparent; border: 1px solid var(--line-2); color: var(--muted); }
.gcard__body h3 { font-family: var(--serif); font-weight: 600; font-size: 1.5rem; margin: .15rem 0 .1rem; }
.gcard__body p { margin: 0; color: var(--ink-2); font-size: 1rem; }
.gcard__meta { font-family: var(--sans); font-size: .72rem; color: var(--muted); margin-top: .3rem; letter-spacing: .02em; }
/* ---------- Timeline ---------- */
.timeline { display: grid; grid-template-columns: 220px 1fr; gap: 2.4rem; }
.timeline__rail { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0; position: relative; }
.timeline__rail::before { content: ""; position: absolute; left: 7px; top: 14px; bottom: 14px; width: 1.5px; background: var(--line-2); }
.timeline__rail li { position: relative; }
.era {
font-family: var(--serif); font-size: 1.25rem; font-weight: 600; color: var(--muted);
background: none; border: 0; cursor: pointer; padding: .55em 0 .55em 2rem; width: 100%; text-align: left;
position: relative; transition: color .2s;
}
.era::before {
content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
width: 15px; height: 15px; border-radius: 50%; background: var(--wall);
border: 2px solid var(--line-2); transition: .2s;
}
.era:hover { color: var(--ink); }
.era.is-active { color: var(--red); }
.era.is-active::before { background: var(--red); border-color: var(--red); box-shadow: 0 0 0 4px var(--red-50); }
.timeline__panel {
background: var(--wall); border: 1px solid var(--line); border-radius: var(--r-md);
padding: 2rem 2.2rem; box-shadow: var(--shadow); min-height: 220px;
animation: fade .35s ease;
}
@keyframes fade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.timeline__panel .badge { margin-bottom: .9rem; }
.timeline__panel h3 { font-family: var(--serif); font-weight: 700; font-size: 1.9rem; margin: 0 0 .2rem; }
.timeline__panel .tl-year { font-family: var(--sans); font-size: .8rem; letter-spacing: .1em; text-transform: uppercase; color: var(--gold); font-weight: 600; }
.timeline__panel p { color: var(--ink-2); font-size: 1.08rem; margin: .8rem 0 0; max-width: 60ch; }
.timeline__panel .tl-tag { font-family: var(--sans); font-size: .76rem; color: var(--muted); margin-top: 1rem; }
/* ---------- Archive ---------- */
.archive__grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
.archive__intro p { color: var(--ink-2); font-size: 1.1rem; }
.archive__search { display: flex; gap: .6rem; margin: 1.4rem 0 0; }
.archive__search input {
flex: 1; font-family: var(--garamond); font-size: 1.05rem; padding: .7em 1em;
border: 1px solid var(--line-2); border-radius: var(--r-sm); background: var(--wall); color: var(--ink);
}
.archive__search input:focus { outline: none; border-color: var(--red); box-shadow: 0 0 0 3px var(--red-50); }
.archive__results { margin-top: 1rem; display: flex; flex-direction: column; gap: .5rem; }
.archive__results .hit {
display: flex; gap: .8rem; align-items: baseline; padding: .65rem .85rem;
background: var(--wall); border: 1px solid var(--line); border-radius: var(--r-sm);
animation: fade .3s ease; font-size: .98rem;
}
.archive__results .hit .ref { font-family: var(--sans); font-size: .68rem; letter-spacing: .08em; color: var(--gold); font-weight: 600; white-space: nowrap; }
.archive__results .hit strong { color: var(--ink); }
.archive__results .empty { color: var(--muted); font-style: italic; padding: .5rem 0; }
.archive__feature { margin: 0; background: var(--wall); border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; box-shadow: var(--shadow); }
.archive__photo svg { width: 100%; aspect-ratio: 8/9; filter: sepia(.4) contrast(1.05); border-bottom: 1px solid var(--line); }
.archive__feature figcaption { padding: 1.2rem 1.4rem 1.5rem; }
.archive__feature figcaption p { margin: .5rem 0 0; }
.archive__feature figcaption strong { font-family: var(--serif); font-size: 1.35rem; font-weight: 600; color: var(--ink); line-height: 1.25; }
.archive__cite { font-size: .95rem; color: var(--ink-2); }
.archive__feature .btn { margin-top: 1rem; }
/* ---------- Visit ---------- */
.visit__grid { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 4rem); align-items: start; }
.visit__facts { list-style: none; margin: 1.4rem 0 2rem; padding: 0; }
.visit__facts li { display: flex; justify-content: space-between; gap: 1rem; padding: .85rem 0; border-bottom: 1px solid var(--line); }
.visit__facts span { font-family: var(--sans); font-size: .78rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.visit__facts strong { font-weight: 600; text-align: right; }
.visit__card { background: var(--sepia); color: var(--parchment); border-radius: var(--r-lg); padding: clamp(1.6rem, 4vw, 2.4rem); box-shadow: var(--shadow-lg); }
.visit__card h3 { font-family: var(--serif); font-weight: 700; font-size: 1.8rem; margin: 0 0 .4rem; }
.visit__card p { color: rgba(239,230,211,.85); margin: 0 0 1.2rem; }
.visit__card em { font-style: italic; color: #f3e9d6; }
.visit__form { display: flex; gap: .55rem; }
.visit__form input { flex: 1; font-family: var(--garamond); font-size: 1.02rem; padding: .7em 1em; border: 1px solid rgba(239,230,211,.3); border-radius: var(--r-sm); background: rgba(239,230,211,.1); color: var(--parchment); }
.visit__form input::placeholder { color: rgba(239,230,211,.55); }
.visit__form input:focus { outline: none; border-color: var(--parchment); background: rgba(239,230,211,.18); }
.visit__form .btn--solid { background: var(--parchment); color: var(--sepia-d); box-shadow: none; }
.visit__form .btn--solid:hover { background: #fff; }
.visit__fine { font-family: var(--sans); font-size: .74rem; color: rgba(239,230,211,.6); margin: .9rem 0 0; }
/* ---------- Footer ---------- */
.footer { background: var(--ink); color: var(--parchment); padding: 2.6rem 0; }
.footer__inner { display: flex; flex-wrap: wrap; gap: 1.5rem 2.5rem; align-items: center; justify-content: space-between; }
.footer__brand strong { font-family: var(--serif); font-size: 1.25rem; font-weight: 600; }
.footer__brand p { margin: .2rem 0 0; font-family: var(--sans); font-size: .8rem; color: rgba(239,230,211,.6); }
.footer__links { display: flex; gap: 1.3rem; font-family: var(--sans); font-size: .85rem; }
.footer__links a { color: rgba(239,230,211,.85); text-decoration: none; }
.footer__links a:hover { color: var(--parchment); text-decoration: underline; }
.footer__note { width: 100%; margin: 0; padding-top: 1.2rem; border-top: 1px solid rgba(239,230,211,.15); font-family: var(--sans); font-size: .74rem; color: rgba(239,230,211,.5); }
/* ---------- Toast ---------- */
.toast {
position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%) translateY(20px);
background: var(--ink); color: var(--parchment); font-family: var(--sans); font-size: .9rem;
padding: .85em 1.3em; border-radius: 999px; box-shadow: var(--shadow-lg); z-index: 80;
opacity: 0; transition: opacity .3s ease, transform .3s ease; max-width: 90vw; text-align: center;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; }
.hero__plate { order: -1; }
.gallery-grid { grid-template-columns: repeat(2, 1fr); }
.timeline { grid-template-columns: 1fr; }
.timeline__rail { flex-direction: row; flex-wrap: wrap; gap: .4rem; }
.timeline__rail::before { display: none; }
.era { width: auto; padding: .4em .9em; border: 1px solid var(--line-2); border-radius: 999px; }
.era::before { display: none; }
.era.is-active { background: var(--red-50); }
.archive__grid, .visit__grid { grid-template-columns: 1fr; }
}
@media (max-width: 760px) {
.nav, .topbar__actions .btn, .open-flag { display: none; }
.menu-toggle { display: flex; margin-left: auto; }
.topbar__actions { margin-left: 0; }
}
@media (max-width: 520px) {
.gallery-grid { grid-template-columns: 1fr; }
.hero__stats { gap: 1.4rem; }
.hero__stats dt { font-size: 1.4rem; }
.archive__search, .visit__form { flex-direction: column; }
.archive__search .btn, .visit__form .btn { width: 100%; }
.footer__inner { flex-direction: column; align-items: flex-start; }
.section__sub, .archive__intro p { font-size: 1rem; }
.visit__facts strong { font-size: .95rem; }
}// Thornbury Heritage Museum — landing interactions (vanilla JS)
(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.hidden = false;
// force reflow so transition runs
void toastEl.offsetWidth;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
setTimeout(function () { toastEl.hidden = true; }, 320);
}, 2800);
}
// Any element with data-toast triggers a toast
document.addEventListener("click", function (e) {
var t = e.target.closest("[data-toast]");
if (t) toast(t.getAttribute("data-toast"));
});
/* ---------- Opening-hours flag (Tue–Sun, 10–17) ---------- */
(function () {
var flag = document.getElementById("openFlag");
if (!flag) return;
var now = new Date();
var day = now.getDay(); // 0 Sun .. 6 Sat
var hour = now.getHours();
var openDay = day !== 1; // closed Mondays
var openHour = hour >= 10 && hour < 17;
if (openDay && openHour) {
flag.textContent = "Open now · until 17:00";
flag.className = "open-flag is-open";
} else {
flag.textContent = openDay ? "Closed · opens 10:00" : "Closed Mondays";
flag.className = "open-flag is-closed";
}
})();
/* ---------- Mobile nav ---------- */
(function () {
var toggle = document.getElementById("menuToggle");
var menu = document.getElementById("mobileNav");
if (!toggle || !menu) return;
toggle.addEventListener("click", function () {
var open = toggle.getAttribute("aria-expanded") === "true";
toggle.setAttribute("aria-expanded", String(!open));
menu.hidden = open;
});
menu.addEventListener("click", function (e) {
if (e.target.tagName === "A") {
toggle.setAttribute("aria-expanded", "false");
menu.hidden = true;
}
});
})();
/* ---------- Gallery filtering ---------- */
(function () {
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var cards = Array.prototype.slice.call(document.querySelectorAll(".gcard"));
if (!chips.length) return;
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
var f = chip.getAttribute("data-filter");
var shown = 0;
cards.forEach(function (card) {
var match = f === "all" ||
card.getAttribute("data-floor") === f ||
card.getAttribute("data-theme") === f;
card.classList.toggle("is-hidden", !match);
if (match) shown++;
});
toast(shown + (shown === 1 ? " gallery" : " galleries") + " shown");
});
});
})();
/* ---------- Timeline tabs ---------- */
(function () {
var eras = [
{
ref: "Gallery VII", year: "c.1600", tag: "The valley before the mills",
title: "The Drovers' Road",
body: "Long before chimneys, the valley was a thoroughfare for cattle drovers crossing to the lowland fairs. A worn waystone, two toll tokens and a drover's horn survive — the earliest objects in the collection, gathered from a hedge bank in 1903."
},
{
ref: "Gallery I", year: "1740–1860", tag: "Industry arrives",
title: "The Mill Years",
body: "Water power turned Thornbury into a worsted town. The Thornbury Worsted Company employed three hundred hands at its height. Our ledgers record wages to the half-penny, and the carding combs still smell faintly of lanolin."
},
{
ref: "Gallery VI", year: "1843", tag: "Catastrophe & memory",
title: "The Great Flood",
body: "In the spring of 1843 the river rose nine feet in a night. Eleven were lost; the church bell was carried half a mile downstream and recovered from a meadow. High-water marks on the gallery wall are taken from the surviving cottages."
},
{
ref: "Gallery III", year: "1861", tag: "Connection",
title: "Railway & Reform",
body: "The branch line opened in 1861, and with it came newspapers, a reading society, and the first elected parish board. The stationmaster's lamp and the original timetable board anchor the gallery."
},
{
ref: "Gallery V", year: "1900–1945", tag: "In their own words",
title: "Voices of the Valley",
body: "Two world wars are remembered here not through generals but through letters home, ration books, and the recorded voices of those who stayed. The listening room plays 340 interviews gathered between 1971 and 1989."
},
{
ref: "Gallery IX", year: "1946–1979", tag: "After the mills",
title: "Rebuilding",
body: "The last mill closed in 1962. This gallery follows the valley's reinvention — the cooperative dairy, the comprehensive school, and the founding of this very museum by public subscription in 1881, refurbished after the war."
}
];
var rail = document.getElementById("timelineRail");
var panel = document.getElementById("timelinePanel");
if (!rail || !panel) return;
var buttons = Array.prototype.slice.call(rail.querySelectorAll(".era"));
function render(i) {
var e = eras[i];
panel.innerHTML =
'<span class="badge">' + e.ref + '</span>' +
'<span class="tl-year">' + e.year + '</span>' +
'<h3>' + e.title + '</h3>' +
'<p>' + e.body + '</p>' +
'<p class="tl-tag">' + e.tag + '</p>';
}
function select(i, focus) {
buttons.forEach(function (b, j) {
var on = j === i;
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", String(on));
b.tabIndex = on ? 0 : -1;
});
render(i);
if (focus) buttons[i].focus();
}
buttons.forEach(function (b, i) {
b.addEventListener("click", function () { select(i); });
b.addEventListener("keydown", function (ev) {
var n;
if (ev.key === "ArrowDown" || ev.key === "ArrowRight") n = (i + 1) % buttons.length;
else if (ev.key === "ArrowUp" || ev.key === "ArrowLeft") n = (i - 1 + buttons.length) % buttons.length;
else if (ev.key === "Home") n = 0;
else if (ev.key === "End") n = buttons.length - 1;
if (n != null) { ev.preventDefault(); select(n, true); }
});
});
select(0);
})();
/* ---------- Gallery card keyboard activation ---------- */
(function () {
document.querySelectorAll(".gcard").forEach(function (card) {
card.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
card.click();
}
});
card.addEventListener("click", function () {
var name = card.querySelector("h3");
if (name) toast("Opening “" + name.textContent + "” — in the real museum this would load the gallery guide.");
});
});
})();
/* ---------- Archive search ---------- */
(function () {
var form = document.getElementById("archiveForm");
var input = document.getElementById("archiveInput");
var results = document.getElementById("archiveResults");
if (!form || !input || !results) return;
var records = [
{ ref: "THM/OH/118", title: "Edith Marsden, weaver — oral history", terms: ["edith", "marsden", "weaver", "mill", "oral", "voice"] },
{ ref: "THM/LED/04", title: "Thornbury Worsted Co. wage ledger, 1849", terms: ["ledger", "mill", "wage", "1849", "worsted", "company"] },
{ ref: "THM/HART/22", title: "Hartley family letters, 1861–1894", terms: ["hartley", "letter", "1861", "1894", "family"] },
{ ref: "THM/FLD/07", title: "Account of the Great Flood, 1843", terms: ["flood", "1843", "river", "bell"] },
{ ref: "THM/PHO/210", title: "Photograph: the branch line opening, 1861", terms: ["railway", "1861", "photograph", "station", "branch"] },
{ ref: "THM/PAR/01", title: "Parish register of baptisms, 1600–1740", terms: ["parish", "register", "1600", "baptism", "drover"] }
];
function search(q) {
q = q.trim().toLowerCase();
if (!q) return [];
return records.filter(function (r) {
if (r.title.toLowerCase().indexOf(q) !== -1) return true;
return r.terms.some(function (t) { return t.indexOf(q) !== -1; });
});
}
function show(q) {
var hits = search(q);
results.innerHTML = "";
if (!q.trim()) { return; }
if (!hits.length) {
var empty = document.createElement("p");
empty.className = "empty";
empty.textContent = "No records for “" + q.trim() + "”. Try a name, a year, or a place.";
results.appendChild(empty);
return;
}
hits.forEach(function (h) {
var row = document.createElement("div");
row.className = "hit";
row.innerHTML = '<span class="ref">' + h.ref + '</span><span><strong>' + h.title + '</strong></span>';
results.appendChild(row);
});
}
form.addEventListener("submit", function (e) {
e.preventDefault();
var hits = search(input.value);
show(input.value);
if (input.value.trim()) {
toast(hits.length ? hits.length + " record" + (hits.length === 1 ? "" : "s") + " found in the archive" : "Nothing found — try another term");
}
});
var debounce;
input.addEventListener("input", function () {
clearTimeout(debounce);
debounce = setTimeout(function () { show(input.value); }, 220);
});
})();
/* ---------- Membership form ---------- */
(function () {
var form = document.getElementById("joinForm");
if (!form) return;
form.addEventListener("submit", function (e) {
e.preventDefault();
var email = form.querySelector("input");
if (email && email.value) {
toast("Welcome to the Friends — a welcome letter is on its way.");
form.reset();
}
});
})();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Thornbury Heritage Museum — A History of People & Place</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:wght@500;600;700&family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="topbar" id="top">
<div class="wrap topbar__inner">
<a class="brand" href="#top" aria-label="Thornbury Heritage Museum, home">
<span class="brand__seal" aria-hidden="true">
<svg viewBox="0 0 48 48" width="40" height="40" role="img" aria-label="Museum seal">
<circle cx="24" cy="24" r="22" fill="none" stroke="currentColor" stroke-width="1.4"/>
<circle cx="24" cy="24" r="17" fill="none" stroke="currentColor" stroke-width="0.8"/>
<path d="M14 30 L24 14 L34 30 Z" fill="none" stroke="currentColor" stroke-width="1.2"/>
<line x1="14" y1="30" x2="34" y2="30" stroke="currentColor" stroke-width="1.2"/>
<text x="24" y="40" text-anchor="middle" font-size="6" fill="currentColor" font-family="Georgia, serif">1881</text>
</svg>
</span>
<span class="brand__name">
<strong>Thornbury</strong>
<em>Heritage Museum</em>
</span>
</a>
<nav class="nav" aria-label="Primary">
<a href="#galleries">Galleries</a>
<a href="#timeline">Timeline</a>
<a href="#archive">Archive</a>
<a href="#visit">Visit</a>
</nav>
<div class="topbar__actions">
<span class="open-flag" id="openFlag" aria-live="polite">Checking hours…</span>
<a class="btn btn--solid" href="#visit" data-toast="Ticket desk opens at 10:00. Members enter free.">Plan a visit</a>
</div>
<button class="menu-toggle" id="menuToggle" aria-expanded="false" aria-controls="mobileNav" aria-label="Open menu">
<span></span><span></span><span></span>
</button>
</div>
<nav class="mobile-nav" id="mobileNav" aria-label="Mobile" hidden>
<a href="#galleries">Galleries</a>
<a href="#timeline">Timeline</a>
<a href="#archive">Archive</a>
<a href="#visit">Visit</a>
</nav>
</header>
<main id="main">
<!-- HERO -->
<section class="hero" aria-labelledby="heroTitle">
<div class="hero__plate" aria-hidden="true">
<svg viewBox="0 0 600 480" preserveAspectRatio="xMidYMid slice" class="hero__engraving" role="img" aria-label="Engraving of the old mill town">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e8dcc2"/>
<stop offset="1" stop-color="#d8c7a4"/>
</linearGradient>
</defs>
<rect width="600" height="480" fill="url(#sky)"/>
<g stroke="#6b4f34" stroke-width="1.1" fill="none" opacity="0.85">
<path d="M0 360 L120 300 L240 340 L360 285 L480 330 L600 290"/>
<path d="M0 380 L600 320" opacity="0.5"/>
<rect x="90" y="250" width="70" height="110"/>
<rect x="120" y="210" width="14" height="44"/>
<rect x="250" y="270" width="90" height="90"/>
<path d="M250 270 L295 230 L340 270"/>
<rect x="420" y="245" width="80" height="115"/>
<path d="M420 245 L460 205 L500 245"/>
<circle cx="475" cy="95" r="34"/>
<line x1="475" y1="42" x2="475" y2="58"/>
<line x1="475" y1="132" x2="475" y2="148"/>
<line x1="421" y1="95" x2="437" y2="95"/>
<line x1="513" y1="95" x2="529" y2="95"/>
</g>
<g stroke="#6b4f34" stroke-width="0.5" opacity="0.3">
<line x1="0" y1="40" x2="600" y2="40"/>
<line x1="0" y1="60" x2="600" y2="60"/>
<line x1="0" y1="80" x2="600" y2="80"/>
</g>
</svg>
<span class="hero__mat"></span>
</div>
<div class="hero__body">
<p class="eyebrow">Est. 1881 · A people's archive of the valley</p>
<h1 id="heroTitle">Where the<br/> valley remembers<br/> itself.</h1>
<p class="lede">
Four centuries of the Thornbury valley — its mills and migrations, its
floods, harvests and handwriting — gathered into ten permanent galleries
and an open archive of more than <strong>twelve thousand</strong> objects,
letters, and recorded voices.
</p>
<div class="hero__cta">
<a class="btn btn--solid" href="#galleries">Explore the galleries</a>
<a class="btn btn--ghost" href="#archive" data-toast="The reading room welcomes researchers Tue–Sat.">Search the archive →</a>
</div>
<dl class="hero__stats">
<div><dt>12,400+</dt><dd>catalogued objects</dd></div>
<div><dt>1881</dt><dd>founded by subscription</dd></div>
<div><dt>340</dt><dd>recorded oral histories</dd></div>
</dl>
</div>
</section>
<!-- MARQUEE OF ERAS -->
<section class="eras" aria-label="Eras at a glance">
<div class="eras__track" id="erasTrack">
<span>· The Drovers' Road, c.1600 </span>
<span>· The Mill Years, 1740–1860 </span>
<span>· The Great Flood of 1843 </span>
<span>· Railway & Reform, 1861 </span>
<span>· Voices of the Valley, 1900–1945 </span>
<span>· Rebuilding, 1946–1979 </span>
</div>
</section>
<!-- GALLERIES -->
<section class="section" id="galleries" aria-labelledby="galTitle">
<div class="wrap">
<header class="section__head">
<p class="kicker">Permanent collection</p>
<h2 id="galTitle">Ten galleries, one continuous story</h2>
<p class="section__sub">
Each room follows a thread of the valley's life. Filter by floor and
theme to plan your route through the house.
</p>
</header>
<div class="filters" role="group" aria-label="Filter galleries">
<button class="chip is-active" data-filter="all" aria-pressed="true">All rooms</button>
<button class="chip" data-filter="ground" aria-pressed="false">Ground floor</button>
<button class="chip" data-filter="upper" aria-pressed="false">Upper floor</button>
<button class="chip" data-filter="industry" aria-pressed="false">Industry</button>
<button class="chip" data-filter="domestic" aria-pressed="false">Domestic life</button>
</div>
<div class="gallery-grid" id="galleryGrid">
<article class="gcard" data-floor="ground" data-theme="industry" tabindex="0">
<div class="gcard__plate" style="--c1:#7a5638;--c2:#4d3722;">
<svg viewBox="0 0 80 80" aria-hidden="true"><circle cx="40" cy="40" r="22" fill="none" stroke="#efe6d3" stroke-width="2"/><path d="M40 18 L40 62 M18 40 L62 40 M25 25 L55 55 M55 25 L25 55" stroke="#efe6d3" stroke-width="1.5"/></svg>
</div>
<div class="gcard__body">
<span class="badge">Gallery I · Ground</span>
<h3>The Mill Floor</h3>
<p>The water-wheel, the carding combs, and the ledgers of the Thornbury Worsted Company.</p>
<span class="gcard__meta">26 objects · audio guide</span>
</div>
</article>
<article class="gcard" data-floor="ground" data-theme="domestic" tabindex="0">
<div class="gcard__plate" style="--c1:#8a2f28;--c2:#5e2018;">
<svg viewBox="0 0 80 80" aria-hidden="true"><rect x="22" y="26" width="36" height="30" fill="none" stroke="#efe6d3" stroke-width="2"/><path d="M22 26 L40 14 L58 26" fill="none" stroke="#efe6d3" stroke-width="2"/><rect x="34" y="40" width="12" height="16" fill="#efe6d3"/></svg>
</div>
<div class="gcard__body">
<span class="badge">Gallery II · Ground</span>
<h3>Hearth & Home</h3>
<p>A reconstructed weaver's cottage, with furniture, samplers, and a settle worn smooth by use.</p>
<span class="gcard__meta">41 objects · period room</span>
</div>
</article>
<article class="gcard" data-floor="ground" data-theme="industry" tabindex="0">
<div class="gcard__plate" style="--c1:#5b6b4a;--c2:#3a4530;">
<svg viewBox="0 0 80 80" aria-hidden="true"><path d="M14 56 L66 56 M20 56 L24 30 L40 30 L40 56 M40 38 L60 38 L60 56" fill="none" stroke="#efe6d3" stroke-width="2"/><circle cx="50" cy="22" r="6" fill="#efe6d3"/></svg>
</div>
<div class="gcard__body">
<span class="badge">Gallery III · Ground</span>
<h3>Railway & Reform</h3>
<p>The 1861 branch line that remade the valley — signage, signalling, and a stationmaster's lamp.</p>
<span class="gcard__meta">33 objects · model layout</span>
</div>
</article>
<article class="gcard" data-floor="upper" data-theme="domestic" tabindex="0">
<div class="gcard__plate" style="--c1:#6b4f34;--c2:#473322;">
<svg viewBox="0 0 80 80" aria-hidden="true"><rect x="24" y="20" width="32" height="40" rx="2" fill="none" stroke="#efe6d3" stroke-width="2"/><path d="M30 30 L50 30 M30 38 L50 38 M30 46 L44 46" stroke="#efe6d3" stroke-width="1.5"/></svg>
</div>
<div class="gcard__body">
<span class="badge">Gallery IV · Upper</span>
<h3>Letters & Ledgers</h3>
<p>Correspondence of the Hartley family across three generations, displayed in archival vitrines.</p>
<span class="gcard__meta">58 documents · transcribed</span>
</div>
</article>
<article class="gcard" data-floor="upper" data-theme="domestic" tabindex="0">
<div class="gcard__plate" style="--c1:#8a2f28;--c2:#4d1a15;">
<svg viewBox="0 0 80 80" aria-hidden="true"><circle cx="40" cy="40" r="20" fill="none" stroke="#efe6d3" stroke-width="2"/><circle cx="40" cy="40" r="3" fill="#efe6d3"/><path d="M40 24 L40 40 L52 48" stroke="#efe6d3" stroke-width="2" fill="none"/></svg>
</div>
<div class="gcard__body">
<span class="badge">Gallery V · Upper</span>
<h3>Voices of the Valley</h3>
<p>Listening booths of recorded memory — wartime, work, and weather, in the valley's own accent.</p>
<span class="gcard__meta">340 recordings · listening room</span>
</div>
</article>
<article class="gcard" data-floor="upper" data-theme="industry" tabindex="0">
<div class="gcard__plate" style="--c1:#4a5560;--c2:#2c333b;">
<svg viewBox="0 0 80 80" aria-hidden="true"><path d="M40 16 C30 30 30 44 40 64 C50 44 50 30 40 16 Z" fill="none" stroke="#efe6d3" stroke-width="2"/><path d="M20 50 Q40 40 60 50" stroke="#efe6d3" stroke-width="1.5" fill="none"/></svg>
</div>
<div class="gcard__body">
<span class="badge">Gallery VI · Upper</span>
<h3>The Great Flood</h3>
<p>The deluge of 1843, told through high-water marks, rescue accounts, and a salvaged church bell.</p>
<span class="gcard__meta">19 objects · immersive</span>
</div>
</article>
</div>
</div>
</section>
<!-- TIMELINE TEASER -->
<section class="section section--alt" id="timeline" aria-labelledby="tlTitle">
<div class="wrap">
<header class="section__head">
<p class="kicker">A timeline of the valley</p>
<h2 id="tlTitle">Four centuries, in turning points</h2>
<p class="section__sub">Select an era to read its short account. The full chronology runs through Gallery VIII.</p>
</header>
<div class="timeline">
<ol class="timeline__rail" id="timelineRail" role="tablist" aria-label="Eras">
<li><button class="era is-active" role="tab" aria-selected="true" data-era="0">c.1600</button></li>
<li><button class="era" role="tab" aria-selected="false" data-era="1">1740</button></li>
<li><button class="era" role="tab" aria-selected="false" data-era="2">1843</button></li>
<li><button class="era" role="tab" aria-selected="false" data-era="3">1861</button></li>
<li><button class="era" role="tab" aria-selected="false" data-era="4">1914</button></li>
<li><button class="era" role="tab" aria-selected="false" data-era="5">1946</button></li>
</ol>
<article class="timeline__panel" id="timelinePanel" role="tabpanel" aria-live="polite">
<!-- filled by JS -->
</article>
</div>
</div>
</section>
<!-- ARCHIVE FEATURE -->
<section class="section archive" id="archive" aria-labelledby="arTitle">
<div class="wrap archive__grid">
<div class="archive__intro">
<p class="kicker">Open archive & oral history</p>
<h2 id="arTitle">Every voice kept, every hand traced</h2>
<p>
Our reading room holds parish registers, mill ledgers, photographs and
more than three hundred recorded interviews. Search a name, a year, or a
place — then book a table to handle the originals.
</p>
<form class="archive__search" id="archiveForm" role="search" aria-label="Search the archive">
<label class="sr-only" for="archiveInput">Search the archive</label>
<input id="archiveInput" type="search" placeholder="Try “Hartley”, “1843”, or “mill”…" autocomplete="off" />
<button class="btn btn--solid" type="submit">Search</button>
</form>
<div class="archive__results" id="archiveResults" aria-live="polite"></div>
</div>
<figure class="archive__feature">
<div class="archive__photo" aria-hidden="true">
<svg viewBox="0 0 320 360" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Sepia portrait of a valley weaver, c.1890">
<rect width="320" height="360" fill="#cdb98f"/>
<ellipse cx="160" cy="140" rx="58" ry="70" fill="#a8895f"/>
<path d="M100 360 Q160 220 220 360 Z" fill="#8a6a45"/>
<rect x="118" y="120" width="84" height="46" rx="4" fill="#6b4f34" opacity="0.55"/>
<g stroke="#6b4f34" stroke-width="0.6" opacity="0.35"><line x1="0" y1="50" x2="320" y2="50"/><line x1="0" y1="120" x2="320" y2="120"/><line x1="0" y1="240" x2="320" y2="240"/></g>
</svg>
</div>
<figcaption>
<span class="badge badge--quiet">THM/OH/118</span>
<p><strong>“We worked to the bell, and the bell never stopped.”</strong></p>
<p class="archive__cite">Edith Marsden, weaver, recorded 1974 — recalling the mill of her youth.</p>
<button class="btn btn--ghost btn--sm" id="playClip" data-toast="Playing oral-history clip THM/OH/118 — 2 min 14 sec.">▶ Play 2:14 clip</button>
</figcaption>
</figure>
</div>
</section>
<!-- VISIT / CTA -->
<section class="section section--alt visit" id="visit" aria-labelledby="visitTitle">
<div class="wrap visit__grid">
<div>
<p class="kicker">Plan your visit</p>
<h2 id="visitTitle">The house on Drovers' Hill</h2>
<ul class="visit__facts">
<li><span>Hours</span><strong>Tue – Sun · 10:00 – 17:00</strong></li>
<li><span>Admission</span><strong>Pay-what-you-can · Members free</strong></li>
<li><span>Reading room</span><strong>Tue – Sat · by appointment</strong></li>
<li><span>Where</span><strong>2 Drovers' Hill, Thornbury</strong></li>
</ul>
<a class="btn btn--solid" href="#top" data-toast="Reserved! A confirmation has been sent to your inbox.">Reserve a free time-slot</a>
</div>
<div class="visit__card">
<h3>Become a Friend of the Museum</h3>
<p>Free entry, reading-room priority, and the quarterly journal <em>The Valley Record</em>.</p>
<form class="visit__form" id="joinForm" aria-label="Membership signup">
<label class="sr-only" for="joinEmail">Email address</label>
<input id="joinEmail" type="email" placeholder="you@example.org" required />
<button class="btn btn--solid" type="submit">Join</button>
</form>
<p class="visit__fine">From £30 / year. We keep your details in the archive of nobody.</p>
</div>
</div>
</section>
</main>
<footer class="footer">
<div class="wrap footer__inner">
<div class="footer__brand">
<strong>Thornbury Heritage Museum</strong>
<p>2 Drovers' Hill, Thornbury · Registered charity, est. 1881</p>
</div>
<nav class="footer__links" aria-label="Footer">
<a href="#galleries">Galleries</a>
<a href="#timeline">Timeline</a>
<a href="#archive">Archive</a>
<a href="#visit">Visit</a>
</nav>
<p class="footer__note">Illustrative UI only — demo data; not a real museum.</p>
</div>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>History / Heritage Museum Landing
A marketing landing for the fictional Thornbury Heritage Museum, written in a calm, scholarly register. The page opens on an engraved hero of the old mill town and a sepia stat row, then runs a continuously scrolling marquee of the valley’s eras. Ten permanent galleries are presented as framed plate cards — each with a catalogue badge, medium and object count — that can be filtered by floor or theme with a single click.
Interactions are all vanilla JavaScript. The timeline is a proper ARIA tablist: arrow
keys move between eras and each selection swaps in a short archival account with its gallery
reference and year. The open archive section runs a live, debounced search over a small
set of records (letters, ledgers, parish registers and oral histories), matching on names,
years and places, and an oral-history feature card plays a quoted clip via a toast. A
sticky header shows live opening hours (closed Mondays), the membership and reservation
forms validate and confirm, and every action surfaces through a small toast() helper.
The design follows a parchment-and-sepia heritage palette with a deep-red accent, engraved serif display type over a quiet sans for UI, soft mats and thin frames around imagery, and a responsive layout that collapses cleanly to a single column down to ~360px.
Illustrative UI only — demo data; not a real museum system.