Photography — Portfolio Gallery
A gallery-dark, image-first portfolio with category chips, a CSS-columns masonry of fifteen-plus full-bleed photographs, and a keyboard-friendly lightbox that steps through the filtered set with caption, counter, and smooth fade-and-scale transitions.
MCP
Code
:root {
--ink: #0b0b0c;
--bg: #101012;
--surface: #17171b;
--surface-2: #1e1e23;
--paper: #f6f4ef;
--sand: #c9b79c;
--sand-d: #a8916f;
--muted: #9a9aa2;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--paper);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img { display: block; max-width: 100%; }
a { color: inherit; }
::selection { background: var(--sand); color: var(--ink); }
:focus-visible {
outline: 2px solid var(--sand);
outline-offset: 3px;
}
/* ---------- Header ---------- */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 26px clamp(18px, 5vw, 64px);
border-bottom: 1px solid var(--line);
flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 14px; }
.brand-mark {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: var(--r-md);
background: linear-gradient(145deg, var(--sand), var(--sand-d));
color: var(--ink);
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: 19px;
letter-spacing: 0.5px;
}
.brand-label {
display: block;
font-size: 10px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
}
.brand-name {
margin: 2px 0 0;
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: 22px;
letter-spacing: 0.2px;
}
.site-meta {
display: flex;
align-items: center;
gap: 14px;
font-size: 13px;
color: var(--muted);
flex-wrap: wrap;
}
.meta-dot { color: var(--line-2); }
.meta-cta {
padding: 9px 18px;
border: 1px solid var(--line-2);
border-radius: 999px;
text-decoration: none;
color: var(--paper);
font-weight: 600;
font-size: 13px;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.meta-cta:hover {
background: var(--sand);
border-color: var(--sand);
color: var(--ink);
}
/* ---------- Intro ---------- */
.intro {
padding: clamp(38px, 7vw, 80px) clamp(18px, 5vw, 64px) 24px;
max-width: 780px;
}
.intro-label {
margin: 0 0 14px;
font-size: 11px;
letter-spacing: 0.26em;
text-transform: uppercase;
color: var(--sand);
}
.intro-title {
margin: 0 0 16px;
font-family: "Fraunces", Georgia, serif;
font-weight: 400;
font-size: clamp(30px, 5.5vw, 54px);
line-height: 1.08;
letter-spacing: -0.5px;
}
.intro-copy {
margin: 0;
max-width: 52ch;
color: var(--muted);
font-size: 15px;
}
/* ---------- Filter bar ---------- */
.filter-bar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
gap: 10px;
padding: 18px clamp(18px, 5vw, 64px);
background: rgba(16, 16, 18, 0.82);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
overflow-x: auto;
scrollbar-width: none;
}
.filter-bar::-webkit-scrollbar { display: none; }
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
padding: 9px 16px;
border: 1px solid var(--line-2);
border-radius: 999px;
background: transparent;
color: var(--paper);
font-family: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.12s ease;
}
.chip:hover { border-color: var(--sand); }
.chip:active { transform: scale(0.96); }
.chip.is-active {
background: var(--paper);
border-color: var(--paper);
color: var(--ink);
}
.chip-count {
display: inline-grid;
place-items: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 999px;
background: var(--surface-2);
color: var(--muted);
font-size: 11px;
font-weight: 600;
}
.chip.is-active .chip-count {
background: var(--ink);
color: var(--sand);
}
/* ---------- Masonry ---------- */
.masonry {
column-count: 4;
column-gap: 14px;
padding: 24px clamp(18px, 5vw, 64px) 60px;
}
@media (max-width: 1080px) { .masonry { column-count: 3; } }
@media (max-width: 760px) { .masonry { column-count: 2; } }
.frame {
position: relative;
display: block;
width: 100%;
margin: 0 0 14px;
padding: 0;
border: 0;
border-radius: var(--r-md);
overflow: hidden;
background: var(--surface);
cursor: pointer;
break-inside: avoid;
color: var(--paper);
text-align: left;
transform: translateY(0) scale(1);
opacity: 1;
transition: opacity 0.4s ease, transform 0.4s ease, box-shadow 0.25s ease;
}
.frame img {
width: 100%;
height: auto;
transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1), filter 0.4s ease;
filter: saturate(0.94) brightness(0.94);
}
.frame:hover { box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5); }
.frame:hover img { transform: scale(1.06); filter: saturate(1.05) brightness(1); }
.frame-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 4px;
padding: 16px;
background: linear-gradient(to top, rgba(11, 11, 12, 0.85) 0%, rgba(11, 11, 12, 0.1) 55%, transparent 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
.frame:hover .frame-overlay,
.frame:focus-visible .frame-overlay { opacity: 1; }
.frame-badge {
align-self: flex-start;
padding: 3px 9px;
border-radius: 999px;
background: var(--sand);
color: var(--ink);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.frame-title {
font-family: "Fraunces", Georgia, serif;
font-size: 17px;
font-weight: 600;
}
.frame-loc {
font-size: 12px;
color: var(--sand);
letter-spacing: 0.03em;
}
/* filter animation states */
.frame.is-hiding {
opacity: 0;
transform: translateY(12px) scale(0.94);
pointer-events: none;
}
.frame.is-gone { display: none; }
.frame.is-entering {
animation: frameIn 0.5s ease both;
}
@keyframes frameIn {
from { opacity: 0; transform: translateY(16px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ---------- Footer ---------- */
.site-footer {
padding: 48px clamp(18px, 5vw, 64px) 64px;
border-top: 1px solid var(--line);
}
.footer-name {
margin: 0 0 6px;
font-family: "Fraunces", Georgia, serif;
font-size: 20px;
}
.footer-copy { margin: 0; color: var(--muted); font-size: 14px; }
.footer-copy a { color: var(--sand); text-decoration: none; }
.footer-copy a:hover { text-decoration: underline; }
/* ---------- Lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 24px;
background: rgba(6, 6, 7, 0.94);
backdrop-filter: blur(6px);
animation: fadeIn 0.25s ease both;
}
.lightbox[hidden] { display: none; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.lb-stage {
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
max-width: min(90vw, 1100px);
}
.lb-img {
max-width: 100%;
max-height: 78vh;
border-radius: var(--r-md);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
animation: stageIn 0.35s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
@keyframes stageIn {
from { opacity: 0; transform: scale(0.97); }
to { opacity: 1; transform: scale(1); }
}
.lb-caption {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
color: var(--paper);
text-align: center;
}
.lb-badge {
padding: 3px 10px;
border-radius: 999px;
background: var(--sand);
color: var(--ink);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.lb-title { font-family: "Fraunces", Georgia, serif; font-size: 18px; }
.lb-loc { color: var(--muted); font-size: 13px; }
.lb-counter {
color: var(--muted);
font-size: 12px;
letter-spacing: 0.1em;
font-variant-numeric: tabular-nums;
}
.lb-close {
position: absolute;
top: 18px;
right: 20px;
width: 44px;
height: 44px;
border: 1px solid var(--line-2);
border-radius: 999px;
background: var(--surface);
color: var(--paper);
font-size: 26px;
line-height: 1;
cursor: pointer;
transition: background 0.2s ease, transform 0.12s ease;
}
.lb-close:hover { background: var(--surface-2); }
.lb-close:active { transform: scale(0.92); }
.lb-nav {
flex: 0 0 auto;
width: 52px;
height: 52px;
border: 1px solid var(--line-2);
border-radius: 999px;
background: var(--surface);
color: var(--paper);
font-size: 26px;
line-height: 1;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease, transform 0.12s ease;
}
.lb-nav:hover { background: var(--sand); border-color: var(--sand); color: var(--ink); }
.lb-nav:active { transform: scale(0.9); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
z-index: 120;
padding: 11px 20px;
border-radius: 999px;
background: var(--paper);
color: var(--ink);
font-size: 13px;
font-weight: 600;
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Mobile ---------- */
@media (max-width: 520px) {
.site-header { padding: 18px; }
.brand-name { font-size: 19px; }
.site-meta { width: 100%; font-size: 12px; }
.intro { padding: 32px 18px 18px; }
.filter-bar { padding: 14px 18px; gap: 8px; }
.chip { padding: 8px 13px; font-size: 12px; }
.masonry { column-count: 1; column-gap: 0; padding: 18px; }
.lb-nav { width: 44px; height: 44px; font-size: 22px; }
.lb-close { top: 12px; right: 12px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}// Photography — Portfolio Gallery
// Filterable CSS-columns masonry + keyboard-driven lightbox. Vanilla JS.
(function () {
"use strict";
var U = "https://images.unsplash.com/";
var Q = "?auto=format&fit=crop&w=900&q=70";
// Fictional portfolio archive. category drives filtering.
var PHOTOS = [
{ id: "p01", cat: "weddings", title: "First Dance", loc: "Sintra, PT", img: "photo-1519741497674-611481863552", ratio: 66 },
{ id: "p02", cat: "portraits", title: "Studio No. 4", loc: "Lisbon, PT", img: "photo-1494790108377-be9c29b29330", ratio: 125 },
{ id: "p03", cat: "products", title: "Ceramic Study", loc: "Porto, PT", img: "photo-1600585154340-be6161a56a0c", ratio: 75 },
{ id: "p04", cat: "travel", title: "Blue Hour Bridge", loc: "Kyoto, JP", img: "photo-1493976040374-85c8e12f0c0e", ratio: 66 },
{ id: "p05", cat: "weddings", title: "Vows at Dusk", loc: "Amalfi, IT", img: "photo-1465495976277-4387d4b0b4c6", ratio: 130 },
{ id: "p06", cat: "portraits", title: "Window Light", loc: "Lisbon, PT", img: "photo-1506794778202-cad84cf45f1d", ratio: 128 },
{ id: "p07", cat: "products", title: "Amber Bottle", loc: "Studio", img: "photo-1523275335684-37898b6baf30", ratio: 100 },
{ id: "p08", cat: "travel", title: "Salt Flats", loc: "Uyuni, BO", img: "photo-1470071459604-3b5ec3a7fe05", ratio: 66 },
{ id: "p09", cat: "weddings", title: "Confetti Exit", loc: "Sevilla, ES", img: "photo-1511285560929-80b456fea0bc", ratio: 70 },
{ id: "p10", cat: "portraits", title: "Golden Coat", loc: "Madrid, ES", img: "photo-1544005313-94ddf0286df2", ratio: 132 },
{ id: "p11", cat: "products", title: "Fresh Roast", loc: "Studio", img: "photo-1461023058943-07fcbe16d735", ratio: 66 },
{ id: "p12", cat: "travel", title: "Alpine Fog", loc: "Dolomites, IT", img: "photo-1506905925346-21bda4d32df4", ratio: 66 },
{ id: "p13", cat: "weddings", title: "Held Close", loc: "Sintra, PT", img: "photo-1583939003579-730e3918a45a", ratio: 120 },
{ id: "p14", cat: "portraits", title: "Quiet Profile", loc: "Lisbon, PT", img: "photo-1529626455594-4ff0802cfb7e", ratio: 100 },
{ id: "p15", cat: "products", title: "Leather & Brass", loc: "Studio", img: "photo-1553062407-98eeb64c6a62", ratio: 75 },
{ id: "p16", cat: "travel", title: "Terracotta Roofs", loc: "Marrakesh, MA", img: "photo-1539020140153-e479b8c22e70", ratio: 66 },
{ id: "p17", cat: "portraits", title: "Backlit Curls", loc: "Porto, PT", img: "photo-1531123897727-8f129e1688ce", ratio: 130 },
{ id: "p18", cat: "travel", title: "Harbor Morning", loc: "Nazaré, PT", img: "photo-1502602898657-3e91760cbb34", ratio: 66 }
];
var gallery = document.getElementById("gallery");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var toastEl = document.getElementById("toast");
var lightbox = document.getElementById("lightbox");
var lbImg = document.getElementById("lbImg");
var lbBadge = document.getElementById("lbBadge");
var lbTitle = document.getElementById("lbTitle");
var lbLoc = document.getElementById("lbLoc");
var lbCounter = document.getElementById("lbCounter");
var lbClose = document.getElementById("lbClose");
var lbPrev = document.getElementById("lbPrev");
var lbNext = document.getElementById("lbNext");
var LABELS = {
all: "All work",
weddings: "Weddings",
portraits: "Portraits",
products: "Products",
travel: "Travel"
};
var currentFilter = "all";
var visible = []; // photo objects currently shown (filtered order)
var lbIndex = -1; // index within `visible`
var lastFocused = null;
var toastTimer = null;
// ---- Build the grid ----
function buildGrid() {
var frag = document.createDocumentFragment();
PHOTOS.forEach(function (p) {
var btn = document.createElement("button");
btn.className = "frame is-entering";
btn.type = "button";
btn.dataset.cat = p.cat;
btn.dataset.id = p.id;
btn.setAttribute("aria-label", "View " + p.title + " — " + p.loc);
var img = document.createElement("img");
img.loading = "lazy";
img.decoding = "async";
img.src = U + p.img + Q;
img.alt = p.title + ", " + LABELS[p.cat] + " in " + p.loc;
// reserve space to reduce reflow jump
img.width = 900;
img.height = Math.round(900 * p.ratio / 100);
var overlay = document.createElement("div");
overlay.className = "frame-overlay";
overlay.innerHTML =
'<span class="frame-badge">' + LABELS[p.cat] + '</span>' +
'<span class="frame-title">' + p.title + '</span>' +
'<span class="frame-loc">' + p.loc + '</span>';
btn.appendChild(img);
btn.appendChild(overlay);
btn.addEventListener("click", function () { openLightbox(p.id, btn); });
frag.appendChild(btn);
});
gallery.appendChild(frag);
}
// ---- Chip counts ----
function paintCounts() {
var counts = { all: PHOTOS.length };
PHOTOS.forEach(function (p) { counts[p.cat] = (counts[p.cat] || 0) + 1; });
document.querySelectorAll(".chip-count").forEach(function (el) {
var key = el.getAttribute("data-count");
el.textContent = counts[key] || 0;
});
}
// ---- Filtering ----
function applyFilter(filter) {
currentFilter = filter;
visible = [];
var frames = gallery.querySelectorAll(".frame");
frames.forEach(function (frame) {
var match = filter === "all" || frame.dataset.cat === filter;
if (match) {
var wasHidden = frame.classList.contains("is-gone");
frame.classList.remove("is-gone", "is-hiding");
if (wasHidden) {
// re-entry animation
frame.classList.remove("is-entering");
void frame.offsetWidth; // reflow to restart animation
frame.classList.add("is-entering");
}
var photo = PHOTOS.filter(function (p) { return p.id === frame.dataset.id; })[0];
if (photo) visible.push(photo);
} else {
frame.classList.add("is-hiding");
}
});
// remove from flow after the fade so masonry recollapses
window.setTimeout(function () {
frames.forEach(function (frame) {
if (frame.classList.contains("is-hiding")) frame.classList.add("is-gone");
});
}, 400);
}
function setActiveChip(chip) {
chips.forEach(function (c) {
var on = c === chip;
c.classList.toggle("is-active", on);
c.setAttribute("aria-selected", on ? "true" : "false");
});
}
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
var filter = chip.dataset.filter;
if (filter === currentFilter) return;
setActiveChip(chip);
applyFilter(filter);
var n = filter === "all" ? PHOTOS.length : PHOTOS.filter(function (p) { return p.cat === filter; }).length;
toast(LABELS[filter] + " — " + n + " frame" + (n === 1 ? "" : "s"));
});
});
// ---- Lightbox ----
function openLightbox(id, trigger) {
lbIndex = visible.map(function (p) { return p.id; }).indexOf(id);
if (lbIndex < 0) return;
lastFocused = trigger || document.activeElement;
renderLightbox();
lightbox.hidden = false;
document.body.style.overflow = "hidden";
lbClose.focus();
}
function renderLightbox() {
var p = visible[lbIndex];
if (!p) return;
lbImg.src = U + p.img + "?auto=format&fit=crop&w=1600&q=80";
lbImg.alt = p.title + ", " + LABELS[p.cat] + " in " + p.loc;
// restart the stage-in animation
lbImg.style.animation = "none";
void lbImg.offsetWidth;
lbImg.style.animation = "";
lbBadge.textContent = LABELS[p.cat];
lbTitle.textContent = p.title;
lbLoc.textContent = p.loc;
lbCounter.textContent = (lbIndex + 1) + " / " + visible.length;
}
function closeLightbox() {
lightbox.hidden = true;
document.body.style.overflow = "";
if (lastFocused && lastFocused.focus) lastFocused.focus();
}
function step(dir) {
if (!visible.length) return;
lbIndex = (lbIndex + dir + visible.length) % visible.length;
renderLightbox();
}
lbClose.addEventListener("click", closeLightbox);
lbPrev.addEventListener("click", function () { step(-1); });
lbNext.addEventListener("click", function () { step(1); });
lightbox.addEventListener("click", function (e) {
if (e.target === lightbox) closeLightbox();
});
document.addEventListener("keydown", function (e) {
if (lightbox.hidden) return;
if (e.key === "Escape") closeLightbox();
else if (e.key === "ArrowRight") step(1);
else if (e.key === "ArrowLeft") step(-1);
});
// ---- Toast ----
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 2200);
}
// ---- Init ----
buildGrid();
paintCounts();
applyFilter("all");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Marlowe Reyes — Photography Portfolio</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=Fraunces:opsz,[email protected],400;9..144,600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="site-header">
<div class="brand">
<span class="brand-mark">MR</span>
<div class="brand-text">
<span class="brand-label">Photography</span>
<h1 class="brand-name">Marlowe Reyes</h1>
</div>
</div>
<nav class="site-meta" aria-label="Studio details">
<span class="meta-item">Based in Lisbon</span>
<span class="meta-dot" aria-hidden="true">•</span>
<span class="meta-item">Available worldwide</span>
<a class="meta-cta" href="#contact">Book a session</a>
</nav>
</header>
<section class="intro">
<p class="intro-label">Selected work — 2021 → 2026</p>
<h2 class="intro-title">Light, unhurried. Stories told in frames.</h2>
<p class="intro-copy">A rotating archive of weddings, portraits, product tables and long travel days. Filter the grid, then tap any frame to view it full-bleed.</p>
</section>
<div class="filter-bar" role="tablist" aria-label="Filter portfolio by category">
<button class="chip is-active" role="tab" aria-selected="true" data-filter="all">All work <span class="chip-count" data-count="all">0</span></button>
<button class="chip" role="tab" aria-selected="false" data-filter="weddings">Weddings <span class="chip-count" data-count="weddings">0</span></button>
<button class="chip" role="tab" aria-selected="false" data-filter="portraits">Portraits <span class="chip-count" data-count="portraits">0</span></button>
<button class="chip" role="tab" aria-selected="false" data-filter="products">Products <span class="chip-count" data-count="products">0</span></button>
<button class="chip" role="tab" aria-selected="false" data-filter="travel">Travel <span class="chip-count" data-count="travel">0</span></button>
</div>
<main class="masonry" id="gallery" aria-live="polite"><!-- frames injected by script.js --></main>
<footer class="site-footer" id="contact">
<p class="footer-name">Marlowe Reyes Studio</p>
<p class="footer-copy">Commissions open for late 2026. Say hello — <a href="mailto:[email protected]">[email protected]</a></p>
</footer>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Photo viewer" hidden>
<button class="lb-close" id="lbClose" aria-label="Close viewer">×</button>
<button class="lb-nav lb-prev" id="lbPrev" aria-label="Previous photo">‹</button>
<figure class="lb-stage">
<img class="lb-img" id="lbImg" alt="" />
<figcaption class="lb-caption">
<span class="lb-badge" id="lbBadge"></span>
<span class="lb-title" id="lbTitle"></span>
<span class="lb-loc" id="lbLoc"></span>
<span class="lb-counter" id="lbCounter"></span>
</figcaption>
</figure>
<button class="lb-nav lb-next" id="lbNext" aria-label="Next photo">›</button>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A dark, editorial portfolio for a fictional photographer. The header pairs a Fraunces display wordmark with thin uppercase labels, and a row of filter chips lets visitors narrow the grid by discipline: Weddings, Portraits, Products, and Travel. Selecting a chip animates the non-matching frames out with a subtle fade-and-scale while the survivors settle into a fresh CSS-columns masonry layout.
The masonry holds fifteen-plus photographs at natural aspect ratios, each with a hover overlay that surfaces the title, location, and a category badge. Clicking any frame opens a full-screen lightbox with the active image, a caption block, a live counter, and previous/next controls that only traverse the currently filtered set.
The lightbox is fully keyboard driven — arrow keys move between shots, Escape closes, and focus returns to the frame you opened it from. A small toast confirms filter changes, and everything degrades gracefully down to a single column at 360px.