Creator — Photographer Landing
A gallery-quiet personal landing page for a fictional photographer, built in vanilla HTML, CSS and JavaScript. A full-bleed cinematic hero opens onto a filterable masonry portfolio with a keyboard-accessible lightbox, then services with pricing, a story-driven about block with stats, client logos, testimonials and a validating booking form. Minimal sans typography, an ink-on-white palette and tasteful scroll reveals keep the focus on the work.
MCP
Code
:root {
--ink: #16161a;
--ink-soft: #44454d;
--ink-faint: #8a8b93;
--paper: #ffffff;
--paper-warm: #f6f4f1;
--line: #e7e4df;
--accent: #16161a;
--shadow: 0 24px 60px -28px rgba(22, 22, 26, 0.35);
--max: 1160px;
--ease: cubic-bezier(0.22, 0.61, 0.36, 1);
--t1: #d9cfc4;
--t2: #b9c2bf;
--t3: #cfc6d4;
--t4: #c8c0b5;
--t5: #d6cdc6;
--t6: #bcc3cb;
--t7: #c4bcb6;
--t8: #cdc4bb;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
font-weight: 400;
color: var(--ink);
background: var(--paper);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
h1, h2, h3 {
font-family: "Fraunces", Georgia, serif;
font-weight: 300;
letter-spacing: -0.01em;
line-height: 1.05;
margin: 0;
}
.skip-link {
position: absolute;
left: -999px;
top: 8px;
background: var(--ink);
color: #fff;
padding: 10px 16px;
border-radius: 4px;
z-index: 200;
}
.skip-link:focus { left: 16px; }
:focus-visible {
outline: 2px solid var(--ink);
outline-offset: 3px;
}
/* ---------- NAV ---------- */
.nav {
position: fixed;
inset: 0 0 auto 0;
z-index: 100;
background: rgba(255, 255, 255, 0.78);
backdrop-filter: saturate(160%) blur(14px);
border-bottom: 1px solid transparent;
transition: border-color 0.4s, background 0.4s;
}
.nav.is-scrolled { border-color: var(--line); }
.nav__inner {
max-width: var(--max);
margin: 0 auto;
padding: 18px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
letter-spacing: 0.02em;
}
.brand__mark { font-size: 1.15rem; }
.brand__name { font-size: 0.98rem; }
.nav__links {
display: flex;
align-items: center;
gap: 30px;
font-size: 0.9rem;
color: var(--ink-soft);
}
.nav__links a { position: relative; transition: color 0.25s; }
.nav__links a:not(.nav__cta)::after {
content: "";
position: absolute;
left: 0; bottom: -5px;
width: 100%; height: 1px;
background: var(--ink);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s var(--ease);
}
.nav__links a:not(.nav__cta):hover { color: var(--ink); }
.nav__links a:not(.nav__cta):hover::after { transform: scaleX(1); }
.nav__cta {
border: 1px solid var(--ink);
padding: 9px 18px;
border-radius: 100px;
color: var(--ink);
font-weight: 500;
transition: background 0.25s, color 0.25s;
}
.nav__cta:hover { background: var(--ink); color: #fff; }
.nav__toggle {
display: none;
flex-direction: column;
gap: 5px;
background: none;
border: 0;
padding: 8px;
cursor: pointer;
}
.nav__toggle span {
width: 24px; height: 2px;
background: var(--ink);
transition: transform 0.3s, opacity 0.3s;
}
.nav__toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.nav__mobile {
display: flex;
flex-direction: column;
padding: 8px 24px 22px;
gap: 4px;
border-top: 1px solid var(--line);
background: var(--paper);
}
.nav__mobile a {
padding: 13px 4px;
font-size: 1.05rem;
border-bottom: 1px solid var(--line);
color: var(--ink-soft);
}
.nav__mobile a:last-child { border-bottom: 0; color: var(--ink); font-weight: 600; }
/* ---------- BUTTONS ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 26px;
border-radius: 100px;
font-size: 0.92rem;
font-weight: 500;
letter-spacing: 0.01em;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.25s var(--ease), background 0.25s, color 0.25s, border-color 0.25s;
}
.btn--solid { background: var(--ink); color: #fff; }
.btn--solid:hover { transform: translateY(-2px); background: #2b2b32; }
.btn--ghost { border-color: rgba(255,255,255,0.55); color: #fff; }
.btn--ghost:hover { background: rgba(255,255,255,0.12); border-color: #fff; }
.btn--full { width: 100%; }
/* ---------- HERO ---------- */
.hero {
position: relative;
min-height: 100vh;
display: flex;
align-items: flex-end;
color: #fff;
overflow: hidden;
}
.hero__media {
position: absolute;
inset: 0;
background:
radial-gradient(120% 90% at 75% 15%, #5b5246 0%, #2c2823 45%, #14110d 100%);
transform: scale(1.06);
animation: heroZoom 18s ease-in-out infinite alternate;
}
.hero__media::after {
content: "";
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 30% 40%, rgba(255,255,255,0.10), transparent 30%),
radial-gradient(circle at 70% 70%, rgba(255,255,255,0.06), transparent 35%);
mix-blend-mode: screen;
}
@keyframes heroZoom {
from { transform: scale(1.06); }
to { transform: scale(1.16); }
}
.hero__overlay {
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(12,10,8,0.35) 0%, rgba(12,10,8,0) 30%, rgba(12,10,8,0.55) 100%);
}
.hero__content {
position: relative;
max-width: var(--max);
width: 100%;
margin: 0 auto;
padding: 0 24px 88px;
}
.hero__eyebrow {
text-transform: uppercase;
letter-spacing: 0.32em;
font-size: 0.72rem;
margin: 0 0 18px;
color: rgba(255,255,255,0.78);
}
.hero__title {
font-size: clamp(2.8rem, 8vw, 6rem);
color: #fff;
margin-bottom: 22px;
}
.hero__lede {
max-width: 40ch;
font-size: clamp(1rem, 2.2vw, 1.18rem);
color: rgba(255,255,255,0.85);
font-weight: 300;
margin: 0 0 30px;
}
.hero__actions { display: flex; gap: 14px; flex-wrap: wrap; }
.hero__scroll {
position: absolute;
right: 24px; bottom: 34px;
width: 26px; height: 44px;
border: 1px solid rgba(255,255,255,0.5);
border-radius: 14px;
}
.hero__scroll span {
position: absolute;
left: 50%; top: 9px;
width: 3px; height: 8px;
margin-left: -1.5px;
background: #fff;
border-radius: 2px;
animation: scrollDot 1.6s var(--ease) infinite;
}
@keyframes scrollDot {
0% { opacity: 0; transform: translateY(0); }
40% { opacity: 1; }
100% { opacity: 0; transform: translateY(16px); }
}
/* ---------- SECTIONS ---------- */
.section {
max-width: var(--max);
margin: 0 auto;
padding: 110px 24px;
}
.section__head { margin-bottom: 48px; max-width: 30ch; }
.kicker {
text-transform: uppercase;
letter-spacing: 0.26em;
font-size: 0.72rem;
color: var(--ink-faint);
margin: 0 0 14px;
}
.section__head h2 { font-size: clamp(1.9rem, 4.5vw, 3rem); }
/* ---------- FILTERS ---------- */
.filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 36px;
}
.filter {
border: 1px solid var(--line);
background: none;
border-radius: 100px;
padding: 9px 20px;
font-size: 0.86rem;
color: var(--ink-soft);
cursor: pointer;
transition: all 0.25s;
}
.filter:hover { border-color: var(--ink); color: var(--ink); }
.filter.is-active { background: var(--ink); color: #fff; border-color: var(--ink); }
/* ---------- GALLERY ---------- */
.gallery {
column-count: 3;
column-gap: 18px;
}
.shot {
margin: 0 0 18px;
break-inside: avoid;
position: relative;
cursor: pointer;
overflow: hidden;
border-radius: 4px;
transition: opacity 0.45s, transform 0.45s;
}
.shot.is-hidden { display: none; }
.shot__img {
width: 100%;
background-size: cover;
transition: transform 0.7s var(--ease);
}
.shot[data-tone="1"] .shot__img { aspect-ratio: 3/4; background: linear-gradient(160deg, var(--t1), #8e8377); }
.shot[data-tone="2"] .shot__img { aspect-ratio: 4/3; background: linear-gradient(160deg, var(--t2), #6f7a76); }
.shot[data-tone="3"] .shot__img { aspect-ratio: 1/1; background: linear-gradient(160deg, var(--t3), #847b8c); }
.shot[data-tone="4"] .shot__img { aspect-ratio: 4/5; background: linear-gradient(160deg, var(--t4), #8a8175); }
.shot[data-tone="5"] .shot__img { aspect-ratio: 4/3; background: linear-gradient(160deg, var(--t5), #8c8279); }
.shot[data-tone="6"] .shot__img { aspect-ratio: 3/4; background: linear-gradient(160deg, var(--t6), #707a82); }
.shot[data-tone="7"] .shot__img { aspect-ratio: 1/1; background: linear-gradient(160deg, var(--t7), #7d756f); }
.shot[data-tone="8"] .shot__img { aspect-ratio: 4/5; background: linear-gradient(160deg, var(--t8), #847b72); }
.shot:hover .shot__img { transform: scale(1.05); }
.shot figcaption {
position: absolute;
inset: auto 0 0 0;
padding: 18px 16px 14px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
color: #fff;
background: linear-gradient(180deg, transparent, rgba(10,9,7,0.7));
opacity: 0;
transform: translateY(8px);
transition: opacity 0.35s, transform 0.35s;
}
.shot:hover figcaption,
.shot:focus-visible figcaption { opacity: 1; transform: translateY(0); }
.shot figcaption span { font-family: "Fraunces", serif; font-size: 1.05rem; }
.shot figcaption em {
font-style: normal;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 0.66rem;
opacity: 0.8;
}
/* ---------- SERVICES ---------- */
.services { background: var(--paper-warm); max-width: none; }
.services > .section__head,
.services > .cards { max-width: var(--max); margin-left: auto; margin-right: auto; }
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 22px;
padding: 0 24px;
}
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: 6px;
padding: 34px 30px;
transition: transform 0.4s var(--ease), box-shadow 0.4s;
}
.card:hover { transform: translateY(-6px); box-shadow: var(--shadow); }
.card__no {
font-family: "Fraunces", serif;
font-size: 0.9rem;
color: var(--ink-faint);
}
.card h3 { font-size: 1.6rem; margin: 12px 0 14px; }
.card p { color: var(--ink-soft); font-size: 0.95rem; margin: 0 0 14px; }
.card__price {
font-size: 0.82rem !important;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink) !important;
margin: 0 !important;
}
/* ---------- ABOUT ---------- */
.about__grid {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: 56px;
align-items: center;
}
.about__portrait {
aspect-ratio: 4/5;
border-radius: 6px;
background: linear-gradient(150deg, #cabfb3, #6f655a);
position: relative;
overflow: hidden;
}
.about__portrait::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at 35% 30%, rgba(255,255,255,0.22), transparent 40%);
}
.about__text h2 { font-size: clamp(1.7rem, 3.4vw, 2.5rem); margin: 14px 0 22px; }
.about__text p { color: var(--ink-soft); margin: 0 0 16px; max-width: 52ch; }
.stats {
list-style: none;
display: flex;
gap: 36px;
padding: 26px 0 0;
margin: 14px 0 0;
border-top: 1px solid var(--line);
}
.stats strong {
display: block;
font-family: "Fraunces", serif;
font-weight: 400;
font-size: 2.1rem;
}
.stats span { font-size: 0.8rem; color: var(--ink-faint); }
/* ---------- CLIENTS ---------- */
.clients { background: var(--ink); color: #fff; max-width: none; }
.clients > .section__head,
.clients > .logos,
.clients > .quotes { max-width: var(--max); margin-left: auto; margin-right: auto; }
.clients .kicker { color: rgba(255,255,255,0.55); }
.clients h2 { color: #fff; }
.logos {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 18px 46px;
padding: 0 24px;
margin: 0 0 64px;
font-family: "Fraunces", serif;
font-size: 1.25rem;
color: rgba(255,255,255,0.55);
}
.logos li { transition: color 0.3s; }
.logos li:hover { color: #fff; }
.quotes {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 26px;
padding: 0 24px;
}
.quote {
margin: 0;
padding: 28px 26px;
border: 1px solid rgba(255,255,255,0.16);
border-radius: 6px;
}
.quote p {
font-family: "Fraunces", serif;
font-weight: 300;
font-size: 1.12rem;
line-height: 1.4;
margin: 0 0 18px;
}
.quote footer { font-size: 0.84rem; color: rgba(255,255,255,0.6); }
/* ---------- CONTACT ---------- */
.contact__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 56px;
align-items: start;
}
.contact__intro h2 { font-size: clamp(1.8rem, 3.6vw, 2.6rem); margin: 14px 0 18px; }
.contact__intro p { color: var(--ink-soft); max-width: 40ch; }
.contact__direct a { border-bottom: 1px solid var(--ink); }
.booking { display: grid; gap: 16px; }
.field { display: grid; gap: 7px; }
.field label {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--ink-faint);
}
.field input,
.field select,
.field textarea {
font: inherit;
padding: 13px 14px;
border: 1px solid var(--line);
border-radius: 5px;
background: var(--paper);
color: var(--ink);
transition: border-color 0.25s;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
outline: none;
border-color: var(--ink);
}
.field textarea { resize: vertical; }
.field input.invalid { border-color: #c0392b; }
/* ---------- FOOTER ---------- */
.footer {
border-top: 1px solid var(--line);
padding: 48px 24px 36px;
}
.footer__inner {
max-width: var(--max);
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 22px;
flex-wrap: wrap;
}
.footer__brand { display: flex; align-items: center; gap: 12px; }
.footer__brand p { margin: 0; color: var(--ink-soft); font-size: 0.92rem; }
.footer__social { display: flex; gap: 24px; font-size: 0.9rem; color: var(--ink-soft); }
.footer__social a:hover { color: var(--ink); }
.footer__fine {
max-width: var(--max);
margin: 26px auto 0;
font-size: 0.78rem;
color: var(--ink-faint);
}
/* ---------- LIGHTBOX ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 300;
display: flex;
align-items: center;
justify-content: center;
background: rgba(14, 12, 10, 0.94);
padding: 5vh 5vw;
opacity: 0;
transition: opacity 0.3s;
}
.lightbox[hidden] { display: none; }
.lightbox.is-open { opacity: 1; }
.lightbox__stage { margin: 0; max-width: 760px; width: 100%; }
.lightbox__img {
width: 100%;
aspect-ratio: 4/3;
border-radius: 5px;
background-size: cover;
background-position: center;
}
.lightbox__stage figcaption {
color: rgba(255,255,255,0.8);
text-align: center;
margin-top: 16px;
font-family: "Fraunces", serif;
font-size: 1.1rem;
}
.lightbox__close,
.lightbox__nav {
position: absolute;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.25);
color: #fff;
cursor: pointer;
border-radius: 50%;
transition: background 0.25s;
}
.lightbox__close { top: 22px; right: 22px; width: 46px; height: 46px; font-size: 1.5rem; }
.lightbox__nav { top: 50%; transform: translateY(-50%); width: 52px; height: 52px; font-size: 1.8rem; }
.lightbox__prev { left: 18px; }
.lightbox__next { right: 18px; }
.lightbox__close:hover,
.lightbox__nav:hover { background: rgba(255,255,255,0.25); }
/* ---------- TOAST ---------- */
.toast {
position: fixed;
left: 50%; bottom: 28px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
padding: 13px 22px;
border-radius: 100px;
font-size: 0.9rem;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
z-index: 400;
}
.toast.is-show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- REVEAL ---------- */
.reveal {
opacity: 0;
transform: translateY(26px);
transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}
.reveal.is-in { opacity: 1; transform: none; }
/* ---------- RESPONSIVE ---------- */
@media (max-width: 940px) {
.gallery { column-count: 2; }
.cards, .quotes { grid-template-columns: repeat(2, 1fr); }
.about__grid, .contact__grid { grid-template-columns: 1fr; gap: 38px; }
.about__portrait { max-width: 420px; }
}
@media (max-width: 720px) {
.nav__links { display: none; }
.nav__toggle { display: flex; }
.section { padding: 78px 22px; }
.cards, .quotes { grid-template-columns: 1fr; }
.hero__content { padding-bottom: 64px; }
}
@media (max-width: 520px) {
.gallery { column-count: 1; }
.hero { min-height: 92vh; }
.stats { flex-wrap: wrap; gap: 22px 34px; }
.footer__inner { flex-direction: column; align-items: flex-start; }
.lightbox__nav { width: 44px; height: 44px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; }
.reveal { opacity: 1; transform: none; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2800);
}
/* ---------- Sticky nav shadow ---------- */
var nav = document.querySelector(".nav");
function onScroll() {
if (!nav) return;
nav.classList.toggle("is-scrolled", window.scrollY > 12);
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
/* ---------- Mobile menu ---------- */
var toggle = document.getElementById("navToggle");
var menu = document.getElementById("mobileMenu");
function setMenu(open) {
if (!toggle || !menu) return;
toggle.setAttribute("aria-expanded", String(open));
toggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
menu.hidden = !open;
}
if (toggle) {
toggle.addEventListener("click", function () {
setMenu(toggle.getAttribute("aria-expanded") !== "true");
});
}
if (menu) {
menu.querySelectorAll("a").forEach(function (a) {
a.addEventListener("click", function () { setMenu(false); });
});
}
/* ---------- Smooth scroll (with reduced-motion respect) ---------- */
var prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll('a[href^="#"]').forEach(function (link) {
link.addEventListener("click", function (e) {
var id = link.getAttribute("href");
if (id.length < 2) return;
var target = document.querySelector(id);
if (!target) return;
e.preventDefault();
target.scrollIntoView({ behavior: prefersReduced ? "auto" : "smooth", block: "start" });
});
});
/* ---------- Scroll reveal ---------- */
var revealEls = document.querySelectorAll(".reveal");
if ("IntersectionObserver" in window && !prefersReduced) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("is-in");
io.unobserve(entry.target);
}
});
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
revealEls.forEach(function (el) { io.observe(el); });
} else {
revealEls.forEach(function (el) { el.classList.add("is-in"); });
}
/* ---------- Gallery filter ---------- */
var filters = document.querySelectorAll(".filter");
var shots = Array.prototype.slice.call(document.querySelectorAll(".shot"));
filters.forEach(function (btn) {
btn.addEventListener("click", function () {
filters.forEach(function (f) {
f.classList.remove("is-active");
f.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
var cat = btn.getAttribute("data-filter");
shots.forEach(function (shot) {
var show = cat === "all" || shot.getAttribute("data-cat") === cat;
shot.classList.toggle("is-hidden", !show);
});
});
});
/* ---------- Lightbox ---------- */
var lb = document.getElementById("lightbox");
var lbImg = document.getElementById("lbImg");
var lbCaption = document.getElementById("lbCaption");
var lbClose = document.getElementById("lbClose");
var lbPrev = document.getElementById("lbPrev");
var lbNext = document.getElementById("lbNext");
var current = 0;
var lastFocused = null;
function shotData(shot) {
var img = shot.querySelector(".shot__img");
var cap = shot.querySelector("figcaption");
return {
bg: img ? getComputedStyle(img).backgroundImage : "",
title: cap ? cap.querySelector("span").textContent : "",
cat: cap ? cap.querySelector("em").textContent : ""
};
}
function visibleShots() {
return shots.filter(function (s) { return !s.classList.contains("is-hidden"); });
}
function renderLightbox(index) {
var list = visibleShots();
if (!list.length) return;
current = (index + list.length) % list.length;
var d = shotData(list[current]);
lbImg.style.backgroundImage = d.bg;
lbCaption.textContent = d.title + " — " + d.cat;
}
function openLightbox(shot) {
var list = visibleShots();
var idx = list.indexOf(shot);
if (idx < 0) idx = 0;
lastFocused = document.activeElement;
lb.hidden = false;
requestAnimationFrame(function () { lb.classList.add("is-open"); });
document.body.style.overflow = "hidden";
renderLightbox(idx);
lbClose.focus();
}
function closeLightbox() {
lb.classList.remove("is-open");
document.body.style.overflow = "";
setTimeout(function () { lb.hidden = true; }, 280);
if (lastFocused) lastFocused.focus();
}
shots.forEach(function (shot) {
shot.addEventListener("click", function () { openLightbox(shot); });
shot.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openLightbox(shot);
}
});
});
if (lbClose) lbClose.addEventListener("click", closeLightbox);
if (lbPrev) lbPrev.addEventListener("click", function () { renderLightbox(current - 1); });
if (lbNext) lbNext.addEventListener("click", function () { renderLightbox(current + 1); });
if (lb) {
lb.addEventListener("click", function (e) {
if (e.target === lb) closeLightbox();
});
}
document.addEventListener("keydown", function (e) {
if (lb.hidden) return;
if (e.key === "Escape") closeLightbox();
if (e.key === "ArrowLeft") renderLightbox(current - 1);
if (e.key === "ArrowRight") renderLightbox(current + 1);
});
/* ---------- Booking form ---------- */
var form = document.getElementById("bookingForm");
if (form) {
form.addEventListener("submit", function (e) {
e.preventDefault();
var name = form.querySelector("#bk-name");
var email = form.querySelector("#bk-email");
var ok = true;
[name, email].forEach(function (input) {
var valid = input.value.trim() !== "" && (input.type !== "email" || /.+@.+\..+/.test(input.value));
input.classList.toggle("invalid", !valid);
if (!valid) ok = false;
});
if (!ok) {
toast("Please add your name and a valid email.");
return;
}
var who = name.value.trim().split(" ")[0];
form.reset();
toast("Thanks, " + who + " — I'll reply within two days.");
});
}
/* ---------- Year (footer is static, but keep CTA links friendly) ---------- */
document.querySelectorAll('.footer__social a, .contact__direct a').forEach(function (a) {
a.addEventListener("click", function (e) {
e.preventDefault();
toast("Demo link — fictional creator.");
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Maren Holt — Photographer</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,wght@9..144,300;9..144,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#work">Skip to content</a>
<!-- NAV -->
<header class="nav" id="top">
<div class="nav__inner">
<a class="brand" href="#top" aria-label="Maren Holt, home">
<span class="brand__mark" aria-hidden="true">◐</span>
<span class="brand__name">Maren Holt</span>
</a>
<nav class="nav__links" aria-label="Primary">
<a href="#work">Work</a>
<a href="#services">Services</a>
<a href="#about">About</a>
<a href="#clients">Clients</a>
<a class="nav__cta" href="#contact">Book a shoot</a>
</nav>
<button class="nav__toggle" id="navToggle" aria-expanded="false" aria-controls="mobileMenu" aria-label="Open menu">
<span></span><span></span><span></span>
</button>
</div>
<div class="nav__mobile" id="mobileMenu" hidden>
<a href="#work">Work</a>
<a href="#services">Services</a>
<a href="#about">About</a>
<a href="#clients">Clients</a>
<a href="#contact">Book a shoot</a>
</div>
</header>
<main>
<!-- HERO -->
<section class="hero" aria-label="Introduction">
<div class="hero__media" aria-hidden="true"></div>
<div class="hero__overlay"></div>
<div class="hero__content">
<p class="hero__eyebrow reveal">Portrait · Editorial · Documentary</p>
<h1 class="hero__title reveal">Light, held<br />a moment longer.</h1>
<p class="hero__lede reveal">I’m Maren Holt, a photographer based in Lisbon working with people, brands and the quiet spaces in between. Available worldwide.</p>
<div class="hero__actions reveal">
<a class="btn btn--solid" href="#contact">Book a shoot</a>
<a class="btn btn--ghost" href="#work">View portfolio</a>
</div>
</div>
<a class="hero__scroll" href="#work" aria-label="Scroll to work">
<span></span>
</a>
</section>
<!-- WORK / GALLERY -->
<section class="section work" id="work">
<div class="section__head reveal">
<p class="kicker">Selected Work</p>
<h2>A frame for every story</h2>
</div>
<div class="filters reveal" role="tablist" aria-label="Filter portfolio">
<button class="filter is-active" data-filter="all" role="tab" aria-selected="true">All</button>
<button class="filter" data-filter="portrait" role="tab" aria-selected="false">Portrait</button>
<button class="filter" data-filter="event" role="tab" aria-selected="false">Event</button>
<button class="filter" data-filter="commercial" role="tab" aria-selected="false">Commercial</button>
</div>
<div class="gallery" id="gallery">
<figure class="shot reveal" data-cat="portrait" data-tone="1" tabindex="0" role="button" aria-label="Open photo: Aurelia, studio portrait">
<div class="shot__img"></div>
<figcaption><span>Aurelia</span><em>Portrait</em></figcaption>
</figure>
<figure class="shot reveal" data-cat="event" data-tone="2" tabindex="0" role="button" aria-label="Open photo: The long table, Douro">
<div class="shot__img"></div>
<figcaption><span>The Long Table</span><em>Event</em></figcaption>
</figure>
<figure class="shot reveal" data-cat="commercial" data-tone="3" tabindex="0" role="button" aria-label="Open photo: Ceramics for Atelier Mira">
<div class="shot__img"></div>
<figcaption><span>Atelier Mira</span><em>Commercial</em></figcaption>
</figure>
<figure class="shot reveal" data-cat="portrait" data-tone="4" tabindex="0" role="button" aria-label="Open photo: Window light, Theo">
<div class="shot__img"></div>
<figcaption><span>Theo, by the window</span><em>Portrait</em></figcaption>
</figure>
<figure class="shot reveal" data-cat="commercial" data-tone="5" tabindex="0" role="button" aria-label="Open photo: Linen campaign for Vela">
<div class="shot__img"></div>
<figcaption><span>Vela Linen</span><em>Commercial</em></figcaption>
</figure>
<figure class="shot reveal" data-cat="event" data-tone="6" tabindex="0" role="button" aria-label="Open photo: First dance, Sintra">
<div class="shot__img"></div>
<figcaption><span>Sintra, first dance</span><em>Event</em></figcaption>
</figure>
<figure class="shot reveal" data-cat="portrait" data-tone="7" tabindex="0" role="button" aria-label="Open photo: Studio series, Noor">
<div class="shot__img"></div>
<figcaption><span>Noor — Series</span><em>Portrait</em></figcaption>
</figure>
<figure class="shot reveal" data-cat="commercial" data-tone="8" tabindex="0" role="button" aria-label="Open photo: Coffee bar, Alma">
<div class="shot__img"></div>
<figcaption><span>Alma Coffee</span><em>Commercial</em></figcaption>
</figure>
</div>
</section>
<!-- SERVICES -->
<section class="section services" id="services">
<div class="section__head reveal">
<p class="kicker">Services</p>
<h2>How we can work together</h2>
</div>
<div class="cards">
<article class="card reveal">
<span class="card__no">01</span>
<h3>Portrait</h3>
<p>Studio or on-location sessions for individuals, families and personal brands. Calm direction, natural light, fully retouched gallery.</p>
<p class="card__price">from €320 · half-day</p>
</article>
<article class="card reveal">
<span class="card__no">02</span>
<h3>Event</h3>
<p>Weddings, launches and intimate gatherings documented as they unfold — unobtrusive, story-first coverage with fast turnaround.</p>
<p class="card__price">from €1,150 · full-day</p>
</article>
<article class="card reveal">
<span class="card__no">03</span>
<h3>Commercial</h3>
<p>Product, food and brand campaigns built with art direction. Mood boards, shot lists and licensing handled end to end.</p>
<p class="card__price">custom · day rate</p>
</article>
</div>
</section>
<!-- ABOUT -->
<section class="section about" id="about">
<div class="about__grid">
<div class="about__portrait reveal" aria-hidden="true"></div>
<div class="about__text reveal">
<p class="kicker">About</p>
<h2>Photographs that feel like the room they were taken in.</h2>
<p>For twelve years I’ve chased the kind of light you only notice when you slow down. My work lives between editorial precision and documentary honesty — nothing staged into stiffness, nothing left to luck.</p>
<p>I shoot on a mix of medium-format film and digital, and I edit every frame by hand. Whether it’s a founder’s headshot or a three-day wedding, the brief is the same: make it true, make it last.</p>
<ul class="stats">
<li><strong>12</strong><span>years shooting</span></li>
<li><strong>240+</strong><span>commissions</span></li>
<li><strong>17</strong><span>countries</span></li>
</ul>
</div>
</div>
</section>
<!-- CLIENTS / TESTIMONIALS -->
<section class="section clients" id="clients">
<div class="section__head reveal">
<p class="kicker">Trusted by</p>
<h2>Selected clients & press</h2>
</div>
<ul class="logos reveal" aria-label="Client logos">
<li>VELA</li>
<li>Atelier Mira</li>
<li>Alma</li>
<li>NORTE Mag</li>
<li>Casa Luz</li>
<li>Field & Form</li>
</ul>
<div class="quotes">
<blockquote class="quote reveal">
<p>“Maren turned a chaotic launch day into a set of images we still use everywhere. She sees the shot before it happens.”</p>
<footer>— Inês Carvalho, Brand Lead, VELA</footer>
</blockquote>
<blockquote class="quote reveal">
<p>“Our wedding photos don’t look posed — they look like the day actually felt. That’s the whole magic.”</p>
<footer>— Daniel & Rui, Sintra</footer>
</blockquote>
<blockquote class="quote reveal">
<p>“The commercial set arrived ahead of schedule and on brief to the pixel. A genuine professional.”</p>
<footer>— Sofia Reis, Editor, NORTE Mag</footer>
</blockquote>
</div>
</section>
<!-- CONTACT / BOOKING -->
<section class="section contact" id="contact">
<div class="contact__grid">
<div class="contact__intro reveal">
<p class="kicker">Booking</p>
<h2>Let’s plan your shoot</h2>
<p>Tell me a little about the project and I’ll reply within two working days with availability and a tailored quote.</p>
<p class="contact__direct">Prefer email? <a href="#contact">hello@marenholt.studio</a></p>
</div>
<form class="booking" id="bookingForm" novalidate>
<div class="field">
<label for="bk-name">Name</label>
<input id="bk-name" name="name" type="text" autocomplete="name" required />
</div>
<div class="field">
<label for="bk-email">Email</label>
<input id="bk-email" name="email" type="email" autocomplete="email" required />
</div>
<div class="field">
<label for="bk-type">Shoot type</label>
<select id="bk-type" name="type">
<option>Portrait</option>
<option>Event</option>
<option>Commercial</option>
<option>Not sure yet</option>
</select>
</div>
<div class="field">
<label for="bk-msg">Tell me about it</label>
<textarea id="bk-msg" name="message" rows="4"></textarea>
</div>
<button class="btn btn--solid btn--full" type="submit">Send enquiry</button>
</form>
</div>
</section>
</main>
<!-- FOOTER -->
<footer class="footer">
<div class="footer__inner">
<div class="footer__brand">
<span class="brand__mark" aria-hidden="true">◐</span>
<p>Maren Holt — Photographer, Lisbon & worldwide.</p>
</div>
<nav class="footer__social" aria-label="Social links">
<a href="#contact">Instagram</a>
<a href="#contact">Behance</a>
<a href="#contact">Newsletter</a>
</nav>
</div>
<p class="footer__fine">© 2026 Maren Holt Studio. Illustrative demo — fictional creator.</p>
</footer>
<!-- LIGHTBOX -->
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Photo viewer" hidden>
<button class="lightbox__close" id="lbClose" aria-label="Close viewer">×</button>
<button class="lightbox__nav lightbox__prev" id="lbPrev" aria-label="Previous photo">‹</button>
<figure class="lightbox__stage">
<div class="lightbox__img" id="lbImg"></div>
<figcaption id="lbCaption"></figcaption>
</figure>
<button class="lightbox__nav lightbox__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>Photographer Landing
A refined, single-page personal brand site for Maren Holt, a fictional Lisbon-based photographer. The layout opens with a fixed, translucent navigation bar and a full-bleed cinematic hero — a slow-zooming gradient stand-in for a signature image — carrying the name, tagline and primary booking call to action. From there the page flows through a masonry portfolio, services, an about story, client trust signals, a booking form and footer, all set in a quiet ink-on-white palette with Fraunces display type over an Inter sans body.
The portfolio is the centrepiece: a CSS-column masonry grid of varied aspect ratios with category filters (Portrait, Event, Commercial) and a fully keyboard-accessible lightbox supporting arrow-key navigation, Escape to close, focus return and backdrop dismissal. Services are presented as numbered cards with day-rate pricing, the about block pairs a portrait with shooting stats, and the clients section gathers logos and testimonials on an inverted dark panel.
Every interaction is hand-written vanilla JavaScript: a mobile nav toggle, smooth in-page scrolling, an IntersectionObserver scroll-reveal, the filter-aware lightbox, and a booking form that validates name and email before firing a friendly toast. Motion is disabled under prefers-reduced-motion, contrast targets WCAG AA, and the layout reflows cleanly from wide desktop down to a single-column 360px view.
Illustrative UI only — fictional creator, not a real person or brand.