Photography — Landing Wedding
A gallery-dark wedding photographer landing with a romantic full-bleed hero, an interactive filterable image grid with lightbox, a three-tier packages teaser, a rotating client testimonial, and a validated inquiry form that captures date, venue and coverage — all wired with vanilla JS micro-interactions, keyboard-navigable controls, and a toast confirmation.
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;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
.eyebrow {
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--sand);
margin: 0 0 14px;
font-weight: 600;
}
.eyebrow.center { text-align: center; }
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-family: var(--sans);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 13px 22px;
border-radius: 999px;
border: 1px solid transparent;
cursor: pointer;
transition: transform 0.18s ease, background 0.18s ease, border-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
text-transform: none;
}
.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(--white); transform: translateY(-2px); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5); }
.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: rgba(255, 255, 255, 0.06); color: var(--paper); border-color: var(--line); padding: 9px 16px; }
.btn-ghost:hover { background: rgba(255, 255, 255, 0.12); border-color: var(--sand); }
.btn-block { width: 100%; }
/* ---------- Header ---------- */
.site-head {
position: sticky;
top: 0;
z-index: 40;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 16px clamp(18px, 5vw, 56px);
background: rgba(16, 16, 18, 0.72);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border: 1px solid var(--sand);
border-radius: 50%;
font-size: 12px;
font-weight: 700;
color: var(--sand);
letter-spacing: 0.02em;
}
.brand-name { font-family: var(--serif); font-size: 20px; letter-spacing: 0.01em; }
.brand-name em { font-style: italic; color: var(--sand); }
.nav { display: flex; gap: 26px; }
.nav a {
font-size: 13px;
color: var(--muted);
position: relative;
transition: color 0.18s ease;
}
.nav a::after {
content: "";
position: absolute;
left: 0; bottom: -6px;
width: 0; height: 1px;
background: var(--sand);
transition: width 0.22s ease;
}
.nav a:hover { color: var(--paper); }
.nav a:hover::after { width: 100%; }
/* ---------- Hero ---------- */
.hero {
position: relative;
min-height: 92vh;
display: flex;
align-items: flex-end;
padding: clamp(40px, 8vw, 120px) clamp(18px, 5vw, 56px) 72px;
overflow: hidden;
}
.hero-media { position: absolute; inset: 0; z-index: 0; }
.hero-media img {
width: 100%; height: 118%;
object-fit: cover;
filter: saturate(0.92) brightness(0.82);
will-change: transform;
}
.hero-veil {
position: absolute; inset: 0;
background:
linear-gradient(180deg, rgba(11, 11, 12, 0.35) 0%, rgba(11, 11, 12, 0.15) 40%, rgba(11, 11, 12, 0.92) 100%),
radial-gradient(120% 80% at 15% 90%, rgba(11, 11, 12, 0.7), transparent 60%);
}
.hero-inner { position: relative; z-index: 2; max-width: 720px; }
.hero-title {
font-family: var(--serif);
font-weight: 400;
font-size: clamp(44px, 8.5vw, 108px);
line-height: 0.98;
letter-spacing: -0.01em;
margin: 0 0 22px;
}
.hero-title em { font-style: italic; color: var(--sand); }
.hero-lede {
max-width: 480px;
color: rgba(246, 244, 239, 0.86);
font-size: clamp(15px, 2vw, 18px);
margin: 0 0 30px;
}
.hero-actions { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 44px; }
.hero-stats {
display: flex;
gap: clamp(28px, 6vw, 64px);
list-style: none;
margin: 0; padding: 0;
}
.hero-stats li { display: flex; flex-direction: column; }
.hero-stats strong {
font-family: var(--serif);
font-size: clamp(28px, 4vw, 42px);
font-weight: 600;
line-height: 1;
}
.hero-stats span {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--muted);
margin-top: 8px;
}
.scroll-cue {
position: absolute;
right: clamp(18px, 5vw, 56px);
bottom: 40px;
z-index: 2;
display: flex;
align-items: center;
gap: 10px;
font-size: 10px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--muted);
}
.scroll-cue span {
width: 1px; height: 40px;
background: linear-gradient(var(--sand), transparent);
animation: cue 1.8s ease-in-out infinite;
}
@keyframes cue { 0%, 100% { transform: scaleY(0.4); opacity: 0.4; } 50% { transform: scaleY(1); opacity: 1; } }
/* ---------- Section heads ---------- */
.sec-head { max-width: 720px; margin: 0 auto clamp(30px, 5vw, 52px); text-align: center; }
.sec-title {
font-family: var(--serif);
font-weight: 400;
font-size: clamp(28px, 5vw, 52px);
line-height: 1.04;
margin: 0;
letter-spacing: -0.01em;
}
.sec-sub { color: var(--muted); margin: 16px auto 0; max-width: 520px; font-size: 15px; }
/* ---------- Gallery ---------- */
.gallery { padding: clamp(64px, 10vw, 128px) clamp(18px, 5vw, 56px); }
.filters { display: inline-flex; flex-wrap: wrap; gap: 8px; margin-top: 24px; }
.chip {
font-family: var(--sans);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
background: var(--surface);
border: 1px solid var(--line);
padding: 9px 16px;
border-radius: 999px;
cursor: pointer;
transition: color 0.18s ease, border-color 0.18s ease, background 0.18s ease;
}
.chip:hover { color: var(--paper); border-color: var(--line-2); }
.chip.is-active { color: var(--ink); background: var(--sand); border-color: var(--sand); }
.chip:focus-visible { outline: 2px solid var(--sand); outline-offset: 2px; }
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 220px;
gap: 14px;
max-width: 1200px;
margin: 0 auto;
}
.frame {
position: relative;
overflow: hidden;
border-radius: var(--r-md);
cursor: pointer;
background: var(--surface-2);
border: 1px solid var(--line);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.frame.tall { grid-row: span 2; }
.frame.wide { grid-column: span 2; }
.frame img {
width: 100%; height: 100%;
object-fit: cover;
transition: transform 0.6s cubic-bezier(0.2, 0.8, 0.2, 1), filter 0.4s ease;
filter: saturate(0.9) brightness(0.9);
}
.frame:hover img { transform: scale(1.06); filter: saturate(1) brightness(1); }
.frame::after {
content: attr(data-cap);
position: absolute;
left: 14px; bottom: 12px;
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--paper);
opacity: 0;
transform: translateY(6px);
transition: opacity 0.3s ease, transform 0.3s ease;
text-shadow: 0 1px 12px rgba(0, 0, 0, 0.8);
}
.frame:hover::after { opacity: 1; transform: translateY(0); }
.frame:focus-visible { outline: 2px solid var(--sand); outline-offset: 3px; }
.frame.is-hidden { display: none; }
/* ---------- Packages ---------- */
.packages { padding: clamp(64px, 10vw, 128px) clamp(18px, 5vw, 56px); background: var(--surface); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.pkgs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
max-width: 1040px;
margin: 0 auto;
align-items: stretch;
}
.pkg {
position: relative;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 30px 26px;
display: flex;
flex-direction: column;
transition: transform 0.22s ease, border-color 0.22s ease, box-shadow 0.22s ease;
}
.pkg:hover { transform: translateY(-6px); border-color: var(--line-2); box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45); }
.pkg.is-featured {
border-color: var(--sand);
background: linear-gradient(180deg, rgba(201, 183, 156, 0.10), rgba(30, 30, 35, 1) 60%);
}
.pkg-badge {
position: absolute;
top: -12px; left: 26px;
background: var(--sand);
color: var(--ink);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
padding: 6px 12px;
border-radius: 999px;
}
.pkg-name {
font-family: var(--serif);
font-size: 22px;
margin: 0 0 6px;
}
.pkg-price {
font-family: var(--serif);
font-size: 42px;
font-weight: 600;
margin: 0 0 18px;
letter-spacing: -0.02em;
}
.pkg-price span { font-size: 20px; color: var(--sand); vertical-align: super; margin-right: 2px; }
.pkg-list { list-style: none; margin: 0 0 24px; padding: 0; flex: 1; }
.pkg-list li {
padding: 9px 0 9px 22px;
position: relative;
color: rgba(246, 244, 239, 0.86);
font-size: 14px;
border-bottom: 1px solid var(--line);
}
.pkg-list li::before { content: "✶"; position: absolute; left: 0; color: var(--sand); font-size: 11px; }
.pkg-btn { width: 100%; }
/* ---------- Testimonials ---------- */
.words {
padding: clamp(72px, 12vw, 148px) clamp(18px, 5vw, 56px);
background:
radial-gradient(80% 60% at 50% 0%, rgba(201, 183, 156, 0.06), transparent 70%),
var(--bg);
}
.words-inner { max-width: 760px; margin: 0 auto; text-align: center; }
.quotes { position: relative; min-height: 200px; }
.quote {
position: absolute;
inset: 0;
opacity: 0;
transform: translateY(14px);
transition: opacity 0.5s ease, transform 0.5s ease;
pointer-events: none;
}
.quote.is-active { opacity: 1; transform: translateY(0); position: relative; pointer-events: auto; }
.quote blockquote {
font-family: var(--serif);
font-size: clamp(20px, 3.4vw, 30px);
line-height: 1.35;
margin: 0 0 24px;
font-style: italic;
}
.quote-by { display: flex; align-items: center; justify-content: center; gap: 12px; }
.quote-by img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; border: 1px solid var(--sand); }
.quote-by cite {
font-style: normal;
font-size: 12px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.quote-by cite strong { color: var(--paper); font-weight: 600; }
.dots { display: flex; gap: 9px; justify-content: center; margin-top: 34px; }
.dot {
width: 8px; height: 8px;
border-radius: 50%;
border: none;
background: var(--line-2);
cursor: pointer;
padding: 0;
transition: background 0.2s ease, transform 0.2s ease;
}
.dot:hover { transform: scale(1.3); }
.dot.is-active { background: var(--sand); width: 22px; border-radius: 999px; }
.dot:focus-visible { outline: 2px solid var(--sand); outline-offset: 3px; }
/* ---------- Inquire ---------- */
.inquire { padding: clamp(64px, 10vw, 128px) clamp(18px, 5vw, 56px); background: var(--surface); border-top: 1px solid var(--line); }
.inquire-grid {
display: grid;
grid-template-columns: 0.9fr 1.1fr;
gap: clamp(28px, 5vw, 64px);
max-width: 1080px;
margin: 0 auto;
align-items: start;
}
.inquire-copy .sec-title { text-align: left; }
.assure { list-style: none; margin: 26px 0 0; padding: 0; }
.assure li {
display: flex; align-items: center; gap: 10px;
padding: 10px 0;
color: rgba(246, 244, 239, 0.86);
font-size: 14px;
border-bottom: 1px solid var(--line);
}
.assure span { color: var(--sand); }
.inquire-form {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: clamp(22px, 4vw, 34px);
}
.field { margin-bottom: 16px; }
.field label {
display: block;
font-size: 11px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 8px;
}
.field label .opt { text-transform: none; letter-spacing: 0; opacity: 0.7; }
.field input {
width: 100%;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
color: var(--paper);
font-family: var(--sans);
font-size: 15px;
padding: 13px 14px;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.field input::placeholder { color: rgba(154, 154, 162, 0.6); }
.field input:focus {
outline: none;
border-color: var(--sand);
box-shadow: 0 0 0 3px rgba(201, 183, 156, 0.16);
}
.field input.invalid { border-color: #e07a6b; box-shadow: 0 0 0 3px rgba(224, 122, 107, 0.14); }
.err { margin: 7px 0 0; font-size: 12px; color: #e79a8e; min-height: 0; transition: min-height 0.2s ease; }
.err:not(:empty) { min-height: 16px; }
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.form-note { margin: 14px 0 0; font-size: 11.5px; color: var(--muted); text-align: center; }
/* ---------- Footer ---------- */
.site-foot {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 48px clamp(18px, 5vw, 56px);
border-top: 1px solid var(--line);
}
.foot-brand .brand-name { font-size: 24px; }
.foot-brand p { margin: 8px 0 0; color: var(--muted); font-size: 13px; }
.foot-links { display: flex; flex-wrap: wrap; gap: 22px; }
.foot-links a { font-size: 13px; color: var(--muted); transition: color 0.18s ease; }
.foot-links a:hover { color: var(--sand); }
.foot-fine { width: 100%; margin: 0; color: rgba(154, 154, 162, 0.6); font-size: 11.5px; }
/* ---------- Lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
background: rgba(8, 8, 9, 0.92);
backdrop-filter: blur(6px);
animation: fade 0.25s ease;
}
.lightbox[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.lb-figure { margin: 0; max-width: 88vw; max-height: 84vh; text-align: center; }
.lb-figure img {
max-width: 100%;
max-height: 76vh;
border-radius: var(--r-md);
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
}
.lb-figure figcaption {
margin-top: 14px;
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
.lb-close, .lb-nav {
position: absolute;
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--line-2);
color: var(--paper);
cursor: pointer;
border-radius: 50%;
transition: background 0.18s ease, transform 0.18s ease, border-color 0.18s ease;
}
.lb-close { top: 22px; right: 22px; width: 44px; height: 44px; font-size: 16px; }
.lb-nav { top: 50%; transform: translateY(-50%); width: 52px; height: 52px; font-size: 26px; line-height: 1; }
.lb-prev { left: 22px; }
.lb-next { right: 22px; }
.lb-close:hover, .lb-nav:hover { background: var(--sand); color: var(--ink); border-color: var(--sand); }
.lb-nav:hover { transform: translateY(-50%) scale(1.06); }
.lb-close:focus-visible, .lb-nav:focus-visible { outline: 2px solid var(--sand); outline-offset: 3px; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 140%);
z-index: 120;
background: var(--paper);
color: var(--ink);
font-size: 14px;
font-weight: 500;
padding: 13px 22px;
border-radius: 999px;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
transition: transform 0.4s cubic-bezier(0.2, 0.9, 0.2, 1);
max-width: min(90vw, 420px);
}
.toast.show { transform: translate(-50%, 0); }
/* ---------- Reveal on scroll ---------- */
.reveal { opacity: 0; transform: translateY(28px); transition: opacity 0.7s ease, transform 0.7s ease; }
.reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; scroll-behavior: auto; }
.reveal { opacity: 1; transform: none; }
}
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.nav { display: none; }
.grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 180px; }
.pkgs { grid-template-columns: 1fr; max-width: 440px; }
.inquire-grid { grid-template-columns: 1fr; }
.inquire-copy .sec-title { text-align: left; }
}
@media (max-width: 520px) {
.head-cta { display: none; }
.hero { min-height: 88vh; padding-bottom: 48px; }
.hero-stats { gap: 26px; }
.scroll-cue { display: none; }
.grid { grid-template-columns: 1fr; grid-auto-rows: 240px; }
.frame.wide { grid-column: span 1; }
.frame.tall { grid-row: span 1; }
.row { grid-template-columns: 1fr; }
.lb-nav { width: 44px; height: 44px; font-size: 22px; }
.lb-prev { left: 10px; }
.lb-next { right: 10px; }
.lb-close { top: 14px; right: 14px; }
.site-foot { flex-direction: column; align-items: flex-start; }
}(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");
}, 3400);
}
/* ---------- Gallery data ---------- */
var PHOTOS = [
{ id: "1519741497674-611481863552", cat: "ceremony", cap: "The first look", cls: "wide" },
{ id: "1465495976277-4387d4b0b4c6", cat: "detail", cap: "Rings & vows", cls: "" },
{ id: "1511285560929-80b456fea0bc", cat: "portrait", cap: "Golden hour", cls: "tall" },
{ id: "1520854221256-17451cc331bf", cat: "ceremony", cap: "The aisle", cls: "" },
{ id: "1519225421980-715cb0215aed", cat: "portrait", cap: "Just married", cls: "" },
{ id: "1522673607200-164d1b6ce486", cat: "detail", cap: "Bouquet", cls: "" },
{ id: "1583939003579-730e3918a45a", cat: "portrait", cap: "Quiet moment", cls: "wide" },
{ id: "1606216794074-735e91aa2c92", cat: "detail", cap: "The dress", cls: "tall" },
{ id: "1519657337289-077653f724ed", cat: "ceremony", cap: "Confetti", cls: "" },
{ id: "1537633552985-df8429e8048b", cat: "portrait", cap: "Reception", cls: "" }
];
var grid = document.getElementById("grid");
var frag = document.createDocumentFragment();
PHOTOS.forEach(function (p, i) {
var btn = document.createElement("button");
btn.className = "frame " + p.cls;
btn.type = "button";
btn.dataset.cat = p.cat;
btn.dataset.cap = p.cap;
btn.dataset.index = i;
btn.setAttribute("aria-label", "Open image: " + p.cap);
var img = document.createElement("img");
img.loading = "lazy";
img.alt = p.cap;
img.src = "https://images.unsplash.com/photo-" + p.id + "?auto=format&fit=crop&w=900&q=65";
btn.appendChild(img);
frag.appendChild(btn);
});
grid.appendChild(frag);
/* ---------- Filters ---------- */
var chips = document.querySelectorAll(".chip");
var frames = Array.prototype.slice.call(grid.querySelectorAll(".frame"));
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) {
c.classList.remove("is-active");
c.setAttribute("aria-selected", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
var f = chip.dataset.filter;
frames.forEach(function (frame) {
var show = f === "all" || frame.dataset.cat === f;
frame.classList.toggle("is-hidden", !show);
});
});
});
/* ---------- 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 lastFocus = null;
function visibleFrames() {
return frames.filter(function (f) { return !f.classList.contains("is-hidden"); });
}
function openLightbox(frame) {
var vis = visibleFrames();
current = vis.indexOf(frame);
lastFocus = frame;
renderLightbox(vis);
lb.hidden = false;
document.body.style.overflow = "hidden";
lbClose.focus();
}
function renderLightbox(vis) {
var frame = vis[current];
if (!frame) return;
var src = frame.querySelector("img").src.replace("w=900", "w=1500");
lbImg.src = src;
lbImg.alt = frame.dataset.cap;
lbCap.textContent = frame.dataset.cap + " · " + (current + 1) + " / " + vis.length;
}
function step(dir) {
var vis = visibleFrames();
current = (current + dir + vis.length) % vis.length;
renderLightbox(vis);
}
function closeLightbox() {
lb.hidden = true;
document.body.style.overflow = "";
if (lastFocus) lastFocus.focus();
}
grid.addEventListener("click", function (e) {
var frame = e.target.closest(".frame");
if (frame) openLightbox(frame);
});
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 === "ArrowLeft") step(-1);
else if (e.key === "ArrowRight") step(1);
});
/* Touch swipe on lightbox */
var touchX = null;
lb.addEventListener("touchstart", function (e) { touchX = e.touches[0].clientX; }, { passive: true });
lb.addEventListener("touchend", function (e) {
if (touchX === null) return;
var dx = e.changedTouches[0].clientX - touchX;
if (Math.abs(dx) > 50) step(dx < 0 ? 1 : -1);
touchX = null;
}, { passive: true });
/* ---------- Testimonials ---------- */
var QUOTES = [
{
text: "They melted into the day like guests, not photographers. Every frame feels like a memory we forgot we had.",
name: "Priya & Daniel",
meta: "Married in Ravello",
av: "1544005313-94ddf0286df2"
},
{
text: "We cried looking at the gallery. Amara and Wren caught the exact second my dad saw me in the dress.",
name: "Sofia & Marcus",
meta: "Married in Kyoto",
av: "1438761681033-6461ffad8d80"
},
{
text: "Calm, kind, and impossibly talented. Our families still talk about how easy the whole day felt.",
name: "Elena & Theo",
meta: "Married in Provence",
av: "1500648767791-00dcc994a43e"
}
];
var quotesEl = document.getElementById("quotes");
var dotsEl = document.getElementById("dots");
var qIndex = 0;
var qTimer;
QUOTES.forEach(function (q, i) {
var fig = document.createElement("figure");
fig.className = "quote" + (i === 0 ? " is-active" : "");
fig.innerHTML =
'<blockquote>“' + q.text + '”</blockquote>' +
'<div class="quote-by">' +
'<img loading="lazy" alt="" src="https://images.unsplash.com/photo-' + q.av + '?auto=format&fit=crop&w=120&q=70" />' +
'<cite><strong>' + q.name + '</strong> · ' + q.meta + '</cite>' +
'</div>';
quotesEl.appendChild(fig);
var dot = document.createElement("button");
dot.className = "dot" + (i === 0 ? " is-active" : "");
dot.type = "button";
dot.setAttribute("role", "tab");
dot.setAttribute("aria-label", "Testimonial " + (i + 1));
dot.addEventListener("click", function () { goQuote(i); resetAuto(); });
dotsEl.appendChild(dot);
});
var quoteEls = quotesEl.querySelectorAll(".quote");
var dotEls = dotsEl.querySelectorAll(".dot");
function goQuote(i) {
qIndex = i;
quoteEls.forEach(function (el, k) { el.classList.toggle("is-active", k === i); });
dotEls.forEach(function (el, k) { el.classList.toggle("is-active", k === i); });
}
function resetAuto() {
clearInterval(qTimer);
qTimer = setInterval(function () { goQuote((qIndex + 1) % QUOTES.length); }, 5500);
}
resetAuto();
var wordsSec = document.getElementById("words");
wordsSec.addEventListener("mouseenter", function () { clearInterval(qTimer); });
wordsSec.addEventListener("mouseleave", resetAuto);
/* ---------- Package buttons ---------- */
document.querySelectorAll(".pkg-btn").forEach(function (b) {
b.addEventListener("click", function () {
toast("“" + b.dataset.pkg + "” selected — scroll down to send your inquiry.");
var f = document.getElementById("inquire");
f.scrollIntoView({ behavior: "smooth" });
});
});
/* ---------- Hero counters ---------- */
var counters = document.querySelectorAll(".hero-stats strong");
var counted = false;
function runCounters() {
if (counted) return;
counted = true;
counters.forEach(function (el) {
var target = parseInt(el.dataset.count, 10);
var start = null;
function tick(ts) {
if (!start) start = ts;
var p = Math.min((ts - start) / 1400, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(target * eased);
if (p < 1) requestAnimationFrame(tick);
else el.textContent = target + "+";
}
requestAnimationFrame(tick);
});
}
setTimeout(runCounters, 400);
/* ---------- Hero parallax ---------- */
var heroImg = document.getElementById("heroImg");
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduceMotion && heroImg) {
window.addEventListener("scroll", function () {
var y = window.scrollY;
if (y < window.innerHeight) {
heroImg.style.transform = "translateY(" + (y * 0.18) + "px)";
}
}, { passive: true });
}
/* ---------- Reveal on scroll ---------- */
var revealTargets = document.querySelectorAll(".sec-head, .pkg, .frame, .inquire-form, .assure");
revealTargets.forEach(function (el) { el.classList.add("reveal"); });
if ("IntersectionObserver" in window && !reduceMotion) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) { en.target.classList.add("in"); io.unobserve(en.target); }
});
}, { threshold: 0.12 });
revealTargets.forEach(function (el) { io.observe(el); });
} else {
revealTargets.forEach(function (el) { el.classList.add("in"); });
}
/* ---------- Inquiry form validation ---------- */
var form = document.getElementById("inquireForm");
var errs = {};
form.querySelectorAll(".err").forEach(function (e) { errs[e.dataset.for] = e; });
function setErr(name, msg) {
var input = form.elements[name];
if (errs[name]) errs[name].textContent = msg || "";
if (input) input.classList.toggle("invalid", !!msg);
return !msg;
}
var emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function validate() {
var ok = true;
var v = form.elements;
ok = setErr("names", v.names.value.trim().length >= 2 ? "" : "Please share your names.") && ok;
ok = setErr("email", emailRe.test(v.email.value.trim()) ? "" : "Enter a valid email address.") && ok;
var dateVal = v.date.value;
if (!dateVal) {
ok = setErr("date", "Pick your wedding date.") && ok;
} else if (new Date(dateVal) < new Date(new Date().toDateString())) {
ok = setErr("date", "Please choose a future date.") && ok;
} else {
setErr("date", "");
}
var g = parseInt(v.guests.value, 10);
ok = setErr("guests", (g >= 2 && g <= 600) ? "" : "Guests must be between 2 and 600.") && ok;
return ok;
}
form.addEventListener("submit", function (e) {
e.preventDefault();
if (!validate()) {
toast("Please fix the highlighted fields.");
var firstBad = form.querySelector(".invalid");
if (firstBad) firstBad.focus();
return;
}
var names = form.elements.names.value.trim();
var date = new Date(form.elements.date.value).toLocaleDateString(undefined, {
month: "long", day: "numeric", year: "numeric"
});
var btn = form.querySelector('button[type="submit"]');
btn.disabled = true;
btn.textContent = "Checking availability…";
setTimeout(function () {
btn.disabled = false;
btn.textContent = "Send inquiry";
form.reset();
toast("Thank you, " + names + "! We're available on " + date + " — check your inbox within 24h.");
}, 1100);
});
/* Clear error as the user types */
form.addEventListener("input", function (e) {
if (e.target.name && errs[e.target.name] && e.target.classList.contains("invalid")) {
setErr(e.target.name, "");
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Amara & Wren — Wedding 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>
<!-- HEADER -->
<header class="site-head" id="top">
<a class="brand" href="#top" aria-label="Amara and Wren, home">
<span class="brand-mark">A&W</span>
<span class="brand-name">Amara <em>&</em> Wren</span>
</a>
<nav class="nav" aria-label="Primary">
<a href="#gallery">Gallery</a>
<a href="#packages">Packages</a>
<a href="#words">Kind words</a>
</nav>
<a href="#inquire" class="btn btn-ghost head-cta">Check your date</a>
</header>
<!-- HERO -->
<section class="hero" aria-label="Introduction">
<div class="hero-media" aria-hidden="true">
<img id="heroImg" src="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=1600&q=70" alt="" />
<div class="hero-veil"></div>
</div>
<div class="hero-inner">
<p class="eyebrow">Wedding photography · Est. 2016</p>
<h1 class="hero-title">Love, told in<br /><em>quiet</em> light.</h1>
<p class="hero-lede">We photograph weddings the way they actually feel — unhurried, tender, and a little bit wild. Documentary at heart, editorial in finish.</p>
<div class="hero-actions">
<a href="#inquire" class="btn btn-solid">Inquire for 2026</a>
<a href="#gallery" class="btn btn-line">View the gallery</a>
</div>
<ul class="hero-stats" aria-label="Studio at a glance">
<li><strong data-count="240">0</strong><span>weddings</span></li>
<li><strong data-count="19">0</strong><span>countries</span></li>
<li><strong data-count="12">0</strong><span>awards</span></li>
</ul>
</div>
<a href="#gallery" class="scroll-cue" aria-label="Scroll to gallery"><span></span>Scroll</a>
</section>
<!-- GALLERY -->
<section class="gallery" id="gallery" aria-label="Gallery">
<div class="sec-head">
<p class="eyebrow">Selected frames</p>
<h2 class="sec-title">A look through the day</h2>
<div class="filters" role="tablist" aria-label="Filter gallery">
<button class="chip is-active" role="tab" aria-selected="true" data-filter="all">All</button>
<button class="chip" role="tab" aria-selected="false" data-filter="ceremony">Ceremonies</button>
<button class="chip" role="tab" aria-selected="false" data-filter="portrait">Portraits</button>
<button class="chip" role="tab" aria-selected="false" data-filter="detail">Details</button>
</div>
</div>
<div class="grid" id="grid"><!-- injected by JS --></div>
</section>
<!-- PACKAGES -->
<section class="packages" id="packages" aria-label="Packages">
<div class="sec-head">
<p class="eyebrow">Coverage</p>
<h2 class="sec-title">Simple, honest packages</h2>
<p class="sec-sub">Every collection includes a pre-wedding call, a private online gallery, and print rights. Travel quoted separately.</p>
</div>
<div class="pkgs">
<article class="pkg">
<p class="pkg-name">The Elopement</p>
<p class="pkg-price"><span>$</span>2,400</p>
<ul class="pkg-list">
<li>4 hours coverage</li>
<li>One photographer</li>
<li>150+ edited images</li>
<li>72-hour sneak peek</li>
</ul>
<button class="btn btn-line pkg-btn" data-pkg="The Elopement">Choose</button>
</article>
<article class="pkg is-featured">
<span class="pkg-badge">Most booked</span>
<p class="pkg-name">The Full Day</p>
<p class="pkg-price"><span>$</span>4,200</p>
<ul class="pkg-list">
<li>10 hours coverage</li>
<li>Two photographers</li>
<li>500+ edited images</li>
<li>Engagement session</li>
<li>Heirloom album credit</li>
</ul>
<button class="btn btn-solid pkg-btn" data-pkg="The Full Day">Choose</button>
</article>
<article class="pkg">
<p class="pkg-name">The Weekend</p>
<p class="pkg-price"><span>$</span>6,800</p>
<ul class="pkg-list">
<li>Two-day coverage</li>
<li>Two photographers</li>
<li>800+ edited images</li>
<li>Rehearsal + brunch</li>
<li>Fine-art album</li>
</ul>
<button class="btn btn-line pkg-btn" data-pkg="The Weekend">Choose</button>
</article>
</div>
</section>
<!-- TESTIMONIAL -->
<section class="words" id="words" aria-label="Testimonials">
<div class="words-inner">
<p class="eyebrow center">Kind words</p>
<div class="quotes" id="quotes" aria-live="polite">
<!-- injected by JS -->
</div>
<div class="dots" id="dots" role="tablist" aria-label="Choose testimonial"></div>
</div>
</section>
<!-- INQUIRE / CTA -->
<section class="inquire" id="inquire" aria-label="Inquire">
<div class="inquire-grid">
<div class="inquire-copy">
<p class="eyebrow">Let's begin</p>
<h2 class="sec-title">Tell us about your day</h2>
<p class="sec-sub">We take a limited number of weddings each season so every couple gets our full attention. Share a few details and we'll check availability within 24 hours.</p>
<ul class="assure">
<li><span>✶</span> No obligation, ever</li>
<li><span>✶</span> Payment plans available</li>
<li><span>✶</span> Reply within one day</li>
</ul>
</div>
<form class="inquire-form" id="inquireForm" novalidate>
<div class="field">
<label for="names">Your names</label>
<input id="names" name="names" type="text" placeholder="Amara & Wren" autocomplete="name" required />
<p class="err" data-for="names"></p>
</div>
<div class="field">
<label for="email">Email</label>
<input id="email" name="email" type="email" placeholder="[email protected]" autocomplete="email" required />
<p class="err" data-for="email"></p>
</div>
<div class="row">
<div class="field">
<label for="date">Wedding date</label>
<input id="date" name="date" type="date" required />
<p class="err" data-for="date"></p>
</div>
<div class="field">
<label for="guests">Guest count</label>
<input id="guests" name="guests" type="number" min="2" max="600" placeholder="120" required />
<p class="err" data-for="guests"></p>
</div>
</div>
<div class="field">
<label for="venue">Venue or city <span class="opt">(optional)</span></label>
<input id="venue" name="venue" type="text" placeholder="Amalfi Coast, Italy" />
</div>
<button type="submit" class="btn btn-solid btn-block">Send inquiry</button>
<p class="form-note">By sending, you agree to be contacted about your wedding date. We never share your details.</p>
</form>
</div>
</section>
<!-- FOOTER -->
<footer class="site-foot">
<div class="foot-brand">
<span class="brand-name">Amara <em>&</em> Wren</span>
<p>Documentary wedding photography, worldwide.</p>
</div>
<div class="foot-links">
<a href="#gallery">Gallery</a>
<a href="#packages">Packages</a>
<a href="#inquire">Inquire</a>
<a href="#top">Back to top ↑</a>
</div>
<p class="foot-fine">© 2026 Amara & Wren Studio · Fictional demo · Photos via Unsplash</p>
</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">
<img id="lbImg" src="" alt="" />
<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 a fictional wedding photographer (Amara & Wren Studio). The page opens on a romantic full-bleed hero with a Fraunces display headline, thin uppercase labels, and a soft parallax drift on scroll. Below it, a filterable gallery lets visitors toggle between Ceremonies, Portraits, and Details; clicking any frame opens an accessible lightbox with arrow-key and swipe navigation.
The packages teaser presents three coverage tiers as hoverable cards with a highlighted “Most booked” badge, and a rotating testimonial cycles through real-feeling client quotes with dot controls and autoplay that pauses on hover. The inquiry section is a real form: it validates the couple’s names, email, wedding date and estimated guest count, then fires a toast confirmation and a faux availability check.
Everything is vanilla JS — no frameworks. Controls are keyboard-usable with visible focus rings, the layout collapses cleanly to a single column under 520px, and all imagery uses Unsplash placeholder URLs so the demo is fully self-contained.