Photography — Session Types
A gallery-dark grid of six photography session cards — wedding, portrait, family, product, event and real estate — each with a line icon, a short blurb, a from-price badge and a full-bleed image that reveals on hover. Cards lift on hover, respond to keyboard focus and open a compact detail panel with duration, deliverables and a booking action, all driven by lightweight vanilla JS.
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;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(201, 183, 156, 0.10), transparent 60%),
var(--bg);
color: var(--paper);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
.page {
max-width: 1120px;
margin: 0 auto;
padding: 56px 24px 96px;
}
/* ---- Masthead ---- */
.masthead { max-width: 640px; margin-bottom: 44px; }
.eyebrow {
margin: 0 0 14px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--sand);
}
.masthead h1 {
margin: 0 0 16px;
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: clamp(2.1rem, 5vw, 3.1rem);
line-height: 1.06;
letter-spacing: -0.01em;
}
.lede {
margin: 0;
color: var(--muted);
font-size: 1.02rem;
max-width: 52ch;
}
/* ---- Grid ---- */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.card {
position: relative;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 260px;
padding: 22px;
text-align: left;
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: var(--surface);
color: var(--paper);
cursor: pointer;
overflow: hidden;
isolation: isolate;
font: inherit;
transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1),
box-shadow 0.35s ease, border-color 0.35s ease;
}
/* full-bleed reveal image */
.card__img {
position: absolute;
inset: 0;
z-index: -2;
background-size: cover;
background-position: center;
opacity: 0;
transform: scale(1.08);
transition: opacity 0.5s ease, transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}
.card__scrim {
position: absolute;
inset: 0;
z-index: -1;
background: linear-gradient(180deg, rgba(11, 11, 12, 0.35) 0%, rgba(11, 11, 12, 0.82) 70%, rgba(11, 11, 12, 0.94) 100%);
opacity: 0;
transition: opacity 0.5s ease;
}
.card:hover,
.card:focus-visible {
transform: translateY(-8px);
border-color: var(--line-2);
box-shadow: 0 22px 48px -20px rgba(0, 0, 0, 0.8);
outline: none;
}
.card:focus-visible { box-shadow: 0 0 0 2px var(--sand), 0 22px 48px -20px rgba(0, 0, 0, 0.8); }
.card:hover .card__img,
.card:focus-visible .card__img { opacity: 0.5; transform: scale(1); }
.card:hover .card__scrim,
.card:focus-visible .card__scrim { opacity: 1; }
.card:active { transform: translateY(-4px); }
.card__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.card__icon {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: var(--r-md);
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--sand);
transition: background 0.35s ease, color 0.35s ease, border-color 0.35s ease;
}
.card__icon svg { width: 22px; height: 22px; }
.card:hover .card__icon,
.card:focus-visible .card__icon {
background: var(--sand);
color: var(--ink);
border-color: var(--sand);
}
.badge {
align-self: flex-start;
padding: 6px 11px;
border-radius: 999px;
border: 1px solid var(--line-2);
background: rgba(11, 11, 12, 0.45);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
white-space: nowrap;
backdrop-filter: blur(4px);
}
.badge b { color: var(--sand); font-weight: 700; }
.card__label {
margin: 0;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--sand);
}
.card__body { margin-top: auto; display: flex; flex-direction: column; gap: 8px; }
.card__title {
margin: 0;
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: 1.5rem;
line-height: 1.1;
}
.card__blurb {
margin: 0;
font-size: 0.92rem;
color: var(--muted);
}
.card:hover .card__blurb,
.card:focus-visible .card__blurb { color: var(--paper); }
.card__cta {
display: inline-flex;
align-items: center;
gap: 7px;
margin-top: 4px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--sand);
opacity: 0;
transform: translateX(-6px);
transition: opacity 0.35s ease, transform 0.35s ease;
}
.card:hover .card__cta,
.card:focus-visible .card__cta { opacity: 1; transform: translateX(0); }
.card__cta svg { width: 15px; height: 15px; }
/* ---- Detail panel ---- */
.detail {
position: relative;
margin-top: 28px;
padding: 30px 32px;
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
background: linear-gradient(180deg, var(--surface-2), var(--surface));
box-shadow: 0 24px 60px -30px rgba(0, 0, 0, 0.9);
animation: rise 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
.detail__close {
position: absolute;
top: 16px;
right: 16px;
width: 34px;
height: 34px;
border-radius: 999px;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--paper);
font-size: 20px;
line-height: 1;
cursor: pointer;
transition: background 0.25s ease, color 0.25s ease;
}
.detail__close:hover { background: var(--sand); color: var(--ink); }
.detail__close:focus-visible { outline: 2px solid var(--sand); outline-offset: 2px; }
.detail__label {
margin: 0 0 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--sand);
}
.detail__title {
margin: 0 0 8px;
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: 1.9rem;
}
.detail__blurb { margin: 0 0 22px; color: var(--muted); max-width: 60ch; }
.detail__meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin: 0 0 24px;
padding: 0;
}
.detail__meta > div {
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: rgba(11, 11, 12, 0.4);
}
.detail__meta dt {
margin: 0 0 4px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--sand-d);
}
.detail__meta dd { margin: 0; font-size: 0.98rem; font-weight: 500; }
.detail__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding-top: 20px;
border-top: 1px solid var(--line);
}
.detail__price { font-size: 1.15rem; font-weight: 600; }
.detail__price b { color: var(--sand); }
.btn {
font: inherit;
font-weight: 600;
cursor: pointer;
border-radius: var(--r-md);
padding: 12px 22px;
border: 1px solid var(--line-2);
background: transparent;
color: var(--paper);
transition: transform 0.15s ease, background 0.25s ease, color 0.25s ease;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--sand); outline-offset: 2px; }
.btn--primary { background: var(--sand); color: var(--ink); border-color: var(--sand); }
.btn--primary:hover { background: var(--white); border-color: var(--white); }
/* ---- Toast ---- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
padding: 13px 20px;
border-radius: 999px;
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--paper);
font-size: 0.9rem;
font-weight: 500;
box-shadow: 0 16px 40px -16px rgba(0, 0, 0, 0.9);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 50;
}
.toast.is-visible { opacity: 1; transform: translate(-50%, 0); }
.toast b { color: var(--sand); }
/* ---- Responsive ---- */
@media (max-width: 900px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.page { padding: 40px 16px 72px; }
.grid { grid-template-columns: 1fr; gap: 16px; }
.card { min-height: 220px; }
.card__img { opacity: 0.42; transform: scale(1); }
.card__scrim { opacity: 1; }
.card__blurb { color: var(--paper); }
.detail { padding: 24px 20px; }
.detail__meta { grid-template-columns: 1fr; gap: 12px; }
.detail__foot { flex-direction: column; align-items: stretch; }
.btn--primary { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
* { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
}/* Aperture Studio — Session Types
Vanilla JS: renders session cards, opens a detail panel, toasts on booking. */
(function () {
"use strict";
var ICONS = {
rings:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8.5" cy="14" r="5.5"/><circle cx="15.5" cy="14" r="5.5"/><path d="M8.5 8.5 10 4.5h4l1.5 4"/></svg>',
portrait:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-6.5 8-6.5s8 2.5 8 6.5"/></svg>',
family:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="8" r="3"/><circle cx="16.5" cy="8" r="3"/><path d="M2.5 20c0-3 2.2-5 5-5s5 2 5 5M11.5 20c0-3 2.2-5 5-5s5 2 5 5"/></svg>',
product:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="m12 2 8 4.5v9L12 20l-8-4.5v-9L12 2z"/><path d="m4 6.5 8 4.5 8-4.5M12 11v9"/></svg>',
event:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V4m8 3V4M3 11h18"/></svg>',
realestate:
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11 12 4l8 7"/><path d="M6 10v9h12v-9"/><path d="M10 19v-5h4v5"/></svg>',
};
var ARROW =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>';
var SESSIONS = [
{
id: "wedding",
label: "Full Day",
title: "Wedding",
icon: "rings",
from: 2400,
blurb: "Candid, unhurried coverage from first look to last dance.",
duration: "8–10 hours",
deliverables: "600+ edited images",
turnaround: "4–6 weeks",
img: "https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=800&q=60",
},
{
id: "portrait",
label: "Studio",
title: "Portrait",
icon: "portrait",
from: 320,
blurb: "Editorial headshots and personal branding under soft light.",
duration: "60–90 minutes",
deliverables: "25 edited images",
turnaround: "5–7 days",
img: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?auto=format&fit=crop&w=800&q=60",
},
{
id: "family",
label: "On Location",
title: "Family",
icon: "family",
from: 380,
blurb: "Warm, playful sessions at home or your favourite trail.",
duration: "90 minutes",
deliverables: "40 edited images",
turnaround: "1–2 weeks",
img: "https://images.unsplash.com/photo-1511895426328-dc8714191300?auto=format&fit=crop&w=800&q=60",
},
{
id: "product",
label: "Commercial",
title: "Product",
icon: "product",
from: 540,
blurb: "Clean e-commerce and lifestyle shots that convert.",
duration: "Half day",
deliverables: "Up to 20 SKUs",
turnaround: "3–5 days",
img: "https://images.unsplash.com/photo-1523275335684-37898b6baf30?auto=format&fit=crop&w=800&q=60",
},
{
id: "event",
label: "Coverage",
title: "Event",
icon: "event",
from: 650,
blurb: "Conferences, launches and parties documented start to finish.",
duration: "4 hours",
deliverables: "300+ edited images",
turnaround: "48 hours",
img: "https://images.unsplash.com/photo-1492684223066-81342ee5ff30?auto=format&fit=crop&w=800&q=60",
},
{
id: "realestate",
label: "Property",
title: "Real Estate",
icon: "realestate",
from: 210,
blurb: "Bright, wide interiors and drone exteriors that sell listings.",
duration: "1–2 hours",
deliverables: "30 HDR images",
turnaround: "24 hours",
img: "https://images.unsplash.com/photo-1560518883-ce09059eeffa?auto=format&fit=crop&w=800&q=60",
},
];
var grid = document.getElementById("grid");
var detail = document.getElementById("detail");
var toastEl = document.getElementById("toast");
var activeId = null;
var toastTimer;
function money(n) {
return "$" + n.toLocaleString("en-US");
}
function toast(msg) {
toastEl.innerHTML = msg;
toastEl.classList.add("is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 3200);
}
/* ---- render cards ---- */
SESSIONS.forEach(function (s) {
var card = document.createElement("button");
card.type = "button";
card.className = "card";
card.setAttribute("data-id", s.id);
card.setAttribute(
"aria-label",
s.title + " session, from " + money(s.from) + ". View details."
);
card.innerHTML =
'<div class="card__img" style="background-image:url(' + s.img + ')"></div>' +
'<div class="card__scrim"></div>' +
'<div class="card__top">' +
'<span class="card__icon">' + ICONS[s.icon] + "</span>" +
'<span class="badge">from <b>' + money(s.from) + "</b></span>" +
"</div>" +
'<p class="card__label">' + s.label + "</p>" +
'<div class="card__body">' +
'<h3 class="card__title">' + s.title + "</h3>" +
'<p class="card__blurb">' + s.blurb + "</p>" +
'<span class="card__cta">View details ' + ARROW + "</span>" +
"</div>";
card.addEventListener("click", function () {
openDetail(s.id);
});
grid.appendChild(card);
});
/* ---- detail panel ---- */
function openDetail(id) {
var s = SESSIONS.find(function (x) {
return x.id === id;
});
if (!s) return;
activeId = id;
document.getElementById("detailLabel").textContent = s.label + " Session";
document.getElementById("detailTitle").textContent = s.title;
document.getElementById("detailBlurb").textContent = s.blurb;
document.getElementById("detailPrice").innerHTML =
"Starting <b>" + money(s.from) + "</b>";
document.getElementById("detailMeta").innerHTML =
metaBlock("Duration", s.duration) +
metaBlock("Deliverables", s.deliverables) +
metaBlock("Turnaround", s.turnaround);
detail.hidden = false;
detail.scrollIntoView({ behavior: "smooth", block: "nearest" });
// highlight source card via aria-expanded semantics
var cards = grid.querySelectorAll(".card");
cards.forEach(function (c) {
c.setAttribute(
"aria-pressed",
c.getAttribute("data-id") === id ? "true" : "false"
);
});
}
function metaBlock(term, val) {
return "<div><dt>" + term + "</dt><dd>" + val + "</dd></div>";
}
document.getElementById("detailClose").addEventListener("click", function () {
detail.hidden = true;
activeId = null;
});
document.getElementById("bookBtn").addEventListener("click", function () {
var s = SESSIONS.find(function (x) {
return x.id === activeId;
});
if (!s) return;
toast("Request sent for <b>" + s.title + "</b> — we'll email you within 24h.");
});
/* Escape closes the panel */
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !detail.hidden) {
detail.hidden = true;
activeId = null;
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aperture Studio — Session Types</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>
<div class="page">
<header class="masthead">
<p class="eyebrow">Aperture Studio — Portland, OR</p>
<h1>Sessions built around your story</h1>
<p class="lede">Six ways to work together. Pick a session type to see what's included, how long it runs and when your gallery lands.</p>
</header>
<main class="grid" id="grid" aria-label="Photography session types">
<!-- cards injected by script.js -->
</main>
<aside class="detail" id="detail" hidden aria-live="polite">
<button class="detail__close" id="detailClose" aria-label="Close details">×</button>
<p class="detail__label" id="detailLabel"></p>
<h2 class="detail__title" id="detailTitle"></h2>
<p class="detail__blurb" id="detailBlurb"></p>
<dl class="detail__meta" id="detailMeta"></dl>
<div class="detail__foot">
<span class="detail__price" id="detailPrice"></span>
<button class="btn btn--primary" id="bookBtn">Book this session</button>
</div>
</aside>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A dark, image-first grid of session-type cards for a photography studio. Six offerings — Wedding, Portrait, Family, Product, Event and Real Estate — each pair a crisp SVG line icon with a one-line blurb, a thin uppercase category label and a “from $X” price badge. Fraunces display serif carries the headings while Inter handles the body, giving the layout an editorial, gallery feel against the near-black surfaces.
On hover or keyboard focus each card lifts, deepens its shadow and reveals a full-bleed session photograph behind a soft gradient scrim, so the imagery does the selling. Activating a card (click, Enter or Space) opens a compact detail panel summarising typical duration, deliverables and turnaround, plus a booking button that fires a toast confirmation — a realistic stand-in for a real enquiry flow.
Everything is self-contained vanilla JS: no frameworks, no build step. Controls are real buttons with visible focus rings and aria labelling, the grid collapses to a single column below 520px, and the interactions stay smooth with CSS transitions and a tiny toast helper.