Wedding — Photo Gallery Lightbox
An editorial blush-and-gold wedding gallery with a masonry-style grid of ceremony, portrait and reception photographs that expand into a full-screen lightbox. Click any frame to view it large with an elegant caption, moment badge, live counter and thumbnail filmstrip. Navigate with previous and next buttons, arrow keys, Escape to close, or filter the collection by moment — all wrapped in airy serifs and thin gold rules.
MCP
Code
:root {
--blush: #e8b7b0;
--blush-50: #fbeeec;
--rose: #c98a86;
--gold: #c9a24b;
--gold-d: #a8862f;
--ink: #3a2b2b;
--ink-2: #6b5555;
--muted: #9a8585;
--bg: #fbf7f4;
--cream: #f4ece4;
--white: #fff;
--line: rgba(58, 43, 43, 0.12);
--r-sm: 8px;
--r-md: 16px;
--r-lg: 26px;
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--shadow: 0 24px 60px -28px rgba(58, 43, 43, 0.4);
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: var(--sans);
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1200px 600px at 50% -10%, var(--blush-50), transparent 60%),
var(--bg);
min-height: 100vh;
}
.page {
max-width: 1120px;
margin: 0 auto;
padding: 56px 24px 72px;
}
/* ---------- Masthead ---------- */
.masthead {
text-align: center;
margin-bottom: 40px;
}
.eyebrow {
font-family: var(--sans);
text-transform: uppercase;
letter-spacing: 0.34em;
font-size: 11px;
font-weight: 600;
color: var(--gold-d);
margin: 0 0 10px;
}
.couple {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(2.6rem, 7vw, 4.4rem);
line-height: 1.02;
margin: 0;
color: var(--ink);
}
.date {
font-family: var(--serif);
font-size: 1.15rem;
color: var(--ink-2);
margin: 10px 0 0;
font-style: italic;
}
.rule {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin: 22px auto;
max-width: 360px;
color: var(--gold);
}
.rule::before,
.rule::after {
content: "";
height: 1px;
flex: 1;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
.rule span {
font-size: 14px;
}
.lede {
max-width: 560px;
margin: 0 auto;
color: var(--ink-2);
font-size: 1rem;
}
/* ---------- Filters ---------- */
.filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
justify-content: center;
margin-bottom: 30px;
}
.chip {
font-family: var(--sans);
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ink-2);
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
padding: 9px 18px;
cursor: pointer;
transition: all 0.22s ease;
}
.chip:hover {
border-color: var(--blush);
color: var(--rose);
transform: translateY(-1px);
}
.chip:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.chip.is-active {
background: linear-gradient(135deg, var(--rose), var(--gold-d));
border-color: transparent;
color: var(--white);
box-shadow: 0 8px 20px -8px rgba(201, 138, 134, 0.7);
}
.count {
font-size: 12.5px;
color: var(--muted);
margin-left: 6px;
letter-spacing: 0.04em;
}
.count strong {
color: var(--gold-d);
}
/* ---------- Grid ---------- */
.grid {
columns: 3 260px;
column-gap: 16px;
}
.tile {
position: relative;
display: block;
width: 100%;
break-inside: avoid;
margin: 0 0 16px;
padding: 0;
border: 0;
border-radius: var(--r-md);
overflow: hidden;
cursor: pointer;
background: var(--cream);
box-shadow: 0 10px 30px -18px rgba(58, 43, 43, 0.5);
transition: transform 0.3s ease, box-shadow 0.3s ease;
animation: rise 0.5s ease both;
}
@keyframes rise {
from {
opacity: 0;
transform: translateY(14px);
}
}
.tile::after {
content: "";
position: absolute;
inset: 0;
border: 1px solid rgba(201, 162, 75, 0.35);
border-radius: var(--r-md);
pointer-events: none;
}
.tile:hover {
transform: translateY(-4px);
box-shadow: var(--shadow);
}
.tile:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 3px;
}
.tile-img {
width: 100%;
display: block;
aspect-ratio: var(--ar, 4 / 5);
transition: transform 0.6s ease;
}
.tile:hover .tile-img {
transform: scale(1.05);
}
.tile-veil {
position: absolute;
inset: 0;
background: linear-gradient(
180deg,
rgba(58, 43, 43, 0) 45%,
rgba(58, 43, 43, 0.55) 100%
);
opacity: 0;
transition: opacity 0.3s ease;
display: flex;
align-items: flex-end;
padding: 14px 16px;
}
.tile:hover .tile-veil,
.tile:focus-visible .tile-veil {
opacity: 1;
}
.tile-cap {
color: var(--white);
font-family: var(--serif);
font-size: 1.2rem;
line-height: 1.1;
transform: translateY(6px);
transition: transform 0.3s ease;
}
.tile:hover .tile-cap {
transform: translateY(0);
}
.tile-badge {
position: absolute;
top: 12px;
left: 12px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink);
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(201, 162, 75, 0.5);
padding: 5px 10px;
border-radius: 999px;
backdrop-filter: blur(4px);
}
/* ---------- Footer ---------- */
.foot {
text-align: center;
margin-top: 56px;
color: var(--ink-2);
}
.foot p {
font-family: var(--serif);
font-size: 1.25rem;
margin: 0 0 18px;
}
.foot em {
color: var(--rose);
}
.ghost-btn {
font-family: var(--sans);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--gold-d);
background: transparent;
border: 1px solid var(--gold);
border-radius: 999px;
padding: 11px 26px;
cursor: pointer;
transition: all 0.22s ease;
}
.ghost-btn:hover {
background: var(--gold);
color: var(--white);
transform: translateY(-1px);
}
.ghost-btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.ghost-btn.sm {
padding: 8px 18px;
font-size: 11px;
}
/* ---------- Lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-rows: 1fr auto;
align-items: center;
gap: 12px;
padding: 28px;
}
.lightbox[hidden] {
display: none;
}
.lb-backdrop {
position: absolute;
inset: 0;
background: rgba(58, 43, 43, 0.66);
backdrop-filter: blur(8px);
animation: fade 0.3s ease;
}
@keyframes fade {
from {
opacity: 0;
}
}
.lb-close {
position: absolute;
top: 20px;
right: 22px;
z-index: 3;
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.4);
background: rgba(255, 255, 255, 0.12);
color: var(--white);
font-size: 26px;
line-height: 1;
cursor: pointer;
transition: all 0.2s ease;
}
.lb-close:hover {
background: var(--white);
color: var(--ink);
}
.lb-close:focus-visible,
.lb-nav:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 3px;
}
.lb-nav {
position: relative;
z-index: 2;
width: 54px;
height: 54px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.35);
background: rgba(255, 255, 255, 0.1);
color: var(--white);
font-size: 30px;
line-height: 1;
cursor: pointer;
transition: all 0.2s ease;
align-self: center;
}
.lb-nav:hover {
background: linear-gradient(135deg, var(--rose), var(--gold-d));
border-color: transparent;
transform: scale(1.06);
}
.lb-prev {
grid-column: 1;
grid-row: 1;
}
.lb-next {
grid-column: 3;
grid-row: 1;
}
.lb-stage {
position: relative;
z-index: 2;
grid-column: 2;
grid-row: 1;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
max-height: 100%;
min-height: 0;
}
.lb-frame {
position: relative;
max-width: min(72vw, 720px);
max-height: 64vh;
border-radius: var(--r-md);
overflow: hidden;
box-shadow: 0 40px 90px -30px rgba(0, 0, 0, 0.7);
border: 3px solid var(--white);
animation: pop 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
@keyframes pop {
from {
opacity: 0;
transform: scale(0.94);
}
}
.lb-image {
width: min(72vw, 720px);
height: 64vh;
max-height: 64vh;
background-size: cover;
background-position: center;
transition: background-image 0.2s ease;
}
.lb-badge {
position: absolute;
top: 14px;
left: 14px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink);
background: rgba(255, 255, 255, 0.92);
border: 1px solid rgba(201, 162, 75, 0.6);
padding: 5px 11px;
border-radius: 999px;
}
.lb-caption {
text-align: center;
color: var(--white);
max-width: 560px;
}
.lb-counter {
display: inline-block;
font-family: var(--sans);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.2em;
color: var(--blush);
margin-bottom: 4px;
}
.lb-title {
font-family: var(--serif);
font-size: 1.7rem;
line-height: 1.1;
margin: 2px 0 4px;
}
.lb-sub {
margin: 0 0 12px;
color: rgba(255, 255, 255, 0.75);
font-size: 0.92rem;
}
.lb-actions {
display: flex;
justify-content: center;
}
.lb-actions .ghost-btn {
color: var(--blush);
border-color: rgba(255, 255, 255, 0.4);
}
.lb-actions .ghost-btn:hover {
background: var(--white);
color: var(--ink);
border-color: var(--white);
}
/* ---------- Filmstrip ---------- */
.lb-strip {
position: relative;
z-index: 2;
grid-column: 1 / -1;
grid-row: 2;
display: flex;
gap: 10px;
justify-content: center;
overflow-x: auto;
padding: 8px 4px 4px;
scrollbar-width: thin;
}
.lb-thumb {
flex: 0 0 auto;
width: 62px;
height: 62px;
border-radius: var(--r-sm);
border: 2px solid transparent;
background-size: cover;
background-position: center;
cursor: pointer;
opacity: 0.55;
transition: all 0.22s ease;
}
.lb-thumb:hover {
opacity: 0.85;
}
.lb-thumb.is-active {
opacity: 1;
border-color: var(--gold);
box-shadow: 0 0 0 2px rgba(201, 162, 75, 0.35);
}
.lb-thumb:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
background: var(--ink);
color: var(--white);
font-size: 13px;
font-weight: 500;
letter-spacing: 0.02em;
padding: 12px 22px;
border-radius: 999px;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
z-index: 90;
}
.toast::before {
content: "\2022";
color: var(--gold);
margin-right: 8px;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.grid {
columns: 2 200px;
}
.lb-frame,
.lb-image {
max-width: 86vw;
width: 86vw;
}
}
@media (max-width: 520px) {
.page {
padding: 36px 16px 56px;
}
.grid {
columns: 1;
}
.filters {
gap: 8px;
}
.chip {
padding: 8px 14px;
font-size: 11.5px;
}
.count {
width: 100%;
text-align: center;
margin: 4px 0 0;
}
.lightbox {
padding: 14px;
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto auto;
}
.lb-frame,
.lb-image {
max-width: 92vw;
width: 92vw;
height: 46vh;
max-height: 46vh;
}
.lb-nav {
width: 46px;
height: 46px;
font-size: 26px;
}
.lb-prev {
grid-column: 1;
grid-row: 3;
justify-self: start;
}
.lb-next {
grid-column: 1;
grid-row: 3;
justify-self: end;
margin-top: -46px;
}
.lb-stage {
grid-column: 1;
grid-row: 2;
}
.lb-strip {
grid-row: 4;
justify-content: flex-start;
}
.lb-title {
font-size: 1.4rem;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}/* Wedding — Photo Gallery Lightbox
Vanilla JS, no dependencies. */
(function () {
"use strict";
// --- Data ------------------------------------------------------------
// Unsplash placeholder photographs. Each has a moment, caption and note.
var PHOTOS = [
{ id: "p1", moment: "ceremony", label: "Ceremony", title: "The First Look", note: "Under the olive arch, moments before the vows.", url: "https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=900&q=80", ar: "4 / 5" },
{ id: "p2", moment: "portraits", label: "Portraits", title: "Golden Hour", note: "The two of them wandering the lavender rows.", url: "https://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?auto=format&fit=crop&w=900&q=80", ar: "3 / 4" },
{ id: "p3", moment: "details", label: "Details", title: "The Rings", note: "Warm gold bands resting on ivory linen.", url: "https://images.unsplash.com/photo-1510076857177-7470076d4098?auto=format&fit=crop&w=900&q=80", ar: "3 / 2" },
{ id: "p4", moment: "ceremony", label: "Ceremony", title: "Exchanging Vows", note: "Handwritten promises, read aloud at dusk.", url: "https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=900&q=80", ar: "4 / 5" },
{ id: "p5", moment: "reception", label: "Reception", title: "The First Dance", note: "Fairy lights strung between the plane trees.", url: "https://images.unsplash.com/photo-1470229722913-7c0e2dbbafd3?auto=format&fit=crop&w=900&q=80", ar: "3 / 2" },
{ id: "p6", moment: "details", label: "Details", title: "Peony Bouquet", note: "Blush peonies tied with silk ribbon.", url: "https://images.unsplash.com/photo-1519225421980-715cb0215aed?auto=format&fit=crop&w=900&q=80", ar: "4 / 5" },
{ id: "p7", moment: "portraits", label: "Portraits", title: "Quiet Moment", note: "A stolen glance in the château garden.", url: "https://images.unsplash.com/photo-1520854221256-17451cc331bf?auto=format&fit=crop&w=900&q=80", ar: "3 / 4" },
{ id: "p8", moment: "reception", label: "Reception", title: "The Long Table", note: "Dinner beneath the stars for eighty guests.", url: "https://images.unsplash.com/photo-1464366400600-7168b8af9bc3?auto=format&fit=crop&w=900&q=80", ar: "3 / 2" },
{ id: "p9", moment: "ceremony", label: "Ceremony", title: "The Kiss", note: "Applause rising from the courtyard.", url: "https://images.unsplash.com/photo-1583939003579-730e3918a45a?auto=format&fit=crop&w=900&q=80", ar: "4 / 5" },
{ id: "p10", moment: "details", label: "Details", title: "Place Settings", note: "Calligraphed cards and candlelight.", url: "https://images.unsplash.com/photo-1478146059778-26028b07395a?auto=format&fit=crop&w=900&q=80", ar: "3 / 2" },
{ id: "p11", moment: "reception", label: "Reception", title: "Cheers", note: "A toast to the newlyweds.", url: "https://images.unsplash.com/photo-1530103862676-de8c9debad1d?auto=format&fit=crop&w=900&q=80", ar: "3 / 4" },
{ id: "p12", moment: "portraits", label: "Portraits", title: "Just Married", note: "Confetti and laughter on the stone steps.", url: "https://images.unsplash.com/photo-1522673607200-164d1b6ce486?auto=format&fit=crop&w=900&q=80", ar: "4 / 5" }
];
// --- Elements --------------------------------------------------------
var gallery = document.getElementById("gallery");
var countNum = document.getElementById("count-num");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var lightbox = document.getElementById("lightbox");
var lbImage = document.getElementById("lb-image");
var lbBadge = document.getElementById("lb-badge");
var lbTitle = document.getElementById("lb-title");
var lbSub = document.getElementById("lb-sub");
var lbCounter = document.getElementById("lb-counter");
var lbStrip = document.getElementById("lb-strip");
var lbClose = document.getElementById("lb-close");
var lbPrev = document.getElementById("lb-prev");
var lbNext = document.getElementById("lb-next");
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);
}
// --- State -----------------------------------------------------------
var current = PHOTOS.slice(); // visible / filtered set
var index = 0; // active index within `current`
var lastFocused = null;
// --- Grid render -----------------------------------------------------
function renderGrid(list) {
gallery.innerHTML = "";
list.forEach(function (photo, i) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "tile";
btn.style.setProperty("--ar", photo.ar);
btn.style.animationDelay = (i * 45) + "ms";
btn.setAttribute("aria-label", "Open photograph: " + photo.title);
btn.dataset.id = photo.id;
var img = document.createElement("img");
img.className = "tile-img";
img.loading = "lazy";
img.alt = photo.title + " — " + photo.note;
img.src = photo.url;
img.style.setProperty("--ar", photo.ar);
var badge = document.createElement("span");
badge.className = "tile-badge";
badge.textContent = photo.label;
var veil = document.createElement("span");
veil.className = "tile-veil";
var cap = document.createElement("span");
cap.className = "tile-cap";
cap.textContent = photo.title;
veil.appendChild(cap);
btn.appendChild(img);
btn.appendChild(badge);
btn.appendChild(veil);
btn.addEventListener("click", function () {
open(i);
});
gallery.appendChild(btn);
});
countNum.textContent = String(list.length);
}
// --- Filtering -------------------------------------------------------
function applyFilter(filter) {
current = filter === "all"
? PHOTOS.slice()
: PHOTOS.filter(function (p) { return p.moment === filter; });
renderGrid(current);
}
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");
applyFilter(chip.dataset.filter);
});
});
// --- Lightbox --------------------------------------------------------
function buildStrip() {
lbStrip.innerHTML = "";
current.forEach(function (photo, i) {
var t = document.createElement("button");
t.type = "button";
t.className = "lb-thumb";
t.style.backgroundImage = "url('" + photo.url + "')";
t.setAttribute("aria-label", "View " + photo.title);
t.dataset.i = String(i);
t.addEventListener("click", function () { go(i); });
lbStrip.appendChild(t);
});
}
function paint() {
var photo = current[index];
if (!photo) return;
lbImage.style.backgroundImage = "url('" + photo.url + "')";
lbImage.setAttribute("aria-label", photo.title + " — " + photo.note);
lbBadge.textContent = photo.label;
lbTitle.textContent = photo.title;
lbSub.textContent = photo.note;
lbCounter.textContent = (index + 1) + " / " + current.length;
var thumbs = lbStrip.querySelectorAll(".lb-thumb");
thumbs.forEach(function (t, i) {
var active = i === index;
t.classList.toggle("is-active", active);
if (active) {
t.scrollIntoView({ inline: "center", block: "nearest", behavior: "smooth" });
}
});
}
function open(i) {
lastFocused = document.activeElement;
index = i;
buildStrip();
paint();
lightbox.hidden = false;
document.body.style.overflow = "hidden";
lbClose.focus();
}
function close() {
lightbox.hidden = true;
document.body.style.overflow = "";
if (lastFocused && typeof lastFocused.focus === "function") {
lastFocused.focus();
}
}
function go(i) {
var n = current.length;
index = (i + n) % n;
paint();
}
lbPrev.addEventListener("click", function () { go(index - 1); });
lbNext.addEventListener("click", function () { go(index + 1); });
lbClose.addEventListener("click", close);
lightbox.addEventListener("click", function (e) {
if (e.target.hasAttribute("data-close")) close();
});
document.getElementById("lb-copy").addEventListener("click", function () {
var link = window.location.href.split("#")[0] + "#" + current[index].id;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(link).then(
function () { toast("Photo link copied to clipboard"); },
function () { toast("Share link: " + current[index].id); }
);
} else {
toast("Share link: " + current[index].id);
}
});
document.getElementById("share-all").addEventListener("click", function () {
var link = window.location.href.split("#")[0];
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(link).then(
function () { toast("Gallery link copied — share the memories"); },
function () { toast("Copy the URL to share this gallery"); }
);
} else {
toast("Copy the URL to share this gallery");
}
});
// --- Keyboard --------------------------------------------------------
document.addEventListener("keydown", function (e) {
if (lightbox.hidden) return;
if (e.key === "Escape") { close(); }
else if (e.key === "ArrowLeft") { e.preventDefault(); go(index - 1); }
else if (e.key === "ArrowRight") { e.preventDefault(); go(index + 1); }
});
// --- Init ------------------------------------------------------------
renderGrid(current);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Amélie & Julian — Wedding Gallery</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@400;500;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<header class="masthead">
<p class="eyebrow">The Wedding of</p>
<h1 class="couple">Amélie & Julian</h1>
<p class="date">June 14th, 2026 · Château de Fleurie, Provence</p>
<div class="rule" aria-hidden="true"><span>✦</span></div>
<p class="lede">A collection of moments from our day — the vows, the portraits, the long candlelit dinner beneath the olive trees.</p>
</header>
<nav class="filters" aria-label="Filter photographs by moment">
<button class="chip is-active" type="button" data-filter="all" aria-pressed="true">All Moments</button>
<button class="chip" type="button" data-filter="ceremony" aria-pressed="false">Ceremony</button>
<button class="chip" type="button" data-filter="portraits" aria-pressed="false">Portraits</button>
<button class="chip" type="button" data-filter="reception" aria-pressed="false">Reception</button>
<button class="chip" type="button" data-filter="details" aria-pressed="false">Details</button>
<span class="count" aria-live="polite"><strong id="count-num">12</strong> photographs</span>
</nav>
<section class="grid" id="gallery" aria-label="Wedding photographs">
<!-- tiles injected by script.js -->
</section>
<footer class="foot">
<div class="rule" aria-hidden="true"><span>✦</span></div>
<p>With love & gratitude — <em>A & J</em></p>
<button class="ghost-btn" id="share-all" type="button">Share this gallery</button>
</footer>
</main>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Photograph viewer" hidden>
<div class="lb-backdrop" data-close></div>
<button class="lb-close" id="lb-close" type="button" aria-label="Close viewer">×</button>
<button class="lb-nav lb-prev" id="lb-prev" type="button" aria-label="Previous photograph">
<span aria-hidden="true">‹</span>
</button>
<figure class="lb-stage">
<div class="lb-frame">
<div class="lb-image" id="lb-image" role="img" aria-label="Selected photograph"></div>
<span class="lb-badge" id="lb-badge">Ceremony</span>
</div>
<figcaption class="lb-caption">
<span class="lb-counter" id="lb-counter">1 / 12</span>
<p class="lb-title" id="lb-title"></p>
<p class="lb-sub" id="lb-sub"></p>
<div class="lb-actions">
<button class="ghost-btn sm" id="lb-copy" type="button">Copy link</button>
</div>
</figcaption>
</figure>
<button class="lb-nav lb-next" id="lb-next" type="button" aria-label="Next photograph">
<span aria-hidden="true">›</span>
</button>
<div class="lb-strip" id="lb-strip" aria-label="Photograph thumbnails"></div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A romantic, editorial photo gallery for a wedding site. The grid mixes portrait and landscape frames in a light masonry rhythm, each tile softened with a blush overlay, a gold-hairline border and a moment badge (Ceremony, Portraits, Reception, Details). A filter row above the grid lets guests narrow the collection to a single chapter of the day, with a live count of how many photographs are showing.
Clicking any frame opens a full-screen lightbox: the image is centered on a dimmed cream backdrop with a serif caption, the moment badge, and a counter such as “4 / 12”. A thumbnail filmstrip along the bottom mirrors the active photo and lets you jump directly to any shot. Previous and next controls glide between images, and the whole experience is keyboard-friendly — arrow keys move through the set, Escape closes the view, and focus returns to the frame you opened.
The interaction layer is vanilla JavaScript: filtering rebuilds the visible set, the lightbox reads captions and badges from the DOM, and a small toast confirms actions like copying a share link. Everything is responsive down to about 360px, where the grid collapses to a single column and the filmstrip scrolls horizontally, keeping the airy blush-and-gold aesthetic intact on any screen.