Photography — Landing Product
A gallery-dark landing for a commercial product photographer: a full-bleed hero with a rotating specialty word, a filterable grid of product and lifestyle shots that reveal on scroll, a services rack covering e-commerce, lifestyle, and 360-degree spins, a scrolling client-logo marquee, and a two-step quote CTA with a live budget slider. Vanilla JS drives the filters, lightbox, counters, and a toast-backed inquiry flow — keyboard-usable and fluid down to 360px.
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;
--serif: "Fraunces", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--bg);
color: var(--paper);
font-family: var(--sans);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
img { max-width: 100%; display: block; }
.wrap { width: min(1160px, 92vw); margin-inline: auto; }
a { color: inherit; text-decoration: none; }
.skip-link {
position: absolute; left: -999px; top: 0; z-index: 100;
background: var(--sand); color: var(--ink);
padding: 10px 16px; border-radius: var(--r-sm); font-weight: 600;
}
.skip-link:focus { left: 12px; top: 12px; }
/* Focus visibility */
a:focus-visible, button:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible {
outline: 2px solid var(--sand);
outline-offset: 3px;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: .22em;
font-size: 11px;
font-weight: 600;
color: var(--sand);
margin: 0 0 14px;
}
/* ============ HEADER ============ */
.site-head {
position: sticky; top: 0; z-index: 40;
background: rgba(16, 16, 18, 0.72);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
}
.head-inner {
display: flex; align-items: center; justify-content: space-between;
height: 68px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
width: 14px; height: 14px; border-radius: 3px;
background: linear-gradient(135deg, var(--sand), var(--sand-d));
box-shadow: 0 0 0 4px rgba(201, 183, 156, 0.14);
}
.brand-name {
font-family: var(--serif); font-size: 20px; font-weight: 600;
letter-spacing: .01em;
}
.amp { color: var(--sand); padding: 0 1px; }
.nav { display: flex; align-items: center; gap: 26px; }
.nav a {
font-size: 14px; font-weight: 500; color: var(--muted);
transition: color .18s ease;
}
.nav a:hover { color: var(--paper); }
.nav-cta {
color: var(--ink) !important; background: var(--paper);
padding: 9px 16px; border-radius: 999px; font-weight: 600;
transition: transform .16s ease, background .18s ease;
}
.nav-cta:hover { background: var(--sand); transform: translateY(-1px); }
/* ============ HERO ============ */
.hero { position: relative; overflow: hidden; }
.hero-plate {
position: absolute; inset: 0;
background:
radial-gradient(120% 80% at 80% -10%, rgba(201, 183, 156, 0.20), transparent 55%),
radial-gradient(90% 70% at 0% 110%, rgba(168, 145, 111, 0.16), transparent 60%),
linear-gradient(180deg, #141418, #0d0d0f 70%);
}
.plate-grain {
position: absolute; inset: 0; opacity: .5;
background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px);
background-size: 4px 4px;
mask-image: linear-gradient(180deg, #000, transparent);
}
.hero-inner {
position: relative;
padding: clamp(64px, 12vw, 130px) 0 clamp(48px, 8vw, 96px);
}
.hero h1 {
font-family: var(--serif); font-weight: 600;
font-size: clamp(38px, 7.2vw, 76px);
line-height: 1.02; letter-spacing: -.015em;
margin: 0 0 24px; max-width: 15ch;
}
.rotator {
display: inline-block; color: var(--sand);
border-bottom: 3px solid rgba(201, 183, 156, 0.4);
min-width: 4ch;
}
.rot-word { display: inline-block; transition: opacity .3s ease, transform .3s ease; }
.rot-word.out { opacity: 0; transform: translateY(-8px); }
.lede {
color: var(--muted); font-size: clamp(15px, 2vw, 18px);
max-width: 54ch; margin: 0 0 30px;
}
.hero-actions { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 52px; }
.btn {
display: inline-flex; align-items: center; justify-content: center;
padding: 13px 24px; border-radius: 999px; font-size: 15px; font-weight: 600;
cursor: pointer; border: 1px solid transparent;
transition: transform .16s ease, background .18s ease, border-color .18s ease, color .18s ease;
}
.btn:active { transform: translateY(1px) scale(.99); }
.btn-primary { background: var(--paper); color: var(--ink); }
.btn-primary:hover { background: var(--sand); transform: translateY(-2px); }
.btn-ghost { background: transparent; color: var(--paper); border-color: var(--line-2); }
.btn-ghost:hover { border-color: var(--sand); color: var(--sand); }
.stats {
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px; margin: 0; max-width: 620px;
border-top: 1px solid var(--line); padding-top: 26px;
}
.stat dt {
text-transform: uppercase; letter-spacing: .18em; font-size: 10px;
color: var(--muted); font-weight: 600; margin-bottom: 6px;
}
.stat dd {
margin: 0; font-family: var(--serif); font-weight: 600;
font-size: clamp(30px, 5vw, 46px); line-height: 1;
}
/* ============ SECTIONS ============ */
.section { padding: clamp(56px, 9vw, 104px) 0; }
.section-alt { background: var(--surface); }
.sec-head {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 24px; flex-wrap: wrap; margin-bottom: 40px;
}
.sec-head.center { justify-content: center; text-align: center; }
.sec-head h2 {
font-family: var(--serif); font-weight: 600;
font-size: clamp(26px, 4.4vw, 42px); line-height: 1.06;
letter-spacing: -.01em; margin: 0; max-width: 18ch;
}
/* Reveal on scroll */
.reveal { opacity: 0; transform: translateY(22px); transition: opacity .6s ease, transform .6s ease; }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1; transform: none; transition: none; }
html { scroll-behavior: auto; }
}
/* ============ FILTERS ============ */
.filters { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
font-family: var(--sans); font-size: 12px; font-weight: 600;
text-transform: uppercase; letter-spacing: .12em;
padding: 9px 16px; border-radius: 999px; cursor: pointer;
background: transparent; color: var(--muted);
border: 1px solid var(--line-2);
transition: color .18s, border-color .18s, background .18s;
}
.chip:hover { color: var(--paper); border-color: var(--sand); }
.chip.is-active {
background: var(--sand); color: var(--ink); border-color: var(--sand);
}
/* ============ WORK GRID ============ */
.grid {
display: grid; gap: 16px;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
}
.card {
position: relative; overflow: hidden; border-radius: var(--r-md);
aspect-ratio: 4 / 5; cursor: pointer; border: 1px solid var(--line);
background: var(--surface-2);
transition: transform .3s ease, box-shadow .3s ease, opacity .35s ease;
}
.card.tall { aspect-ratio: 4 / 6; }
.card.wide { grid-column: span 2; aspect-ratio: 16 / 9; }
.card-img { position: absolute; inset: 0; transition: transform .5s ease; }
.card:hover { transform: translateY(-4px); box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5); }
.card:hover .card-img { transform: scale(1.06); }
.card-shade {
position: absolute; inset: 0;
background: linear-gradient(180deg, transparent 40%, rgba(8, 8, 10, 0.85));
opacity: .7; transition: opacity .3s ease;
}
.card:hover .card-shade { opacity: .95; }
.card-meta {
position: absolute; left: 16px; right: 16px; bottom: 14px;
transform: translateY(6px); opacity: .92; transition: transform .3s ease;
}
.card:hover .card-meta { transform: none; }
.card-cat {
display: inline-block; font-size: 10px; font-weight: 700;
text-transform: uppercase; letter-spacing: .14em; color: var(--ink);
background: var(--sand); padding: 3px 8px; border-radius: 999px; margin-bottom: 8px;
}
.card-title { font-family: var(--serif); font-size: 18px; font-weight: 600; margin: 0; }
.card-sub { font-size: 12px; color: var(--muted); margin: 2px 0 0; }
.card.hide { display: none; }
.grid-empty { text-align: center; color: var(--muted); padding: 40px 0; }
/* ============ SERVICES ============ */
.svc-rack {
display: grid; gap: 18px;
grid-template-columns: repeat(3, 1fr); align-items: stretch;
}
.svc-card {
position: relative; background: var(--bg); border: 1px solid var(--line);
border-radius: var(--r-lg); padding: 30px 26px 26px;
display: flex; flex-direction: column;
transition: transform .25s ease, border-color .25s ease;
}
.section-alt .svc-card { background: var(--surface-2); }
.svc-card:hover { transform: translateY(-5px); border-color: var(--sand-d); }
.svc-card.is-feat { border-color: var(--sand); box-shadow: 0 0 0 1px rgba(201, 183, 156, 0.25); }
.svc-num {
font-family: var(--serif); font-size: 13px; color: var(--sand-d);
letter-spacing: .1em; margin-bottom: 10px; display: block;
}
.svc-badge {
position: absolute; top: -11px; right: 22px;
font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .12em;
background: var(--sand); color: var(--ink); padding: 4px 11px; border-radius: 999px;
}
.svc-card h3 { font-family: var(--serif); font-size: 23px; font-weight: 600; margin: 0 0 10px; }
.svc-card > p { color: var(--muted); font-size: 14px; margin: 0 0 18px; }
.svc-list { list-style: none; margin: 0 0 22px; padding: 0; display: grid; gap: 10px; }
.svc-list li {
font-size: 14px; color: var(--paper); padding-left: 24px; position: relative;
}
.svc-list li::before {
content: ""; position: absolute; left: 0; top: 7px;
width: 12px; height: 7px; border-left: 2px solid var(--sand);
border-bottom: 2px solid var(--sand); transform: rotate(-45deg);
}
.svc-price {
margin: auto 0 0; padding-top: 18px; border-top: 1px solid var(--line);
font-family: var(--serif); font-size: 26px; font-weight: 600;
}
.svc-price span { font-family: var(--sans); font-size: 11px; text-transform: uppercase;
letter-spacing: .14em; color: var(--muted); display: block; margin-bottom: 2px; font-weight: 600; }
.svc-price em { font-family: var(--sans); font-size: 13px; color: var(--muted); font-style: normal; }
/* ============ MARQUEE ============ */
.marquee {
overflow: hidden; margin-top: 34px;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
border-block: 1px solid var(--line); padding: 26px 0;
}
.marquee-track {
display: flex; gap: 56px; width: max-content;
animation: scrollx 34s linear infinite;
}
.marquee:hover .marquee-track, .marquee-track.paused { animation-play-state: paused; }
@keyframes scrollx { to { transform: translateX(-50%); } }
.logo {
font-family: var(--serif); font-size: clamp(20px, 3vw, 30px); font-weight: 600;
color: var(--muted); white-space: nowrap; opacity: .7;
transition: color .2s ease, opacity .2s ease; letter-spacing: -.01em;
}
.logo:hover { color: var(--sand); opacity: 1; }
/* ============ QUOTE ============ */
.quote-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: clamp(28px, 5vw, 64px);
align-items: start;
}
.quote-copy h2 { font-family: var(--serif); font-size: clamp(28px, 4.6vw, 46px);
font-weight: 600; line-height: 1.05; margin: 0 0 18px; }
.quote-perks { list-style: none; margin: 24px 0 0; padding: 0; display: grid; gap: 12px; }
.quote-perks li { position: relative; padding-left: 26px; color: var(--paper); font-size: 15px; }
.quote-perks li::before {
content: ""; position: absolute; left: 0; top: 8px; width: 8px; height: 8px;
border-radius: 50%; background: var(--sand);
}
.quote-form {
background: var(--bg); border: 1px solid var(--line);
border-radius: var(--r-lg); padding: 28px;
}
.steps { display: flex; align-items: center; gap: 12px; margin-bottom: 22px; }
.step {
width: 30px; height: 30px; border-radius: 50%;
display: grid; place-items: center; font-weight: 700; font-size: 14px;
background: var(--surface-2); color: var(--muted);
border: 1px solid var(--line-2); transition: all .2s ease;
}
.step.is-active { background: var(--sand); color: var(--ink); border-color: var(--sand); }
.step-line { flex: 1; height: 1px; background: var(--line-2); }
.q-panel { border: 0; margin: 0; padding: 0; display: none; }
.q-panel.is-active { display: block; animation: fadeUp .35s ease; }
.q-panel legend {
font-family: var(--serif); font-size: 20px; font-weight: 600; padding: 0; margin-bottom: 16px;
}
@keyframes fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.field { display: block; margin-bottom: 16px; }
.field > span {
display: flex; justify-content: space-between; align-items: baseline;
font-size: 13px; font-weight: 600; color: var(--muted); margin-bottom: 7px;
}
.field > span b { font-family: var(--serif); font-size: 15px; color: var(--sand); }
.field input[type="text"], .field input[type="email"], .field select, .field textarea {
width: 100%; padding: 12px 14px; font-family: var(--sans); font-size: 15px;
background: var(--surface-2); color: var(--paper);
border: 1px solid var(--line-2); border-radius: var(--r-sm);
transition: border-color .18s ease;
}
.field input::placeholder, .field textarea::placeholder { color: #6d6d74; }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--sand); }
.field textarea { resize: vertical; }
.field.invalid input, .field.invalid select { border-color: #e0796a; }
/* budget range */
input[type="range"] {
-webkit-appearance: none; appearance: none; width: 100%; height: 5px;
border-radius: 999px; background: var(--surface-2); border: 1px solid var(--line-2);
padding: 0; cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 20px; height: 20px;
border-radius: 50%; background: var(--sand); border: 3px solid var(--bg);
box-shadow: 0 0 0 1px var(--sand-d);
}
input[type="range"]::-moz-range-thumb {
width: 20px; height: 20px; border-radius: 50%; background: var(--sand);
border: 3px solid var(--bg); box-shadow: 0 0 0 1px var(--sand-d);
}
.q-actions { display: flex; gap: 12px; margin-top: 22px; }
.q-actions .btn { flex: 1; }
/* ============ FOOTER ============ */
.site-foot { border-top: 1px solid var(--line); padding: 40px 0; }
.foot-inner { display: grid; gap: 8px; }
.foot-inner .brand-name { font-size: 22px; }
.foot-inner p { color: var(--muted); font-size: 14px; margin: 0; }
.foot-inner .fine { font-size: 12px; color: #6d6d74; }
/* ============ LIGHTBOX ============ */
.lightbox {
position: fixed; inset: 0; z-index: 90;
display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px;
padding: clamp(16px, 4vw, 48px);
background: rgba(7, 7, 9, 0.94); backdrop-filter: blur(6px);
animation: fadeUp .25s ease;
}
.lightbox[hidden] { display: none; }
.lb-figure { margin: 0; max-width: 900px; width: 100%; }
.lb-img {
width: 100%; aspect-ratio: 3 / 2; border-radius: var(--r-md);
border: 1px solid var(--line-2);
}
.lb-figure figcaption {
text-align: center; color: var(--paper); margin-top: 14px;
font-family: var(--serif); font-size: 17px;
}
.lb-close {
position: absolute; top: 18px; right: 22px;
width: 44px; height: 44px; border-radius: 50%; cursor: pointer;
background: var(--surface); color: var(--paper); border: 1px solid var(--line-2);
font-size: 26px; line-height: 1; transition: background .18s ease;
}
.lb-close:hover { background: var(--surface-2); color: var(--sand); }
.lb-nav {
width: 52px; height: 52px; border-radius: 50%; cursor: pointer;
background: var(--surface); color: var(--paper); border: 1px solid var(--line-2);
font-size: 28px; line-height: 1; transition: background .18s ease, color .18s ease;
}
.lb-nav:hover { background: var(--sand); color: var(--ink); }
/* ============ TOAST ============ */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 140%);
background: var(--paper); color: var(--ink);
padding: 13px 22px; border-radius: 999px; font-weight: 600; font-size: 14px;
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5); z-index: 95;
transition: transform .35s cubic-bezier(.2, .9, .3, 1.2); max-width: 88vw;
}
.toast.show { transform: translate(-50%, 0); }
/* ============ RESPONSIVE ============ */
@media (max-width: 900px) {
.grid { grid-template-columns: repeat(2, 1fr); }
.svc-rack { grid-template-columns: 1fr; }
.quote-grid { grid-template-columns: 1fr; }
.card.wide { grid-column: span 2; }
}
@media (max-width: 520px) {
.nav { gap: 14px; }
.nav a:not(.nav-cta) { display: none; }
.hero h1 { font-size: 40px; }
.stats { grid-template-columns: 1fr; gap: 14px; }
.stat { display: flex; align-items: baseline; justify-content: space-between; }
.stat dt { margin: 0; }
.grid { grid-template-columns: 1fr; }
.card, .card.tall, .card.wide { aspect-ratio: 4 / 3; grid-column: auto; }
.sec-head { flex-direction: column; align-items: flex-start; }
.lightbox { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
.lb-nav { justify-self: center; }
.q-actions { flex-direction: column-reverse; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
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");
}, 2600);
}
/* ---------- Hero rotator ---------- */
var words = ["sneakers", "fragrance", "ceramics", "eyewear", "skincare"];
var rotEl = document.getElementById("rotWord");
var ri = 0;
if (rotEl) {
setInterval(function () {
rotEl.classList.add("out");
setTimeout(function () {
ri = (ri + 1) % words.length;
rotEl.textContent = words[ri];
rotEl.classList.remove("out");
}, 300);
}, 2400);
}
/* ---------- Work grid data ---------- */
var shade = function (a, b) {
return "linear-gradient(150deg," + a + "," + b + ")";
};
var shots = [
{ cat: "ecom", tag: "E-commerce", title: "Aria Runner", sub: "Pure-white packshot", g: shade("#2a2a30", "#0f0f12"), size: "" },
{ cat: "lifestyle", tag: "Lifestyle", title: "Morning Pour", sub: "Ceramics · natural light", g: shade("#3a2f24", "#14100b"), size: "wide" },
{ cat: "spin", tag: "360° spin", title: "Halo Bottle", sub: "36-frame turntable", g: shade("#20303a", "#0b1116"), size: "" },
{ cat: "ecom", tag: "E-commerce", title: "Field Watch 04", sub: "Ghost-mount detail", g: shade("#2d2b26", "#111014"), size: "tall" },
{ cat: "lifestyle", tag: "Lifestyle", title: "Golden Hour Set", sub: "Eyewear editorial", g: shade("#3a2e2c", "#130f0e"), size: "" },
{ cat: "spin", tag: "360° spin", title: "Terra Mug", sub: "24-frame spin", g: shade("#25302b", "#0d110f"), size: "" },
{ cat: "ecom", tag: "E-commerce", title: "Silk Serum", sub: "Reflection-free glass", g: shade("#2c2733", "#100e15"), size: "" },
{ cat: "lifestyle", tag: "Lifestyle", title: "Studio Flatlay", sub: "Skincare props", g: shade("#332a2a", "#120e0e"), size: "wide" },
];
var grid = document.getElementById("grid");
var gridEmpty = document.getElementById("gridEmpty");
shots.forEach(function (s, i) {
var card = document.createElement("button");
card.className = "card" + (s.size ? " " + s.size : "");
card.type = "button";
card.setAttribute("role", "listitem");
card.dataset.cat = s.cat;
card.dataset.index = i;
card.setAttribute("aria-label", "View " + s.title + ", " + s.tag);
card.innerHTML =
'<div class="card-img" style="background:' + s.g + '"></div>' +
'<div class="card-shade"></div>' +
'<div class="card-meta">' +
'<span class="card-cat">' + s.tag + "</span>" +
'<h3 class="card-title">' + s.title + "</h3>" +
'<p class="card-sub">' + s.sub + "</p>" +
"</div>";
card.addEventListener("click", function () {
openLightbox(i);
});
grid.appendChild(card);
});
/* ---------- Filters ---------- */
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
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.dataset.filter;
var visible = 0;
Array.prototype.slice.call(grid.children).forEach(function (card) {
var show = f === "all" || card.dataset.cat === f;
card.classList.toggle("hide", !show);
if (show) visible++;
});
gridEmpty.hidden = visible !== 0;
});
});
/* ---------- Lightbox ---------- */
var lb = document.getElementById("lightbox");
var lbImg = document.getElementById("lbImg");
var lbCap = document.getElementById("lbCap");
var lbClose = document.getElementById("lbClose");
var lbPrev = document.getElementById("lbPrev");
var lbNext = document.getElementById("lbNext");
var current = 0;
var lastFocused = null;
function visibleIndices() {
return shots
.map(function (_, i) { return i; })
.filter(function (i) {
return !grid.children[i].classList.contains("hide");
});
}
function renderLb(i) {
var s = shots[i];
lbImg.style.background = s.g;
lbCap.textContent = s.title + " — " + s.tag;
current = i;
}
function openLightbox(i) {
lastFocused = document.activeElement;
renderLb(i);
lb.hidden = false;
document.body.style.overflow = "hidden";
lbClose.focus();
}
function closeLightbox() {
lb.hidden = true;
document.body.style.overflow = "";
if (lastFocused) lastFocused.focus();
}
function step(dir) {
var vis = visibleIndices();
if (!vis.length) return;
var pos = vis.indexOf(current);
pos = (pos + dir + vis.length) % vis.length;
renderLb(vis[pos]);
}
lbClose.addEventListener("click", closeLightbox);
lbPrev.addEventListener("click", function () { step(-1); });
lbNext.addEventListener("click", function () { step(1); });
lb.addEventListener("click", function (e) {
if (e.target === lb) closeLightbox();
});
document.addEventListener("keydown", function (e) {
if (lb.hidden) return;
if (e.key === "Escape") closeLightbox();
else if (e.key === "ArrowRight") step(1);
else if (e.key === "ArrowLeft") step(-1);
else if (e.key === "Tab") {
// simple focus trap
var f = [lbClose, lbPrev, lbNext];
var idx = f.indexOf(document.activeElement);
e.preventDefault();
var nextIdx = e.shiftKey ? (idx - 1 + f.length) % f.length : (idx + 1) % f.length;
f[nextIdx].focus();
}
});
/* ---------- Client marquee ---------- */
var brands = [
"Northwind", "Lumen&Co", "Fjordlæ", "Atlas Goods", "Verano",
"Kettle&Fern", "Osmo", "Marlowe", "Basalt", "Suniva",
];
var track = document.getElementById("marqueeTrack");
function fillTrack() {
var html = brands
.map(function (b) { return '<span class="logo">' + b + "</span>"; })
.join("");
track.innerHTML = html + html; // duplicate for seamless loop
}
fillTrack();
/* ---------- Reveal on scroll + stat counters ---------- */
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var statsDone = false;
function runStats() {
if (statsDone) return;
statsDone = true;
document.querySelectorAll(".stat dd").forEach(function (el) {
var target = parseInt(el.dataset.count, 10);
var suffix = el.dataset.suffix || "";
if (reduce) { el.textContent = target.toLocaleString() + suffix; return; }
var start = performance.now();
var dur = 1400;
function tick(now) {
var p = Math.min((now - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(target * eased).toLocaleString() + suffix;
if (p < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
}
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) {
en.target.classList.add("in");
io.unobserve(en.target);
}
});
},
{ threshold: 0.15 }
);
document.querySelectorAll(".reveal").forEach(function (el) { io.observe(el); });
var statsIo = new IntersectionObserver(
function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) { runStats(); statsIo.disconnect(); }
});
},
{ threshold: 0.4 }
);
var statsSec = document.getElementById("stats");
if (statsSec) statsIo.observe(statsSec);
} else {
document.querySelectorAll(".reveal").forEach(function (el) { el.classList.add("in"); });
runStats();
}
/* ---------- Quote form: steps + budget + validation ---------- */
var form = document.getElementById("quoteForm");
var panels = form.querySelectorAll(".q-panel");
var stepDots = form.querySelectorAll(".step");
var budget = document.getElementById("budget");
var budgetOut = document.getElementById("budgetOut");
function euro(n) {
return "€" + Number(n).toLocaleString("en-US");
}
budget.addEventListener("input", function () {
budgetOut.textContent = euro(budget.value);
});
function showPanel(n) {
panels.forEach(function (p) {
p.classList.toggle("is-active", p.dataset.panel === String(n));
});
stepDots.forEach(function (d) {
d.classList.toggle("is-active", Number(d.dataset.step) <= n);
});
}
function validatePanel(n) {
var panel = form.querySelector('.q-panel[data-panel="' + n + '"]');
var ok = true;
panel.querySelectorAll("input[required], select[required]").forEach(function (el) {
var field = el.closest(".field");
var valid = el.value.trim() !== "" && (el.type !== "email" || /\S+@\S+\.\S+/.test(el.value));
field.classList.toggle("invalid", !valid);
if (!valid && ok) { el.focus(); ok = false; }
});
return ok;
}
form.querySelector("[data-next]").addEventListener("click", function () {
if (validatePanel(1)) showPanel(2);
else toast("Please complete the highlighted fields.");
});
form.querySelector("[data-back]").addEventListener("click", function () {
showPanel(1);
});
form.addEventListener("submit", function (e) {
e.preventDefault();
if (!validatePanel(2)) { toast("Please complete the highlighted fields."); return; }
var name = document.getElementById("name").value.trim().split(" ")[0] || "there";
form.reset();
budgetOut.textContent = euro(2500);
showPanel(1);
toast("Thanks " + name + " — we'll reply within one business day.");
});
// Clear invalid state as the user types
form.querySelectorAll("input, select, textarea").forEach(function (el) {
el.addEventListener("input", function () {
var field = el.closest(".field");
if (field) field.classList.remove("invalid");
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Halbrand & Co. — Commercial Product Photography</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:wght@400;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#work">Skip to work</a>
<!-- ============ HEADER ============ -->
<header class="site-head" id="top">
<div class="wrap head-inner">
<a class="brand" href="#top" aria-label="Halbrand and Company home">
<span class="brand-mark" aria-hidden="true"></span>
<span class="brand-name">Halbrand<span class="amp">&</span>Co.</span>
</a>
<nav class="nav" aria-label="Primary">
<a href="#work">Work</a>
<a href="#services">Services</a>
<a href="#clients">Clients</a>
<a class="nav-cta" href="#quote">Get a quote</a>
</nav>
</div>
</header>
<main>
<!-- ============ HERO ============ -->
<section class="hero" aria-labelledby="hero-h">
<div class="hero-plate" aria-hidden="true">
<span class="plate-grain"></span>
</div>
<div class="wrap hero-inner">
<p class="eyebrow">Commercial product studio · Est. 2014 · Lisbon</p>
<h1 id="hero-h">
Photography that sells your
<span class="rotator" aria-live="polite"><span class="rot-word" id="rotWord">sneakers</span></span>
</h1>
<p class="lede">
We light, style and retouch products so they look irresistible on the shelf and on the feed.
E-commerce packshots, lifestyle sets and 360° spins — shot in-house, delivered fast.
</p>
<div class="hero-actions">
<a class="btn btn-primary" href="#quote">Start a project</a>
<a class="btn btn-ghost" href="#work">See recent work</a>
</div>
<dl class="stats" id="stats">
<div class="stat"><dt>Shoots delivered</dt><dd data-count="1840" data-suffix="+">0</dd></div>
<div class="stat"><dt>Brands served</dt><dd data-count="212" data-suffix="">0</dd></div>
<div class="stat"><dt>Avg turnaround</dt><dd data-count="48" data-suffix="h">0</dd></div>
</dl>
</div>
</section>
<!-- ============ WORK GRID ============ -->
<section class="section" id="work" aria-labelledby="work-h">
<div class="wrap">
<div class="sec-head reveal">
<div>
<p class="eyebrow">Selected work</p>
<h2 id="work-h">A frame for every product</h2>
</div>
<div class="filters" role="group" aria-label="Filter work by category">
<button class="chip is-active" data-filter="all" aria-pressed="true">All</button>
<button class="chip" data-filter="ecom" aria-pressed="false">E-commerce</button>
<button class="chip" data-filter="lifestyle" aria-pressed="false">Lifestyle</button>
<button class="chip" data-filter="spin" aria-pressed="false">360° spin</button>
</div>
</div>
<div class="grid" id="grid" role="list">
<!-- cards injected by script.js -->
</div>
<p class="grid-empty" id="gridEmpty" hidden>No shots in this category yet.</p>
</div>
</section>
<!-- ============ SERVICES ============ -->
<section class="section section-alt" id="services" aria-labelledby="svc-h">
<div class="wrap">
<div class="sec-head reveal">
<div>
<p class="eyebrow">What we shoot</p>
<h2 id="svc-h">Three ways to make it look expensive</h2>
</div>
</div>
<div class="svc-rack">
<article class="svc-card reveal">
<span class="svc-num">01</span>
<h3>E-commerce packshots</h3>
<p>Crisp, shadowless product cutouts on pure white — consistent, marketplace-ready and colour-accurate.</p>
<ul class="svc-list">
<li>Ghost mannequin & flat lay</li>
<li>360-image consistency guide</li>
<li>Clipping paths included</li>
</ul>
<p class="svc-price"><span>From</span> €38 <em>/ shot</em></p>
</article>
<article class="svc-card is-feat reveal">
<span class="svc-badge">Most booked</span>
<span class="svc-num">02</span>
<h3>Lifestyle sets</h3>
<p>Styled scenes with props, talent and natural light that show the product living in the wild.</p>
<ul class="svc-list">
<li>Set design & prop sourcing</li>
<li>Model & hand talent</li>
<li>Social-ready crops delivered</li>
</ul>
<p class="svc-price"><span>From</span> €1,450 <em>/ half day</em></p>
</article>
<article class="svc-card reveal">
<span class="svc-num">03</span>
<h3>360° spins</h3>
<p>Interactive turntable capture that lets shoppers rotate the product in their browser.</p>
<ul class="svc-list">
<li>24 or 36 frame sequences</li>
<li>Embeddable viewer files</li>
<li>Retouched every frame</li>
</ul>
<p class="svc-price"><span>From</span> €220 <em>/ product</em></p>
</article>
</div>
</div>
</section>
<!-- ============ CLIENTS ============ -->
<section class="section" id="clients" aria-labelledby="cli-h">
<div class="wrap">
<div class="sec-head reveal center">
<div>
<p class="eyebrow">Trusted by makers</p>
<h2 id="cli-h">On shelves, feeds and storefronts worldwide</h2>
</div>
</div>
</div>
<div class="marquee" id="marquee" aria-label="Client logos">
<div class="marquee-track" id="marqueeTrack">
<!-- wordmarks injected + duplicated by script.js -->
</div>
</div>
</section>
<!-- ============ QUOTE CTA ============ -->
<section class="section section-alt quote" id="quote" aria-labelledby="q-h">
<div class="wrap quote-grid">
<div class="quote-copy reveal">
<p class="eyebrow">Get a quote</p>
<h2 id="q-h">Tell us what you’re launching</h2>
<p class="lede">Two quick steps and we’ll come back within one business day with a shot list and a fixed price. No retainer, no surprises.</p>
<ul class="quote-perks">
<li>Fixed per-project pricing</li>
<li>Licensing for web & paid social included</li>
<li>Re-shoots free if it misses the brief</li>
</ul>
</div>
<form class="quote-form reveal" id="quoteForm" novalidate>
<div class="steps" aria-hidden="true">
<span class="step is-active" data-step="1">1</span>
<span class="step-line"></span>
<span class="step" data-step="2">2</span>
</div>
<fieldset class="q-panel is-active" data-panel="1">
<legend>The project</legend>
<label class="field">
<span>Brand / company</span>
<input type="text" name="brand" id="brand" placeholder="e.g. Northwind Ceramics" required />
</label>
<label class="field">
<span>What are we shooting?</span>
<select name="type" id="type" required>
<option value="">Choose a package…</option>
<option>E-commerce packshots</option>
<option>Lifestyle set</option>
<option>360° spin</option>
<option>Not sure yet</option>
</select>
</label>
<label class="field">
<span>Approx. budget <b id="budgetOut">€2,500</b></span>
<input type="range" name="budget" id="budget" min="500" max="15000" step="250" value="2500" />
</label>
<div class="q-actions">
<button type="button" class="btn btn-primary" data-next>Continue</button>
</div>
</fieldset>
<fieldset class="q-panel" data-panel="2">
<legend>Your details</legend>
<label class="field">
<span>Your name</span>
<input type="text" name="name" id="name" placeholder="Full name" required />
</label>
<label class="field">
<span>Email</span>
<input type="email" name="email" id="email" placeholder="[email protected]" required />
</label>
<label class="field">
<span>Anything else? (optional)</span>
<textarea name="notes" id="notes" rows="3" placeholder="Deadlines, product count, references…"></textarea>
</label>
<div class="q-actions">
<button type="button" class="btn btn-ghost" data-back>Back</button>
<button type="submit" class="btn btn-primary">Send brief</button>
</div>
</fieldset>
</form>
</div>
</section>
</main>
<!-- ============ FOOTER ============ -->
<footer class="site-foot">
<div class="wrap foot-inner">
<span class="brand-name">Halbrand<span class="amp">&</span>Co.</span>
<p>Commercial product photography · Rua do Alecrim 42, Lisbon · [email protected]</p>
<p class="fine">© 2026 Halbrand & Co. Fictional studio for demo purposes.</p>
</div>
</footer>
<!-- ============ LIGHTBOX ============ -->
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Image viewer" hidden>
<button class="lb-close" id="lbClose" aria-label="Close viewer">×</button>
<button class="lb-nav lb-prev" id="lbPrev" aria-label="Previous image">‹</button>
<figure class="lb-figure">
<div class="lb-img" id="lbImg" aria-hidden="true"></div>
<figcaption id="lbCap"></figcaption>
</figure>
<button class="lb-nav lb-next" id="lbNext" aria-label="Next image">›</button>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A dark, image-first landing page for the fictional commercial studio Halbrand & Co., built for brands that sell things worth photographing. The hero pairs a serif Fraunces headline with a rotating specialty word — sneakers, fragrance, ceramics — over a full-bleed gradient plate, plus live counters that tally shoots delivered, brands served, and average turnaround. Thin uppercase labels and warm sand accents carry the gallery mood.
Below the fold, a work grid of product and lifestyle shots filters by category through a row of chips, with each card lifting on hover and opening in a keyboard-navigable lightbox. A services rack lays out e-commerce packshots, lifestyle sets, and 360-degree spins as priced cards, a client marquee scrolls fictional wordmarks on an infinite loop, and a closing quote form walks through a two-step brief with a live budget slider.
Every interaction is vanilla JS: filter chips animate the grid, the lightbox traps focus and
responds to arrow keys, IntersectionObserver fades sections up and starts the stat counters, the
marquee pauses on hover, and submitting the quote drops a toast. Focus rings stay visible
throughout and the whole page reflows cleanly at 360px.