Photography — Landing Portrait
A gallery-dark landing page for a portrait photographer built around a full-bleed hero portrait, a studio-versus-on-location toggle that swaps imagery and copy in place, and a pricing teaser with three session tiers. Thin uppercase labels, a Fraunces display serif, animated stat counters, a lightbox-style portfolio grid, and a booking form with inline validation and toast feedback keep the experience cinematic yet genuinely interactive from desktop 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;
--ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
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;
overflow-x: hidden;
}
img { max-width: 100%; display: block; }
.eyebrow {
font-size: 0.72rem;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--sand);
font-weight: 600;
margin: 0 0 1rem;
}
.section-title {
font-family: "Fraunces", Georgia, serif;
font-weight: 400;
font-size: clamp(2rem, 5vw, 3.2rem);
line-height: 1.05;
letter-spacing: -0.01em;
margin: 0 0 0.75rem;
}
.section-lead {
color: var(--muted);
max-width: 46ch;
margin: 0;
}
.section-head { max-width: 640px; margin: 0 auto 2.75rem; text-align: center; }
.section-head .section-lead { margin-inline: auto; }
/* Buttons */
.btn {
--pad: 0.85rem 1.6rem;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: var(--pad);
border-radius: 999px;
font: inherit;
font-weight: 600;
font-size: 0.92rem;
letter-spacing: 0.01em;
text-decoration: none;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.25s var(--ease), background 0.25s var(--ease),
border-color 0.25s var(--ease), color 0.25s var(--ease);
}
.btn:active { transform: translateY(1px) scale(0.99); }
.btn:focus-visible { outline: 2px solid var(--sand); outline-offset: 3px; }
.btn-solid { background: var(--paper); color: var(--ink); }
.btn-solid:hover { background: var(--sand); transform: translateY(-2px); }
.btn-line { background: transparent; color: var(--paper); border-color: var(--line-2); }
.btn-line:hover { border-color: var(--sand); color: var(--sand); transform: translateY(-2px); }
.btn-ghost { background: transparent; color: var(--paper); border-color: var(--line); }
.btn-ghost:hover { border-color: var(--sand); color: var(--sand); }
/* Topbar */
.topbar {
position: sticky;
top: 0;
z-index: 40;
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1rem clamp(1.1rem, 4vw, 3rem);
background: rgba(16, 16, 18, 0.72);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 0.6rem; text-decoration: none; color: var(--paper); }
.brand-mark {
display: grid;
place-items: center;
width: 34px; height: 34px;
border-radius: 50%;
background: var(--sand);
color: var(--ink);
font-family: "Fraunces", serif;
font-weight: 600;
font-size: 0.9rem;
}
.brand-name { font-weight: 700; letter-spacing: 0.01em; }
.nav { margin-left: auto; display: flex; gap: 1.6rem; }
.nav a {
color: var(--muted);
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
position: relative;
transition: color 0.2s var(--ease);
}
.nav a::after {
content: "";
position: absolute; left: 0; bottom: -6px;
width: 0; height: 1px; background: var(--sand);
transition: width 0.25s var(--ease);
}
.nav a:hover { color: var(--paper); }
.nav a:hover::after { width: 100%; }
.nav-cta { padding: 0.55rem 1.15rem; }
/* Hero */
.hero {
position: relative;
min-height: 92vh;
display: flex;
align-items: flex-end;
padding: clamp(2rem, 6vw, 5rem) clamp(1.1rem, 4vw, 3rem) clamp(3rem, 7vw, 5rem);
overflow: hidden;
}
.hero-media {
position: absolute; inset: 0;
background:
radial-gradient(120% 90% at 78% 12%, rgba(201, 183, 156, 0.35), transparent 55%),
linear-gradient(180deg, rgba(11, 11, 12, 0.15), rgba(11, 11, 12, 0.9)),
url("https://images.unsplash.com/photo-1524504388940-b1c1722653e1?auto=format&fit=crop&w=1600&q=80")
center 22% / cover no-repeat,
var(--surface);
transform: scale(1.05);
animation: heroZoom 18s ease-in-out infinite alternate;
}
@keyframes heroZoom { to { transform: scale(1.14); } }
.hero-grain {
position: absolute; inset: 0;
background-image: radial-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 3px 3px;
mix-blend-mode: overlay;
pointer-events: none;
}
.hero-inner { position: relative; z-index: 2; max-width: 720px; }
.hero-title {
font-family: "Fraunces", Georgia, serif;
font-weight: 400;
font-size: clamp(2.8rem, 9vw, 6rem);
line-height: 0.98;
letter-spacing: -0.02em;
margin: 0 0 1.2rem;
}
.hero-title em { font-style: italic; color: var(--sand); }
.hero-sub { color: #d7d5cf; max-width: 44ch; font-size: 1.05rem; margin: 0 0 2rem; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-bottom: 2.6rem; }
.hero-stats {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: clamp(1.4rem, 5vw, 3.2rem);
margin: 0; padding: 1.6rem 0 0;
border-top: 1px solid var(--line);
}
.hero-stats li { display: flex; flex-direction: column; gap: 0.25rem; }
.stat-num {
font-family: "Fraunces", serif;
font-size: clamp(1.7rem, 4vw, 2.4rem);
font-weight: 600;
color: var(--paper);
line-height: 1;
}
.stat-label {
font-size: 0.72rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.hero-scroll {
position: absolute; z-index: 2;
right: clamp(1.1rem, 4vw, 3rem); bottom: 2.2rem;
width: 26px; height: 44px;
border: 1px solid var(--line-2);
border-radius: 999px;
display: grid; place-items: start center;
}
.hero-scroll span {
width: 3px; height: 9px; margin-top: 7px;
border-radius: 3px; background: var(--sand);
animation: scrollDot 1.6s var(--ease) infinite;
}
@keyframes scrollDot { 0%,100% { opacity: 0.3; transform: translateY(0); } 50% { opacity: 1; transform: translateY(9px); } }
/* Sections shell */
section.experience,
section.work,
section.pricing { padding: clamp(3.5rem, 8vw, 7rem) clamp(1.1rem, 4vw, 3rem); }
/* Reveal */
.reveal { opacity: 0; transform: translateY(28px); transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); }
.reveal.is-in { opacity: 1; transform: none; }
/* Experience */
.seg {
position: relative;
display: inline-flex;
margin: 0 auto 2.4rem;
padding: 5px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface);
left: 50%; transform: translateX(-50%);
}
.seg-btn {
position: relative; z-index: 2;
border: 0; background: transparent;
color: var(--muted);
font: inherit; font-weight: 600; font-size: 0.85rem;
letter-spacing: 0.1em; text-transform: uppercase;
padding: 0.6rem 1.5rem;
border-radius: 999px;
cursor: pointer;
transition: color 0.25s var(--ease);
}
.seg-btn.is-active { color: var(--ink); }
.seg-btn:focus-visible { outline: 2px solid var(--sand); outline-offset: 2px; }
.seg-thumb {
position: absolute; z-index: 1; top: 5px; left: 5px;
height: calc(100% - 10px); width: 50%;
border-radius: 999px;
background: var(--sand);
transition: transform 0.35s var(--ease);
}
.seg.at-location .seg-thumb { transform: translateX(100%); }
.exp-panel {
display: grid;
grid-template-columns: 1.05fr 0.95fr;
gap: clamp(1.5rem, 4vw, 3.5rem);
align-items: center;
max-width: 1080px;
margin: 0 auto;
}
.exp-media {
margin: 0;
aspect-ratio: 4 / 5;
border-radius: var(--r-lg);
border: 1px solid var(--line);
background-size: cover;
background-position: center;
transition: opacity 0.4s var(--ease), transform 0.5s var(--ease);
}
.exp-panel.swapping .exp-media,
.exp-panel.swapping .exp-copy { opacity: 0; transform: translateY(10px); }
.exp-copy { transition: opacity 0.4s var(--ease), transform 0.5s var(--ease); }
.exp-kicker {
font-size: 0.72rem; letter-spacing: 0.2em; text-transform: uppercase;
color: var(--sand-d); font-weight: 600; margin: 0 0 0.7rem;
}
.exp-heading { font-family: "Fraunces", serif; font-weight: 400; font-size: clamp(1.7rem, 3.6vw, 2.4rem); margin: 0 0 0.8rem; }
.exp-text { color: var(--muted); margin: 0 0 1.4rem; max-width: 46ch; }
.exp-chips { list-style: none; display: flex; flex-wrap: wrap; gap: 0.55rem; padding: 0; margin: 0 0 1.4rem; }
.exp-chips li {
font-size: 0.8rem; padding: 0.4rem 0.85rem;
border: 1px solid var(--line-2); border-radius: 999px;
color: #d7d5cf; background: var(--surface-2);
}
.exp-meta { font-size: 0.78rem; letter-spacing: 0.08em; color: var(--muted); text-transform: uppercase; margin: 0; }
/* Portfolio grid */
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 210px;
gap: 14px;
max-width: 1120px;
margin: 0 auto;
}
.tile {
position: relative;
border: 0; padding: 0; margin: 0;
border-radius: var(--r-md);
overflow: hidden;
cursor: pointer;
background: var(--g), var(--surface-2);
background-size: cover;
color: var(--paper);
text-align: left;
isolation: isolate;
}
.tile::before {
content: ""; position: absolute; inset: 0;
background: linear-gradient(180deg, transparent 40%, rgba(11, 11, 12, 0.78));
z-index: 1; opacity: 0.65; transition: opacity 0.3s var(--ease);
}
.tile::after {
content: ""; position: absolute; inset: 0; z-index: 2;
background: rgba(201, 183, 156, 0);
transition: background 0.3s var(--ease);
}
.tile:hover::before { opacity: 0.9; }
.tile:hover::after { background: rgba(201, 183, 156, 0.08); }
.tile:hover { transform: translateY(-3px); }
.tile { transition: transform 0.3s var(--ease); }
.tile:focus-visible { outline: 2px solid var(--sand); outline-offset: 3px; }
.tile-cap {
position: absolute; z-index: 3; left: 14px; right: 14px; bottom: 12px;
font-size: 0.82rem; color: #ece9e2;
transform: translateY(6px); opacity: 0;
transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
}
.tile-cap b { display: block; font-family: "Fraunces", serif; font-size: 1.05rem; font-weight: 600; color: var(--white); }
.tile:hover .tile-cap, .tile:focus-visible .tile-cap { transform: none; opacity: 1; }
.t1 { grid-column: span 2; grid-row: span 2; }
.t2 { grid-column: span 1; grid-row: span 1; }
.t3 { grid-column: span 1; grid-row: span 2; }
.t4 { grid-column: span 1; grid-row: span 1; }
.t5 { grid-column: span 2; grid-row: span 1; }
.t6 { grid-column: span 1; grid-row: span 1; }
/* Pricing */
.tiers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.2rem;
max-width: 1040px;
margin: 0 auto;
align-items: stretch;
}
.tier {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 2rem 1.7rem;
display: flex;
flex-direction: column;
transition: transform 0.3s var(--ease), border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.tier:hover { transform: translateY(-6px); border-color: var(--line-2); }
.tier-featured {
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border-color: var(--sand-d);
}
.tier-flag {
position: absolute; top: -12px; left: 50%; transform: translateX(-50%);
background: var(--sand); color: var(--ink);
font-size: 0.68rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
padding: 0.35rem 0.85rem; border-radius: 999px;
}
.tier-name { font-size: 0.8rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--sand); font-weight: 600; margin: 0 0 0.6rem; }
.tier-price { font-family: "Fraunces", serif; font-size: 2.8rem; font-weight: 600; margin: 0; line-height: 1; }
.tier-price .cur { font-size: 1.4rem; vertical-align: super; color: var(--muted); margin-right: 2px; }
.tier-sub { color: var(--muted); font-size: 0.9rem; margin: 0.4rem 0 1.4rem; }
.tier-list { list-style: none; padding: 0; margin: 0 0 1.6rem; display: grid; gap: 0.7rem; }
.tier-list li { position: relative; padding-left: 1.4rem; font-size: 0.92rem; color: #d7d5cf; }
.tier-list li::before {
content: ""; position: absolute; left: 0; top: 0.5em;
width: 8px; height: 8px; border-radius: 50%;
background: transparent; border: 1.5px solid var(--sand);
}
.tier-cta { margin-top: auto; width: 100%; }
/* Booking */
.book {
display: grid;
grid-template-columns: 1fr 1.05fr;
min-height: 620px;
}
.book-media {
background:
linear-gradient(180deg, rgba(11, 11, 12, 0.25), rgba(11, 11, 12, 0.55)),
url("https://images.unsplash.com/photo-1554048612-b6a482bc67e5?auto=format&fit=crop&w=1200&q=80")
center / cover no-repeat,
var(--surface-2);
}
.book-panel { padding: clamp(2.4rem, 6vw, 5rem) clamp(1.4rem, 5vw, 4rem); background: var(--surface); align-self: stretch; }
.book-panel .section-head { text-align: left; margin: 0 0 1.8rem; }
.book-panel .section-title { font-size: clamp(1.9rem, 4vw, 2.6rem); }
.book-panel .section-lead { margin: 0; }
.form { margin-top: 1.6rem; display: grid; grid-template-columns: 1fr 1fr; gap: 1.1rem; }
.field { display: flex; flex-direction: column; gap: 0.4rem; }
.field-full { grid-column: 1 / -1; }
.field label { font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
.field input, .field select, .field textarea {
font: inherit; color: var(--paper);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 0.75rem 0.85rem;
transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
resize: vertical;
}
.field input::placeholder, .field textarea::placeholder { color: #6b6b73; }
.field input:focus, .field select:focus, .field textarea:focus {
outline: none;
border-color: var(--sand);
box-shadow: 0 0 0 3px rgba(201, 183, 156, 0.18);
}
.field.invalid input, .field.invalid select { border-color: #e0705f; box-shadow: 0 0 0 3px rgba(224, 112, 95, 0.16); }
.err { font-size: 0.75rem; color: #e88b7d; min-height: 0.9em; }
.form-submit { grid-column: 1 / -1; margin-top: 0.4rem; }
/* Footer */
.footer {
display: flex; flex-wrap: wrap; align-items: center; gap: 1rem;
padding: 2.4rem clamp(1.1rem, 4vw, 3rem);
border-top: 1px solid var(--line);
background: var(--ink);
}
.footer .brand { margin-right: auto; }
.footer-note { color: var(--muted); font-size: 0.82rem; margin: 0; flex: 1 1 240px; }
.footer-social { display: flex; gap: 1.2rem; }
.footer-social a { color: var(--muted); text-decoration: none; font-size: 0.85rem; transition: color 0.2s var(--ease); }
.footer-social a:hover { color: var(--sand); }
/* Lightbox */
.lightbox {
position: fixed; inset: 0; z-index: 90;
display: grid; place-items: center;
background: rgba(7, 7, 8, 0.86);
backdrop-filter: blur(6px);
opacity: 0; visibility: hidden;
transition: opacity 0.3s var(--ease), visibility 0.3s var(--ease);
padding: 1.4rem;
}
.lightbox.open { opacity: 1; visibility: visible; }
.lb-frame { margin: 0; max-width: 620px; width: 100%; }
.lb-img {
aspect-ratio: 4 / 5; width: 100%;
border-radius: var(--r-md);
background-size: cover; background-position: center;
border: 1px solid var(--line-2);
transform: scale(0.96); transition: transform 0.3s var(--ease);
}
.lightbox.open .lb-img { transform: scale(1); }
.lb-cap { margin-top: 1rem; text-align: center; font-family: "Fraunces", serif; font-size: 1.15rem; color: var(--paper); }
.lb-close {
position: absolute; top: 1.2rem; right: 1.4rem;
width: 44px; height: 44px; border-radius: 50%;
border: 1px solid var(--line-2); background: var(--surface);
color: var(--paper); font-size: 1.6rem; line-height: 1; cursor: pointer;
transition: background 0.2s var(--ease), transform 0.2s var(--ease);
}
.lb-close:hover { background: var(--sand); color: var(--ink); transform: rotate(90deg); }
.lb-close:focus-visible { outline: 2px solid var(--sand); outline-offset: 3px; }
/* Toast */
.toast-wrap {
position: fixed; z-index: 100; left: 50%; bottom: 1.6rem;
transform: translateX(-50%);
display: flex; flex-direction: column; gap: 0.6rem; align-items: center;
pointer-events: none;
}
.toast {
background: var(--paper); color: var(--ink);
padding: 0.8rem 1.3rem; border-radius: 999px;
font-size: 0.9rem; font-weight: 600;
box-shadow: 0 12px 34px rgba(0, 0, 0, 0.4);
display: flex; align-items: center; gap: 0.55rem;
transform: translateY(20px); opacity: 0;
transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
}
.toast.show { transform: none; opacity: 1; }
.toast .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--sand-d); }
/* Responsive */
@media (max-width: 900px) {
.nav { display: none; }
.exp-panel { grid-template-columns: 1fr; }
.grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 190px; }
.t1, .t3, .t5 { grid-column: span 2; grid-row: span 1; }
.tiers { grid-template-columns: 1fr; max-width: 460px; }
.book { grid-template-columns: 1fr; }
.book-media { min-height: 260px; }
}
@media (max-width: 520px) {
.topbar { padding: 0.8rem 1.1rem; }
.brand-name { display: none; }
.hero { min-height: 88vh; align-items: flex-end; }
.hero-actions { width: 100%; }
.hero-actions .btn { flex: 1 1 auto; }
.hero-stats { gap: 1.2rem 1.6rem; }
.hero-scroll { display: none; }
.grid { grid-template-columns: 1fr; grid-auto-rows: 220px; }
.t1, .t2, .t3, .t4, .t5, .t6 { grid-column: span 1; grid-row: span 1; }
.tile-cap { opacity: 1; transform: none; }
.form { grid-template-columns: 1fr; }
.footer { flex-direction: column; align-items: flex-start; }
.lb-close { top: 0.8rem; right: 0.9rem; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; scroll-behavior: auto; }
.reveal { opacity: 1; transform: none; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastWrap = document.getElementById("toast-wrap");
function toast(msg) {
if (!toastWrap) return;
var el = document.createElement("div");
el.className = "toast";
el.innerHTML = '<span class="dot" aria-hidden="true"></span><span></span>';
el.querySelector("span:last-child").textContent = msg;
toastWrap.appendChild(el);
requestAnimationFrame(function () { el.classList.add("show"); });
setTimeout(function () {
el.classList.remove("show");
setTimeout(function () { el.remove(); }, 320);
}, 2600);
}
/* ---------- Reveal on scroll ---------- */
var revealEls = document.querySelectorAll(".reveal");
if ("IntersectionObserver" in window) {
var revObs = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { e.target.classList.add("is-in"); revObs.unobserve(e.target); }
});
}, { threshold: 0.14 });
revealEls.forEach(function (el) { revObs.observe(el); });
} else {
revealEls.forEach(function (el) { el.classList.add("is-in"); });
}
/* ---------- Animated stat counters ---------- */
var counted = false;
var statsEl = document.getElementById("stats");
function runCounters() {
if (counted) return;
counted = true;
document.querySelectorAll(".stat-num").forEach(function (node) {
var target = parseInt(node.getAttribute("data-count"), 10) || 0;
var suffix = node.getAttribute("data-suffix") || "";
var start = null, dur = 1400;
function step(ts) {
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
node.textContent = Math.round(target * eased).toLocaleString("en-US") + suffix;
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
});
}
if (statsEl && "IntersectionObserver" in window) {
var statObs = new IntersectionObserver(function (entries) {
entries.forEach(function (e) { if (e.isIntersecting) runCounters(); });
}, { threshold: 0.4 });
statObs.observe(statsEl);
} else {
runCounters();
}
/* ---------- Studio vs On-location toggle ---------- */
var MODES = {
studio: {
kicker: "Controlled & timeless",
heading: "The studio room",
text: "A calm, blacked-out space with shaped light and seamless backdrops. Ideal for headshots, beauty, and clean editorial frames where every shadow is a decision.",
chips: ["Shaped strobe & softbox", "Wardrobe rail on-site", "Tethered live review", "Seamless + textured sets"],
meta: "90-minute block · Berlin Mitte studio",
img: "https://images.unsplash.com/photo-1554080353-a576cf803bda?auto=format&fit=crop&w=900&q=80"
},
location: {
kicker: "Alive & atmospheric",
heading: "Out in the world",
text: "Golden-hour rooftops, quiet courtyards, tram-lit streets. We chase real light and let the city set the mood — relaxed, cinematic, unmistakably yours.",
chips: ["Scouted locations", "Golden-hour timing", "Available-light craft", "Movement direction"],
meta: "2-hour window · location of your choice",
img: "https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&w=900&q=80"
}
};
var seg = document.querySelector(".seg");
var segBtns = document.querySelectorAll(".seg-btn");
var panel = document.getElementById("exp-panel");
var mediaEl = panel && panel.querySelector("[data-media]");
var kickerEl = panel && panel.querySelector("[data-kicker]");
var headingEl = panel && panel.querySelector("[data-heading]");
var textEl = panel && panel.querySelector("[data-text]");
var chipsEl = panel && panel.querySelector("[data-chips]");
var metaEl = panel && panel.querySelector("[data-meta]");
function preload(url) { var i = new Image(); i.src = url; }
preload(MODES.studio.img); preload(MODES.location.img);
function applyMode(mode, animate) {
var data = MODES[mode];
if (!data || !panel) return;
function paint() {
mediaEl.style.backgroundImage = "url(" + data.img + ")";
kickerEl.textContent = data.kicker;
headingEl.textContent = data.heading;
textEl.textContent = data.text;
metaEl.textContent = data.meta;
chipsEl.innerHTML = "";
data.chips.forEach(function (c) {
var li = document.createElement("li");
li.textContent = c;
chipsEl.appendChild(li);
});
}
if (animate) {
panel.classList.add("swapping");
setTimeout(function () { paint(); panel.classList.remove("swapping"); }, 260);
} else {
paint();
}
}
segBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
var mode = btn.getAttribute("data-mode");
segBtns.forEach(function (b) {
var on = b === btn;
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", on ? "true" : "false");
});
seg.classList.toggle("at-location", mode === "location");
applyMode(mode, true);
});
});
applyMode("studio", false);
/* ---------- Portfolio lightbox ---------- */
var lightbox = document.getElementById("lightbox");
var lbImg = document.getElementById("lb-img");
var lbCap = document.getElementById("lb-cap");
var lbClose = document.getElementById("lb-close");
var lastFocused = null;
function openLightbox(tile) {
lastFocused = tile;
var bg = getComputedStyle(tile).backgroundImage;
lbImg.style.backgroundImage = bg;
lbCap.textContent = tile.getAttribute("data-title") || "";
lightbox.classList.add("open");
lightbox.setAttribute("aria-hidden", "false");
lbClose.focus();
document.body.style.overflow = "hidden";
}
function closeLightbox() {
lightbox.classList.remove("open");
lightbox.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
if (lastFocused) lastFocused.focus();
}
document.querySelectorAll(".tile").forEach(function (tile) {
tile.addEventListener("click", function () { openLightbox(tile); });
});
lbClose.addEventListener("click", closeLightbox);
lightbox.addEventListener("click", function (e) { if (e.target === lightbox) closeLightbox(); });
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && lightbox.classList.contains("open")) closeLightbox();
});
/* ---------- Pricing → prefill booking ---------- */
var planSelect = document.getElementById("f-plan");
document.querySelectorAll(".tier-cta").forEach(function (btn) {
btn.addEventListener("click", function () {
var plan = btn.getAttribute("data-plan");
if (planSelect) planSelect.value = plan;
toast(plan + " selected — finish your enquiry");
var book = document.getElementById("book");
if (book) book.scrollIntoView({ behavior: "smooth", block: "start" });
setTimeout(function () {
var name = document.getElementById("f-name");
if (name) name.focus();
}, 600);
});
});
/* ---------- Booking form validation ---------- */
var form = document.getElementById("book-form");
function setError(name, msg) {
var field = form.querySelector('[name="' + name + '"]').closest(".field");
var slot = form.querySelector('[data-err="' + name + '"]');
if (msg) {
field.classList.add("invalid");
if (slot) slot.textContent = msg;
} else {
field.classList.remove("invalid");
if (slot) slot.textContent = "";
}
}
function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
if (form) {
form.addEventListener("submit", function (e) {
e.preventDefault();
var name = form.name.value.trim();
var email = form.email.value.trim();
var plan = form.plan.value;
var ok = true;
if (name.length < 2) { setError("name", "Please tell me your name."); ok = false; }
else setError("name", "");
if (!validEmail(email)) { setError("email", "Enter a valid email address."); ok = false; }
else setError("email", "");
if (!plan) { setError("plan", "Pick a session (or “Not sure yet”)."); ok = false; }
else setError("plan", "");
if (!ok) { toast("Please check the highlighted fields"); return; }
var btn = form.querySelector(".form-submit");
btn.disabled = true;
var original = btn.textContent;
btn.textContent = "Sending…";
setTimeout(function () {
btn.disabled = false;
btn.textContent = original;
form.reset();
toast("Thanks " + name.split(" ")[0] + " — enquiry received!");
}, 900);
});
["name", "email", "plan"].forEach(function (n) {
form[n].addEventListener("input", function () { setError(n, ""); });
});
}
/* ---------- Year (footer already static, harmless enhancement) ---------- */
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Marceline Veir — Portrait 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: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>
<!-- Top bar -->
<header class="topbar">
<a class="brand" href="#top">
<span class="brand-mark" aria-hidden="true">MV</span>
<span class="brand-name">Marceline Veir</span>
</a>
<nav class="nav" aria-label="Primary">
<a href="#work">Work</a>
<a href="#experience">Experience</a>
<a href="#pricing">Pricing</a>
<a href="#book">Book</a>
</nav>
<a class="btn btn-ghost nav-cta" href="#book">Enquire</a>
</header>
<main id="top">
<!-- Hero -->
<section class="hero" id="hero">
<div class="hero-media" role="img" aria-label="Studio portrait of a subject in warm light"></div>
<div class="hero-grain" aria-hidden="true"></div>
<div class="hero-inner">
<p class="eyebrow">Portrait · Editorial · Berlin</p>
<h1 class="hero-title">Portraits that<br /><em>hold a gaze.</em></h1>
<p class="hero-sub">
Available-light and studio portraiture for people who would rather feel
seen than posed. Booking select commissions for autumn 2026.
</p>
<div class="hero-actions">
<a class="btn btn-solid" href="#book">Book a session</a>
<a class="btn btn-line" href="#work">View portfolio</a>
</div>
<ul class="hero-stats" id="stats" aria-label="Studio at a glance">
<li>
<span class="stat-num" data-count="1240" data-suffix="+">0</span>
<span class="stat-label">Portraits delivered</span>
</li>
<li>
<span class="stat-num" data-count="11" data-suffix="">0</span>
<span class="stat-label">Years behind the lens</span>
</li>
<li>
<span class="stat-num" data-count="72" data-suffix="h">0</span>
<span class="stat-label">Avg. gallery turnaround</span>
</li>
</ul>
</div>
<a class="hero-scroll" href="#experience" aria-label="Scroll to experiences">
<span></span>
</a>
</section>
<!-- Studio vs On-location -->
<section class="experience reveal" id="experience">
<div class="section-head">
<p class="eyebrow">The experience</p>
<h2 class="section-title">Studio or the world outside</h2>
<p class="section-lead">
Two ways to make a portrait. Pick the light that fits your story —
both come with full direction and a hand-edited gallery.
</p>
</div>
<div class="seg" role="tablist" aria-label="Choose a shooting style">
<button class="seg-btn is-active" role="tab" aria-selected="true" data-mode="studio" id="tab-studio">Studio</button>
<button class="seg-btn" role="tab" aria-selected="false" data-mode="location" id="tab-location">On-location</button>
<span class="seg-thumb" aria-hidden="true"></span>
</div>
<div class="exp-panel" id="exp-panel">
<figure class="exp-media" data-media aria-hidden="false"></figure>
<div class="exp-copy">
<p class="exp-kicker" data-kicker>Controlled & timeless</p>
<h3 class="exp-heading" data-heading>The studio room</h3>
<p class="exp-text" data-text>
A calm, blacked-out space with shaped light and seamless backdrops.
Ideal for headshots, beauty, and clean editorial frames where every
shadow is a decision.
</p>
<ul class="exp-chips" data-chips>
<li>Shaped strobe & softbox</li>
<li>Wardrobe rail on-site</li>
<li>Tethered live review</li>
<li>Seamless + textured sets</li>
</ul>
<p class="exp-meta"><span data-meta>90-minute block · Berlin Mitte studio</span></p>
</div>
</div>
</section>
<!-- Portfolio -->
<section class="work reveal" id="work">
<div class="section-head">
<p class="eyebrow">Selected work</p>
<h2 class="section-title">A few recent faces</h2>
</div>
<div class="grid" id="grid" role="list">
<button class="tile t1" role="listitem" data-title="Ivo — Golden hour rooftop" style="--g:linear-gradient(160deg,#3a2a20,#c9b79c)">
<span class="tile-cap"><b>Ivo</b> Golden hour rooftop</span>
</button>
<button class="tile t2" role="listitem" data-title="Naima — Studio beauty" style="--g:linear-gradient(160deg,#1e1e23,#5a4a3a)">
<span class="tile-cap"><b>Naima</b> Studio beauty</span>
</button>
<button class="tile t3" role="listitem" data-title="The Halden family" style="--g:linear-gradient(160deg,#22303a,#a8916f)">
<span class="tile-cap"><b>The Haldens</b> Family, forest light</span>
</button>
<button class="tile t4" role="listitem" data-title="Søren — Editorial profile" style="--g:linear-gradient(160deg,#2a1e2a,#9a7a6a)">
<span class="tile-cap"><b>Søren</b> Editorial profile</span>
</button>
<button class="tile t5" role="listitem" data-title="Priya — Window light" style="--g:linear-gradient(160deg,#20242e,#c9b79c)">
<span class="tile-cap"><b>Priya</b> Window light</span>
</button>
<button class="tile t6" role="listitem" data-title="Malik — Contrast headshot" style="--g:linear-gradient(160deg,#141416,#7a6a5a)">
<span class="tile-cap"><b>Malik</b> Contrast headshot</span>
</button>
</div>
</section>
<!-- Pricing teaser -->
<section class="pricing reveal" id="pricing">
<div class="section-head">
<p class="eyebrow">Sessions</p>
<h2 class="section-title">Straightforward pricing</h2>
<p class="section-lead">
Deposits secure your date. Final galleries arrive within three days —
retouching and print rights included.
</p>
</div>
<div class="tiers">
<article class="tier">
<p class="tier-name">Portrait Mini</p>
<p class="tier-price"><span class="cur">€</span>180</p>
<p class="tier-sub">30 minutes · one look</p>
<ul class="tier-list">
<li>15 edited frames</li>
<li>Studio or single location</li>
<li>Online gallery, 6 months</li>
<li>Personal print licence</li>
</ul>
<button class="btn btn-line tier-cta" data-plan="Portrait Mini">Reserve Mini</button>
</article>
<article class="tier tier-featured">
<span class="tier-flag">Most booked</span>
<p class="tier-name">Signature Session</p>
<p class="tier-price"><span class="cur">€</span>360</p>
<p class="tier-sub">90 minutes · two looks</p>
<ul class="tier-list">
<li>40 edited frames</li>
<li>Studio <em>and</em> on-location</li>
<li>Wardrobe & direction guide</li>
<li>Commercial print licence</li>
</ul>
<button class="btn btn-solid tier-cta" data-plan="Signature Session">Reserve Signature</button>
</article>
<article class="tier">
<p class="tier-name">Editorial Day</p>
<p class="tier-price"><span class="cur">€</span>940</p>
<p class="tier-sub">Half day · unlimited looks</p>
<ul class="tier-list">
<li>90+ edited frames</li>
<li>Multi-location, full crew</li>
<li>Hair & make-up partner</li>
<li>Extended usage rights</li>
</ul>
<button class="btn btn-line tier-cta" data-plan="Editorial Day">Reserve Editorial</button>
</article>
</div>
</section>
<!-- Booking -->
<section class="book reveal" id="book">
<div class="book-media" role="img" aria-label="Behind the scenes of a portrait shoot"></div>
<div class="book-panel">
<p class="eyebrow">Enquire</p>
<h2 class="section-title">Let's plan your portrait</h2>
<p class="section-lead">Tell me a little about the shoot. I reply within one working day.</p>
<form class="form" id="book-form" novalidate>
<div class="field">
<label for="f-name">Name</label>
<input id="f-name" name="name" type="text" autocomplete="name" placeholder="Your full name" />
<span class="err" data-err="name"></span>
</div>
<div class="field">
<label for="f-email">Email</label>
<input id="f-email" name="email" type="email" autocomplete="email" placeholder="[email protected]" />
<span class="err" data-err="email"></span>
</div>
<div class="field">
<label for="f-plan">Session</label>
<select id="f-plan" name="plan">
<option value="">Choose a session…</option>
<option>Portrait Mini</option>
<option>Signature Session</option>
<option>Editorial Day</option>
<option>Not sure yet</option>
</select>
<span class="err" data-err="plan"></span>
</div>
<div class="field field-full">
<label for="f-msg">Details</label>
<textarea id="f-msg" name="message" rows="3" placeholder="Dates in mind, mood, location…"></textarea>
</div>
<button class="btn btn-solid form-submit" type="submit">Send enquiry</button>
</form>
</div>
</section>
</main>
<footer class="footer">
<div class="brand">
<span class="brand-mark" aria-hidden="true">MV</span>
<span class="brand-name">Marceline Veir Studio</span>
</div>
<p class="footer-note">Portrait & editorial photography · Berlin · © 2026 — fictional demo</p>
<div class="footer-social" aria-label="Social links">
<a href="#top">Instagram</a><a href="#top">Behance</a><a href="#top">Journal</a>
</div>
</footer>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Portfolio preview">
<button class="lb-close" id="lb-close" aria-label="Close preview">×</button>
<figure class="lb-frame">
<div class="lb-img" id="lb-img"></div>
<figcaption class="lb-cap" id="lb-cap"></figcaption>
</figure>
</div>
<div class="toast-wrap" id="toast-wrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>A dark, image-first landing page for Marceline Vей Studio, a fictional portrait photographer. The hero pairs a full-bleed portrait with an oversized Fraunces headline, thin uppercase eyebrow labels, and a booking CTA that scrolls to the enquiry form. Below it, a segmented Studio vs On-location toggle swaps the featured image, description, and feature chips in place with a soft crossfade so visitors can compare the two shooting experiences without leaving the section.
The portfolio is a masonry-style grid of gradient-and-photo tiles that open in a minimal lightbox with keyboard and overlay dismissal. A pricing teaser lays out three session tiers — Portrait Mini, Signature Session, and Editorial Day — with a highlighted recommended card, hover lift, and a “reserve” button that pre-fills the booking form. Animated stat counters (shoots delivered, years behind the lens, average turnaround) tick up when scrolled into view.
Everything is vanilla: an IntersectionObserver drives the counters and reveal animations, the toggle and lightbox are plain event handlers, and the booking form runs inline validation with a small toast() helper for confirmation. The layout is fully responsive with a dedicated @media (max-width:520px) pass, visible focus rings, and aria attributes on the interactive controls.