Wedding — Landing Elegant
An editorial, blush-and-gold wedding landing page with a serif hero and animated countdown, a Cormorant-set love story timeline, a filterable photo gallery with lightbox, a details grid for ceremony and reception, and a validated RSVP form with a guest-count stepper. Thin gold rules, airy spacing, and smooth reveal-on-scroll micro-interactions give it a romantic, print-inspired feel that works down to small phones.
MCP
Code
:root {
--blush: #e8b7b0;
--blush-50: #fbeeec;
--rose: #c98a86;
--gold: #c9a24b;
--gold-d: #a8862f;
--ink: #3a2b2b;
--ink-2: #6b5555;
--muted: #9a8585;
--bg: #fbf7f4;
--cream: #f4ece4;
--white: #fff;
--line: rgba(58, 43, 43, 0.12);
--r-sm: 8px;
--r-md: 16px;
--r-lg: 26px;
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--shadow: 0 18px 44px -22px rgba(58, 43, 43, 0.35);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: var(--sans);
color: var(--ink);
background: var(--bg);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3 {
margin: 0;
font-family: var(--serif);
font-weight: 500;
letter-spacing: 0.01em;
}
a {
color: inherit;
}
img {
max-width: 100%;
}
:focus-visible {
outline: 2px solid var(--gold-d);
outline-offset: 3px;
border-radius: 4px;
}
/* Buttons */
.btn {
display: inline-block;
border: none;
cursor: pointer;
font-family: var(--sans);
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
text-decoration: none;
padding: 0.95rem 2.1rem;
border-radius: 999px;
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.btn--gold {
background: linear-gradient(135deg, var(--gold) 0%, var(--gold-d) 100%);
color: #fff;
box-shadow: 0 12px 26px -12px rgba(168, 134, 47, 0.7);
}
.btn--gold:hover {
transform: translateY(-2px);
box-shadow: 0 18px 34px -12px rgba(168, 134, 47, 0.75);
}
.btn--gold:active {
transform: translateY(0);
}
.btn--wide {
grid-column: 1 / -1;
justify-self: center;
margin-top: 0.4rem;
}
/* Top bar */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem clamp(1rem, 5vw, 3.5rem);
background: rgba(251, 247, 244, 0.82);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.35rem;
text-decoration: none;
font-family: var(--serif);
font-size: 1.5rem;
color: var(--ink);
}
.brand__amp {
color: var(--gold-d);
font-size: 1.1rem;
}
.nav {
display: flex;
align-items: center;
gap: clamp(0.9rem, 2.5vw, 2rem);
}
.nav a {
text-decoration: none;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-2);
transition: color 0.16s ease;
}
.nav a:hover {
color: var(--gold-d);
}
.nav__cta {
color: var(--gold-d) !important;
border: 1px solid var(--gold);
padding: 0.5rem 1rem;
border-radius: 999px;
}
.nav__cta:hover {
background: var(--gold);
color: #fff !important;
}
/* Hero */
.hero {
position: relative;
text-align: center;
padding: clamp(3rem, 9vw, 6rem) 1.2rem clamp(4rem, 10vw, 7rem);
background:
radial-gradient(1200px 500px at 50% -10%, var(--blush-50), transparent 60%),
radial-gradient(800px 400px at 90% 10%, rgba(201, 162, 75, 0.1), transparent 60%),
var(--bg);
}
.hero__eyebrow {
margin: 0;
font-size: 0.74rem;
font-weight: 600;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--rose);
}
.hero__rule {
width: 64px;
height: 1px;
margin: 1.4rem auto;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
.hero__title {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: clamp(0.4rem, 2vw, 1.4rem);
font-size: clamp(3.2rem, 13vw, 7rem);
line-height: 0.95;
font-weight: 500;
color: var(--ink);
}
.hero__amp {
color: var(--gold-d);
font-style: italic;
font-size: 0.62em;
}
.hero__date {
margin: 1.5rem 0 0.3rem;
font-family: var(--serif);
font-size: clamp(1.1rem, 3vw, 1.5rem);
color: var(--ink-2);
}
.hero__place {
margin: 0;
font-size: 0.8rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
/* Countdown */
.countdown {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: clamp(0.6rem, 3vw, 1.8rem);
margin: 2.6rem auto 2.4rem;
max-width: 520px;
}
.count {
min-width: 74px;
padding: 1rem 0.6rem;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--shadow);
}
.count__num {
display: block;
font-family: var(--serif);
font-size: clamp(1.8rem, 6vw, 2.6rem);
font-weight: 600;
color: var(--gold-d);
font-variant-numeric: tabular-nums;
}
.count__label {
display: block;
margin-top: 0.2rem;
font-size: 0.64rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--muted);
}
.scrollcue {
display: block;
width: 1px;
height: 56px;
margin: 2.6rem auto 0;
overflow: hidden;
}
.scrollcue__line {
display: block;
width: 1px;
height: 100%;
background: var(--gold);
transform-origin: top;
animation: cue 1.8s ease-in-out infinite;
}
@keyframes cue {
0% {
transform: scaleY(0);
opacity: 0;
}
40% {
transform: scaleY(1);
opacity: 1;
}
100% {
transform: scaleY(1) translateY(56px);
opacity: 0;
}
}
/* Sections */
.section {
max-width: 1080px;
margin: 0 auto;
padding: clamp(3.5rem, 9vw, 6rem) clamp(1.2rem, 5vw, 2rem);
text-align: center;
}
.section--cream {
max-width: none;
background: var(--cream);
}
.section--cream > * {
max-width: 1080px;
margin-left: auto;
margin-right: auto;
}
.kicker {
margin: 0;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--rose);
}
.section__title {
margin: 0.6rem 0 0;
font-size: clamp(2rem, 6vw, 3.2rem);
color: var(--ink);
}
.rule-c {
margin: 1rem auto 2.6rem;
color: var(--gold);
font-size: 1rem;
position: relative;
width: 200px;
}
.rule-c::before,
.rule-c::after {
content: "";
position: absolute;
top: 50%;
width: 74px;
height: 1px;
background: linear-gradient(90deg, transparent, var(--gold));
}
.rule-c::before {
left: 0;
}
.rule-c::after {
right: 0;
transform: scaleX(-1);
}
/* Timeline */
.timeline {
list-style: none;
margin: 0;
padding: 0;
max-width: 720px;
margin-inline: auto;
text-align: left;
}
.tl {
display: grid;
grid-template-columns: 92px 1fr;
gap: 1.4rem;
padding: 1.6rem 0;
border-bottom: 1px solid var(--line);
}
.tl:last-child {
border-bottom: none;
}
.tl__year {
font-family: var(--serif);
font-size: 1.7rem;
font-weight: 600;
color: var(--gold-d);
line-height: 1;
padding-top: 0.2rem;
}
.tl__body h3 {
font-size: 1.5rem;
color: var(--ink);
margin-bottom: 0.35rem;
}
.tl__body p {
margin: 0;
color: var(--ink-2);
font-size: 0.98rem;
}
/* Filters */
.filters {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.6rem;
margin-bottom: 2rem;
}
.chip {
border: 1px solid var(--line);
background: var(--white);
color: var(--ink-2);
font-family: var(--sans);
font-size: 0.74rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 0.55rem 1.15rem;
border-radius: 999px;
cursor: pointer;
transition: all 0.16s ease;
}
.chip:hover {
border-color: var(--gold);
color: var(--gold-d);
}
.chip.is-active {
background: var(--ink);
border-color: var(--ink);
color: var(--white);
}
/* Gallery */
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.9rem;
}
.tile {
position: relative;
aspect-ratio: 3 / 4;
border-radius: var(--r-md);
border: none;
padding: 0;
cursor: pointer;
overflow: hidden;
box-shadow: var(--shadow);
transition: transform 0.22s ease, box-shadow 0.22s ease;
}
.tile:hover {
transform: translateY(-4px);
box-shadow: 0 26px 50px -22px rgba(58, 43, 43, 0.45);
}
.tile__cap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0.9rem 0.9rem 0.75rem;
text-align: left;
font-family: var(--serif);
font-size: 1.05rem;
color: #fff;
background: linear-gradient(transparent, rgba(58, 43, 43, 0.62));
opacity: 0;
transform: translateY(8px);
transition: opacity 0.22s ease, transform 0.22s ease;
}
.tile:hover .tile__cap,
.tile:focus-visible .tile__cap {
opacity: 1;
transform: translateY(0);
}
.tile.is-hidden {
display: none;
}
/* Detail cards */
.cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.1rem;
text-align: left;
}
.card {
position: relative;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 1.8rem 1.6rem 1.5rem;
box-shadow: var(--shadow);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 26px 50px -22px rgba(168, 134, 47, 0.4);
}
.card__badge {
display: inline-block;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--gold-d);
background: var(--blush-50);
border: 1px solid rgba(201, 162, 75, 0.35);
padding: 0.3rem 0.7rem;
border-radius: 999px;
margin-bottom: 0.9rem;
}
.card h3 {
font-size: 1.55rem;
color: var(--ink);
margin-bottom: 0.2rem;
}
.card p {
margin: 0.15rem 0 0;
color: var(--ink-2);
font-weight: 500;
}
.card__muted {
margin-top: 0.7rem !important;
font-weight: 400 !important;
font-size: 0.9rem;
color: var(--muted) !important;
}
/* RSVP */
.rsvp__lead {
margin: -1.6rem 0 2.4rem;
font-family: var(--serif);
font-size: 1.15rem;
color: var(--ink-2);
}
.rsvp {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.3rem;
max-width: 640px;
margin-inline: auto;
text-align: left;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: clamp(1.6rem, 5vw, 2.6rem);
box-shadow: var(--shadow);
}
.field {
display: flex;
flex-direction: column;
gap: 0.45rem;
}
.field--full {
grid-column: 1 / -1;
}
.field label,
.field__label {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-2);
}
.opt {
color: var(--muted);
font-weight: 500;
text-transform: none;
letter-spacing: 0;
}
.field input,
.field select,
.field textarea {
font-family: var(--sans);
font-size: 0.95rem;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 0.75rem 0.85rem;
transition: border-color 0.16s ease, box-shadow 0.16s ease;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px rgba(201, 162, 75, 0.18);
}
.field textarea {
resize: vertical;
}
.field input.invalid {
border-color: var(--rose);
box-shadow: 0 0 0 3px rgba(201, 138, 134, 0.18);
}
.err {
margin: 0;
min-height: 1em;
font-size: 0.78rem;
color: var(--rose);
}
/* Toggle */
.toggle {
display: flex;
gap: 0.6rem;
}
.toggle__opt {
flex: 1;
position: relative;
cursor: pointer;
}
.toggle__opt input {
position: absolute;
opacity: 0;
inset: 0;
cursor: pointer;
}
.toggle__opt span {
display: block;
text-align: center;
font-size: 0.82rem;
font-weight: 600;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 0.7rem 0.5rem;
transition: all 0.16s ease;
}
.toggle__opt input:checked + span {
background: var(--blush-50);
border-color: var(--gold);
color: var(--gold-d);
}
.toggle__opt input:focus-visible + span {
outline: 2px solid var(--gold-d);
outline-offset: 2px;
}
/* Stepper */
.stepper {
display: inline-flex;
align-items: center;
gap: 0.4rem;
align-self: flex-start;
}
.stepper__btn {
width: 40px;
height: 40px;
font-size: 1.3rem;
line-height: 1;
color: var(--gold-d);
background: var(--bg);
border: 1px solid var(--line);
border-radius: 50%;
cursor: pointer;
transition: all 0.16s ease;
}
.stepper__btn:hover:not(:disabled) {
background: var(--gold);
color: #fff;
border-color: var(--gold);
}
.stepper__btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.stepper__val {
min-width: 44px;
text-align: center;
font-family: var(--serif);
font-size: 1.6rem;
font-weight: 600;
color: var(--ink);
}
.rsvp.hide-guests [data-guest-field] {
display: none;
}
/* Footer */
.foot {
text-align: center;
padding: 3.5rem 1.2rem 3rem;
border-top: 1px solid var(--line);
background: var(--bg);
}
.foot__mono {
font-family: var(--serif);
font-size: 2rem;
color: var(--gold-d);
margin-bottom: 0.6rem;
}
.foot p {
margin: 0.3rem 0;
font-size: 0.86rem;
letter-spacing: 0.06em;
color: var(--ink-2);
}
.foot__fine {
color: var(--muted) !important;
font-style: italic;
font-family: var(--serif);
font-size: 1rem !important;
}
/* Reveal on scroll */
.reveal {
opacity: 0;
transform: translateY(22px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.reveal.in {
opacity: 1;
transform: none;
}
/* Lightbox */
.lightbox {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1.2rem;
background: rgba(58, 43, 43, 0.82);
backdrop-filter: blur(6px);
}
.lightbox[hidden] {
display: none;
}
.lightbox__figure {
margin: 0;
max-width: 460px;
width: 100%;
}
.lightbox__img {
aspect-ratio: 3 / 4;
border-radius: var(--r-md);
box-shadow: 0 30px 70px -20px rgba(0, 0, 0, 0.6);
}
.lightbox__cap {
margin-top: 0.9rem;
text-align: center;
font-family: var(--serif);
font-size: 1.3rem;
color: #fbeeec;
}
.lightbox__close {
position: absolute;
top: 1rem;
right: 1.2rem;
width: 44px;
height: 44px;
font-size: 1.6rem;
color: #fff;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
cursor: pointer;
}
.lightbox__nav {
width: 48px;
height: 48px;
flex: none;
font-size: 1.7rem;
color: #fff;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
cursor: pointer;
transition: background 0.16s ease;
}
.lightbox__nav:hover,
.lightbox__close:hover {
background: rgba(255, 255, 255, 0.28);
}
/* Toast */
.toast {
position: fixed;
left: 50%;
bottom: 2rem;
transform: translate(-50%, 140%);
z-index: 60;
background: var(--ink);
color: #fff;
font-size: 0.88rem;
font-weight: 500;
padding: 0.85rem 1.4rem;
border-radius: 999px;
box-shadow: 0 18px 40px -14px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(201, 162, 75, 0.5);
transition: transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1.2);
max-width: 90vw;
}
.toast.show {
transform: translate(-50%, 0);
}
/* Responsive */
@media (max-width: 820px) {
.gallery,
.cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
.nav a:not(.nav__cta) {
display: none;
}
.hero__title {
font-size: clamp(2.8rem, 18vw, 4rem);
}
.count {
min-width: 62px;
padding: 0.8rem 0.4rem;
}
.gallery,
.cards {
grid-template-columns: 1fr;
}
.tl {
grid-template-columns: 64px 1fr;
gap: 1rem;
}
.tl__year {
font-size: 1.35rem;
}
.rsvp {
grid-template-columns: 1fr;
}
.toggle {
flex-direction: column;
}
.lightbox__nav {
width: 42px;
height: 42px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
scroll-behavior: auto !important;
}
.reveal {
opacity: 1;
transform: none;
transition: none;
}
}"use strict";
/* ---------- Toast helper ---------- */
const toastEl = document.getElementById("toast");
let toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 3400);
}
/* ---------- Countdown ---------- */
const WEDDING = new Date("2026-09-12T15:30:00").getTime();
const units = document.querySelectorAll(".count__num");
function pad(n) {
return String(n).padStart(2, "0");
}
function tick() {
const diff = WEDDING - Date.now();
const clamp = Math.max(diff, 0);
const days = Math.floor(clamp / 864e5);
const hours = Math.floor((clamp % 864e5) / 36e5);
const minutes = Math.floor((clamp % 36e5) / 6e4);
const seconds = Math.floor((clamp % 6e4) / 1e3);
const map = { days, hours: pad(hours), minutes: pad(minutes), seconds: pad(seconds) };
units.forEach((el) => {
el.textContent = map[el.dataset.unit];
});
}
tick();
setInterval(tick, 1000);
/* ---------- Gallery data + render ---------- */
const PHOTOS = [
{ cap: "The proposal, Ravello", cat: "engagement", g: "linear-gradient(135deg,#e8b7b0,#c98a86)" },
{ cap: "Golden hour", cat: "travels", g: "linear-gradient(160deg,#c9a24b,#a8862f)" },
{ cap: "Sunday mornings", cat: "everyday", g: "linear-gradient(135deg,#f4ece4,#e8b7b0)" },
{ cap: "Lemon groves", cat: "travels", g: "linear-gradient(150deg,#c9a24b,#e8b7b0)" },
{ cap: "First dance rehearsal", cat: "everyday", g: "linear-gradient(135deg,#6b5555,#c98a86)" },
{ cap: "She said yes", cat: "engagement", g: "linear-gradient(160deg,#c98a86,#a8862f)" },
{ cap: "Coastline drive", cat: "travels", g: "linear-gradient(135deg,#fbeeec,#c9a24b)" },
{ cap: "Coffee for two", cat: "everyday", g: "linear-gradient(150deg,#e8b7b0,#6b5555)" },
{ cap: "The ring", cat: "engagement", g: "linear-gradient(135deg,#a8862f,#e8b7b0)" },
];
const grid = document.getElementById("galleryGrid");
PHOTOS.forEach((p, i) => {
const btn = document.createElement("button");
btn.className = "tile";
btn.type = "button";
btn.dataset.cat = p.cat;
btn.dataset.index = String(i);
btn.style.background = p.g;
btn.setAttribute("aria-label", "View photo: " + p.cap);
btn.innerHTML = '<span class="tile__cap">' + p.cap + "</span>";
grid.appendChild(btn);
});
/* ---------- Filters ---------- */
const chips = document.querySelectorAll(".chip");
chips.forEach((chip) => {
chip.addEventListener("click", () => {
chips.forEach((c) => {
c.classList.remove("is-active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
const f = chip.dataset.filter;
grid.querySelectorAll(".tile").forEach((t) => {
t.classList.toggle("is-hidden", f !== "all" && t.dataset.cat !== f);
});
});
});
/* ---------- Lightbox ---------- */
const lb = document.getElementById("lightbox");
const lbImg = document.getElementById("lbImg");
const lbCap = document.getElementById("lbCap");
let current = 0;
let lastFocused = null;
function visibleTiles() {
return Array.from(grid.querySelectorAll(".tile:not(.is-hidden)"));
}
function showPhoto(idx) {
const tiles = visibleTiles();
if (!tiles.length) return;
current = (idx + tiles.length) % tiles.length;
const tile = tiles[current];
const p = PHOTOS[Number(tile.dataset.index)];
lbImg.style.background = p.g;
lbCap.textContent = p.cap;
}
function openLightbox(tile) {
lastFocused = tile;
const tiles = visibleTiles();
showPhoto(tiles.indexOf(tile));
lb.hidden = false;
document.getElementById("lbClose").focus();
document.body.style.overflow = "hidden";
}
function closeLightbox() {
lb.hidden = true;
document.body.style.overflow = "";
if (lastFocused) lastFocused.focus();
}
grid.addEventListener("click", (e) => {
const tile = e.target.closest(".tile");
if (tile) openLightbox(tile);
});
document.getElementById("lbClose").addEventListener("click", closeLightbox);
document.getElementById("lbNext").addEventListener("click", () => showPhoto(current + 1));
document.getElementById("lbPrev").addEventListener("click", () => showPhoto(current - 1));
lb.addEventListener("click", (e) => {
if (e.target === lb) closeLightbox();
});
document.addEventListener("keydown", (e) => {
if (lb.hidden) return;
if (e.key === "Escape") closeLightbox();
if (e.key === "ArrowRight") showPhoto(current + 1);
if (e.key === "ArrowLeft") showPhoto(current - 1);
});
/* ---------- RSVP stepper ---------- */
const guestCount = document.getElementById("guestCount");
const MIN = 1;
const MAX = 6;
let guests = 1;
document.querySelectorAll(".stepper__btn").forEach((b) => {
b.addEventListener("click", () => {
guests = Math.min(MAX, Math.max(MIN, guests + Number(b.dataset.step)));
guestCount.textContent = String(guests);
syncStepper();
});
});
function syncStepper() {
const [minus, plus] = document.querySelectorAll(".stepper__btn");
minus.disabled = guests <= MIN;
plus.disabled = guests >= MAX;
}
syncStepper();
/* ---------- RSVP attendance toggle ---------- */
const form = document.getElementById("rsvpForm");
form.querySelectorAll('input[name="attending"]').forEach((r) => {
r.addEventListener("change", () => {
form.classList.toggle("hide-guests", form.attending.value === "no");
});
});
/* ---------- Validation + submit ---------- */
function setError(name, msg) {
const field = form.querySelector('[name="' + name + '"]');
const err = form.querySelector('.err[data-for="' + name + '"]');
if (field) field.classList.toggle("invalid", Boolean(msg));
if (err) err.textContent = msg || "";
}
const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
form.addEventListener("submit", (e) => {
e.preventDefault();
let ok = true;
const name = form.name.value.trim();
const email = form.email.value.trim();
if (name.length < 2) {
setError("name", "Please enter your full name.");
ok = false;
} else {
setError("name", "");
}
if (!emailRe.test(email)) {
setError("email", "Please enter a valid email address.");
ok = false;
} else {
setError("email", "");
}
if (!ok) {
const firstBad = form.querySelector(".invalid");
if (firstBad) firstBad.focus();
toast("Please fix the highlighted fields.");
return;
}
const attending = form.attending.value === "yes";
if (attending) {
toast("Thank you, " + name.split(" ")[0] + "! " + guests + (guests > 1 ? " seats" : " seat") + " reserved.");
} else {
toast("We will miss you, " + name.split(" ")[0] + ". Thank you for letting us know.");
}
form.reset();
guests = 1;
guestCount.textContent = "1";
form.classList.remove("hide-guests");
syncStepper();
});
form.querySelectorAll("input").forEach((inp) => {
inp.addEventListener("input", () => setError(inp.name, ""));
});
/* ---------- Reveal on scroll ---------- */
const revealTargets = document.querySelectorAll(
".section .kicker, .section__title, .tl, .card, .tile, .rsvp, .rule-c, .rsvp__lead"
);
revealTargets.forEach((el) => el.classList.add("reveal"));
if ("IntersectionObserver" in window) {
const io = new IntersectionObserver(
(entries) => {
entries.forEach((en) => {
if (en.isIntersecting) {
en.target.classList.add("in");
io.unobserve(en.target);
}
});
},
{ threshold: 0.12 }
);
revealTargets.forEach((el) => io.observe(el));
} else {
revealTargets.forEach((el) => el.classList.add("in"));
}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Amara & Elliot — September 12, 2026</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=Cormorant+Garamond:wght@400;500;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" aria-label="Amara and Elliot, home">
<span class="brand__mono">A</span>
<span class="brand__amp">&</span>
<span class="brand__mono">E</span>
</a>
<nav class="nav" aria-label="Primary">
<a href="#story">Our Story</a>
<a href="#gallery">Gallery</a>
<a href="#details">Details</a>
<a class="nav__cta" href="#rsvp">RSVP</a>
</nav>
</header>
<main id="top">
<!-- Hero -->
<section class="hero" aria-labelledby="hero-title">
<p class="hero__eyebrow">Together with their families</p>
<div class="hero__rule" aria-hidden="true"></div>
<h1 class="hero__title" id="hero-title">
<span>Amara</span>
<span class="hero__amp">&</span>
<span>Elliot</span>
</h1>
<p class="hero__date">Saturday, the twelfth of September · Two thousand twenty-six</p>
<p class="hero__place">Villa Serena · Amalfi Coast, Italy</p>
<div class="countdown" aria-label="Countdown to the wedding day">
<div class="count"><span class="count__num" data-unit="days">0</span><span class="count__label">Days</span></div>
<div class="count"><span class="count__num" data-unit="hours">0</span><span class="count__label">Hours</span></div>
<div class="count"><span class="count__num" data-unit="minutes">0</span><span class="count__label">Minutes</span></div>
<div class="count"><span class="count__num" data-unit="seconds">0</span><span class="count__label">Seconds</span></div>
</div>
<a class="btn btn--gold" href="#rsvp">Reserve your seat</a>
<a class="scrollcue" href="#story" aria-label="Scroll to our story">
<span class="scrollcue__line" aria-hidden="true"></span>
</a>
</section>
<!-- Story -->
<section class="section" id="story" aria-labelledby="story-title">
<p class="kicker">Our Story</p>
<h2 class="section__title" id="story-title">How two paths became one</h2>
<div class="rule-c" aria-hidden="true">✦</div>
<ol class="timeline">
<li class="tl">
<div class="tl__year">2019</div>
<div class="tl__body">
<h3>A rainy bookshop</h3>
<p>We reached for the same worn copy of a poetry collection in a little shop in Trastevere. Neither of us let go — so we shared a coffee instead.</p>
</div>
</li>
<li class="tl">
<div class="tl__year">2021</div>
<div class="tl__body">
<h3>The long way home</h3>
<p>Two cities, endless letters, and a shared calendar of countdowns. Distance taught us how much we wanted the same horizon.</p>
</div>
</li>
<li class="tl">
<div class="tl__year">2024</div>
<div class="tl__body">
<h3>A quiet yes</h3>
<p>On a cliffside at dusk, with the sea below and no one watching, Elliot asked and Amara laughed before she said yes.</p>
</div>
</li>
<li class="tl">
<div class="tl__year">2026</div>
<div class="tl__body">
<h3>Forever, out loud</h3>
<p>Now we gather everyone we love to celebrate the beginning of the rest of it. We would be honored to have you there.</p>
</div>
</li>
</ol>
</section>
<!-- Gallery -->
<section class="section section--cream" id="gallery" aria-labelledby="gallery-title">
<p class="kicker">Moments</p>
<h2 class="section__title" id="gallery-title">A gallery of us</h2>
<div class="rule-c" aria-hidden="true">✦</div>
<div class="filters" role="group" aria-label="Filter photos">
<button class="chip is-active" data-filter="all" aria-pressed="true">All</button>
<button class="chip" data-filter="engagement" aria-pressed="false">Engagement</button>
<button class="chip" data-filter="travels" aria-pressed="false">Travels</button>
<button class="chip" data-filter="everyday" aria-pressed="false">Everyday</button>
</div>
<div class="gallery" id="galleryGrid"><!-- populated by script.js --></div>
</section>
<!-- Details -->
<section class="section" id="details" aria-labelledby="details-title">
<p class="kicker">The Day</p>
<h2 class="section__title" id="details-title">Details & itinerary</h2>
<div class="rule-c" aria-hidden="true">✦</div>
<div class="cards">
<article class="card">
<span class="card__badge">3:30 PM</span>
<h3>The Ceremony</h3>
<p>Villa Serena Gardens</p>
<p class="card__muted">A short outdoor ceremony beneath the lemon trees. Please be seated by 3:15.</p>
</article>
<article class="card">
<span class="card__badge">5:00 PM</span>
<h3>Cocktail Hour</h3>
<p>The Terrace</p>
<p class="card__muted">Aperitivo, sea views, and live strings while we capture a few portraits.</p>
</article>
<article class="card">
<span class="card__badge">7:00 PM</span>
<h3>Reception & Dinner</h3>
<p>The Orangery</p>
<p class="card__muted">A seated dinner, toasts, and dancing until the last song of the night.</p>
</article>
<article class="card">
<span class="card__badge">Info</span>
<h3>Dress Code</h3>
<p>Formal garden attire</p>
<p class="card__muted">Blush, cream, and gold encouraged. Heels-on-grass friendly footwear advised.</p>
</article>
<article class="card">
<span class="card__badge">Stay</span>
<h3>Where to Rest</h3>
<p>Hotel Belmare</p>
<p class="card__muted">A block of rooms is held under Amara & Elliot until August 1st.</p>
</article>
<article class="card">
<span class="card__badge">Travel</span>
<h3>Getting There</h3>
<p>Naples (NAP), 1h transfer</p>
<p class="card__muted">A shuttle departs the hotel lobby at 2:45 PM and returns at midnight.</p>
</article>
</div>
</section>
<!-- RSVP -->
<section class="section section--cream" id="rsvp" aria-labelledby="rsvp-title">
<p class="kicker">Kindly Reply</p>
<h2 class="section__title" id="rsvp-title">Will you join us?</h2>
<div class="rule-c" aria-hidden="true">✦</div>
<p class="rsvp__lead">Please respond by the first of August, 2026.</p>
<form class="rsvp" id="rsvpForm" novalidate>
<div class="field">
<label for="name">Full name</label>
<input id="name" name="name" type="text" autocomplete="name" placeholder="Amara Whitfield" required />
<p class="err" data-for="name"></p>
</div>
<div class="field">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" placeholder="[email protected]" required />
<p class="err" data-for="email"></p>
</div>
<div class="field">
<span class="field__label">Attendance</span>
<div class="toggle" role="radiogroup" aria-label="Attendance">
<label class="toggle__opt">
<input type="radio" name="attending" value="yes" checked />
<span>Joyfully accepts</span>
</label>
<label class="toggle__opt">
<input type="radio" name="attending" value="no" />
<span>Regretfully declines</span>
</label>
</div>
</div>
<div class="field" data-guest-field>
<span class="field__label" id="guestsLabel">Number of guests</span>
<div class="stepper" role="group" aria-labelledby="guestsLabel">
<button type="button" class="stepper__btn" data-step="-1" aria-label="Decrease guests">−</button>
<output class="stepper__val" id="guestCount" aria-live="polite">1</output>
<button type="button" class="stepper__btn" data-step="1" aria-label="Increase guests">+</button>
</div>
</div>
<div class="field" data-guest-field>
<label for="meal">Meal preference</label>
<select id="meal" name="meal">
<option value="chef">Chef's tasting menu</option>
<option value="fish">Catch of the day</option>
<option value="vegetarian">Garden vegetarian</option>
<option value="vegan">Vegan</option>
</select>
</div>
<div class="field field--full">
<label for="note">A note for the couple <span class="opt">(optional)</span></label>
<textarea id="note" name="note" rows="3" placeholder="Song requests, love, or a favorite memory…"></textarea>
</div>
<button type="submit" class="btn btn--gold btn--wide">Send RSVP</button>
</form>
</section>
</main>
<footer class="foot">
<div class="foot__mono">A & E</div>
<p>Amara Whitfield & Elliot Reyes · September 12, 2026 · Amalfi Coast</p>
<p class="foot__fine">With love and gratitude — we cannot wait to celebrate with you.</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 photo viewer">×</button>
<button class="lightbox__nav lightbox__nav--prev" id="lbPrev" aria-label="Previous photo">‹</button>
<figure class="lightbox__figure">
<div class="lightbox__img" id="lbImg"></div>
<figcaption class="lightbox__cap" id="lbCap"></figcaption>
</figure>
<button class="lightbox__nav lightbox__nav--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>An elegant single-page wedding celebration site for a fictional couple, Amara & Elliot. The hero pairs an oversized Cormorant Garamond serif treatment with a live countdown that ticks toward the wedding date, thin gold monogram rules, and a scroll cue that eases the guest into the story below.
The page flows through romantic sections: a two-column “our story” timeline, a filterable photo gallery built from CSS gradient tiles that open in a keyboard-accessible lightbox, and a details grid of cards covering the ceremony, reception, dress code, and travel. Reveal-on-scroll animations bring each block in gently, and hover states lift the cards with soft gold shadows.
The RSVP form anchors the page: guests enter their name and email, use a plus/minus stepper to set the number of attendees, choose a meal preference, and toggle attendance. Client-side validation surfaces inline errors, and a small toast confirms the submission — all in vanilla JavaScript with no dependencies.