Photography — Print Store
A gallery-dark fine-art print shop where a grid of full-bleed photographs each offer size and framing choices that instantly recalculate the price. Add prints to a slide-in cart drawer with quantity steppers, a live subtotal, shipping estimate, and a promo line — all rendered from clean vanilla JavaScript with smooth micro-interactions, thin uppercase labels, and a Fraunces display serif for an editorial, museum-shop feel.
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 { 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;
}
.eyebrow {
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--sand);
font-weight: 600;
margin: 0 0 8px;
}
/* ---------- Header ---------- */
.site-head {
position: sticky;
top: 0;
z-index: 30;
display: flex;
align-items: center;
gap: 24px;
padding: 16px 28px;
background: rgba(16, 16, 18, 0.82);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark { color: var(--sand); font-size: 20px; }
.brand-name {
font-family: "Fraunces", Georgia, serif;
font-size: 18px;
font-weight: 600;
letter-spacing: 0.01em;
}
.head-nav {
display: flex;
gap: 22px;
margin-left: auto;
}
.head-nav a {
color: var(--muted);
text-decoration: none;
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 500;
transition: color 0.18s ease;
}
.head-nav a:hover, .head-nav a:focus-visible { color: var(--paper); }
.cart-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 9px 16px;
border: 1px solid var(--line-2);
border-radius: 999px;
background: var(--surface);
color: var(--paper);
font: inherit;
font-size: 12px;
letter-spacing: 0.1em;
text-transform: uppercase;
font-weight: 600;
cursor: pointer;
transition: border-color 0.18s ease, background 0.18s ease, transform 0.12s ease;
}
.cart-btn:hover { border-color: var(--sand); background: var(--surface-2); }
.cart-btn:active { transform: scale(0.97); }
.cart-ico { font-size: 14px; color: var(--sand); }
.cart-count {
min-width: 20px;
height: 20px;
padding: 0 6px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--sand);
color: var(--ink);
border-radius: 999px;
font-size: 11px;
font-weight: 800;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.cart-count.bump { transform: scale(1.4); }
:where(a, button, input):focus-visible {
outline: 2px solid var(--sand);
outline-offset: 2px;
}
/* ---------- Hero ---------- */
.hero {
max-width: 760px;
margin: 0 auto;
padding: 64px 28px 40px;
text-align: center;
}
.hero h1 {
font-family: "Fraunces", Georgia, serif;
font-weight: 400;
font-size: clamp(30px, 5vw, 52px);
line-height: 1.08;
margin: 0 0 16px;
letter-spacing: -0.01em;
}
.hero-sub {
color: var(--muted);
font-size: 15px;
max-width: 520px;
margin: 0 auto;
}
/* ---------- Grid ---------- */
.grid {
max-width: 1180px;
margin: 0 auto;
padding: 20px 28px 90px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 26px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
display: flex;
flex-direction: column;
transition: border-color 0.2s ease, transform 0.2s ease;
}
.card:hover { border-color: var(--line-2); transform: translateY(-3px); }
.card-media {
position: relative;
aspect-ratio: 4 / 5;
background-size: cover;
background-position: center;
}
.card-media::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 55%, rgba(11, 11, 12, 0.55));
}
.card-badge {
position: absolute;
top: 14px;
left: 14px;
z-index: 2;
padding: 5px 10px;
background: rgba(11, 11, 12, 0.6);
border: 1px solid var(--line-2);
border-radius: 999px;
font-size: 10px;
letter-spacing: 0.14em;
text-transform: uppercase;
font-weight: 600;
color: var(--sand);
backdrop-filter: blur(4px);
}
.card-edition {
position: absolute;
bottom: 14px;
left: 16px;
z-index: 2;
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--paper);
}
.card-body { padding: 18px 18px 20px; display: flex; flex-direction: column; gap: 14px; }
.card-title {
font-family: "Fraunces", Georgia, serif;
font-size: 20px;
font-weight: 600;
margin: 0;
}
.card-meta {
font-size: 12px;
color: var(--muted);
letter-spacing: 0.04em;
margin: -6px 0 0;
}
.opt-label {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
margin: 0 0 8px;
}
.pill-row { display: flex; flex-wrap: wrap; gap: 8px; }
.pill {
padding: 7px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: transparent;
color: var(--paper);
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
}
.pill small { display: block; font-size: 9px; font-weight: 500; color: var(--muted); letter-spacing: 0.04em; }
.pill:hover { border-color: var(--sand); }
.pill[aria-pressed="true"] {
background: var(--paper);
color: var(--ink);
border-color: var(--paper);
}
.pill[aria-pressed="true"] small { color: rgba(11, 11, 12, 0.6); }
.card-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 4px;
}
.price {
font-family: "Fraunces", Georgia, serif;
font-size: 24px;
font-weight: 600;
}
.price .price-cur { font-size: 15px; color: var(--muted); margin-right: 1px; }
.add-btn {
padding: 11px 18px;
border: none;
border-radius: var(--r-sm);
background: var(--sand);
color: var(--ink);
font: inherit;
font-size: 12px;
letter-spacing: 0.1em;
text-transform: uppercase;
font-weight: 700;
cursor: pointer;
transition: background 0.16s ease, transform 0.12s ease;
}
.add-btn:hover { background: var(--white); }
.add-btn:active { transform: scale(0.96); }
/* ---------- Drawer ---------- */
.scrim {
position: fixed;
inset: 0;
z-index: 40;
background: rgba(6, 6, 7, 0.6);
opacity: 0;
animation: fade 0.25s ease forwards;
}
.scrim[hidden] { display: none; }
@keyframes fade { to { opacity: 1; } }
.drawer {
position: fixed;
top: 0;
right: 0;
z-index: 50;
width: min(420px, 92vw);
height: 100%;
background: var(--surface);
border-left: 1px solid var(--line-2);
transform: translateX(102%);
transition: transform 0.34s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
}
.drawer.open { transform: translateX(0); box-shadow: -30px 0 60px rgba(0, 0, 0, 0.5); }
.drawer-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 22px 24px;
border-bottom: 1px solid var(--line);
}
.drawer-head h2 {
font-family: "Fraunces", Georgia, serif;
font-size: 24px;
font-weight: 600;
margin: 0;
}
.icon-close {
width: 34px;
height: 34px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: transparent;
color: var(--paper);
font-size: 14px;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease;
}
.icon-close:hover { border-color: var(--sand); background: var(--surface-2); }
.drawer-body {
flex: 1;
overflow-y: auto;
padding: 12px 24px;
}
.line {
display: grid;
grid-template-columns: 62px 1fr;
gap: 14px;
padding: 18px 0;
border-bottom: 1px solid var(--line);
animation: lineIn 0.3s ease;
}
@keyframes lineIn { from { opacity: 0; transform: translateY(8px); } }
.line-thumb {
width: 62px;
height: 76px;
border-radius: var(--r-sm);
background-size: cover;
background-position: center;
border: 1px solid var(--line);
}
.line-info { display: flex; flex-direction: column; gap: 6px; }
.line-title { font-weight: 600; font-size: 14px; }
.line-spec {
font-size: 11px;
color: var(--muted);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.line-ctrls { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }
.stepper {
display: inline-flex;
align-items: center;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
overflow: hidden;
}
.stepper button {
width: 30px;
height: 30px;
border: none;
background: transparent;
color: var(--paper);
font-size: 16px;
cursor: pointer;
transition: background 0.14s ease;
}
.stepper button:hover { background: var(--surface-2); }
.stepper span {
min-width: 34px;
text-align: center;
font-size: 13px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.line-right { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.line-total { font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; }
.line-remove {
border: none;
background: transparent;
color: var(--muted);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
cursor: pointer;
padding: 0;
transition: color 0.14s ease;
}
.line-remove:hover { color: #e6857a; }
.drawer-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
color: var(--muted);
text-align: center;
padding: 24px;
}
.drawer-empty[hidden] { display: none; }
.empty-ico { font-size: 40px; color: var(--line-2); }
.ghost-btn {
padding: 10px 18px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: transparent;
color: var(--paper);
font: inherit;
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 600;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease;
}
.ghost-btn:hover { border-color: var(--sand); background: var(--surface-2); }
.drawer-foot {
padding: 20px 24px 24px;
border-top: 1px solid var(--line);
background: var(--surface-2);
}
.drawer-foot[hidden] { display: none; }
.promo { display: flex; gap: 8px; margin-bottom: 18px; }
.promo input {
flex: 1;
padding: 10px 12px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
color: var(--paper);
font: inherit;
font-size: 13px;
}
.promo input::placeholder { color: var(--muted); }
.totals { margin: 0 0 18px; }
.totals-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
color: var(--muted);
}
.totals-row dt, .totals-row dd { margin: 0; }
.totals-row dd { font-variant-numeric: tabular-nums; color: var(--paper); }
.totals-row.discount dd { color: var(--sand); }
.totals-row.discount[hidden] { display: none; }
.totals-row.grand {
border-top: 1px solid var(--line);
margin-top: 4px;
padding-top: 14px;
}
.totals-row.grand dt {
font-family: "Fraunces", Georgia, serif;
font-size: 17px;
font-weight: 600;
color: var(--paper);
}
.totals-row.grand dd {
font-family: "Fraunces", Georgia, serif;
font-size: 20px;
font-weight: 600;
}
.checkout-btn {
width: 100%;
padding: 15px;
border: none;
border-radius: var(--r-sm);
background: var(--paper);
color: var(--ink);
font: inherit;
font-size: 13px;
letter-spacing: 0.12em;
text-transform: uppercase;
font-weight: 800;
cursor: pointer;
transition: background 0.16s ease, transform 0.12s ease;
}
.checkout-btn:hover { background: var(--white); }
.checkout-btn:active { transform: scale(0.98); }
.foot-note {
text-align: center;
font-size: 11px;
color: var(--muted);
margin: 12px 0 0;
letter-spacing: 0.04em;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
bottom: 26px;
left: 50%;
transform: translate(-50%, 20px);
z-index: 60;
padding: 13px 22px;
background: var(--paper);
color: var(--ink);
border-radius: 999px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.site-head { padding: 14px 16px; gap: 12px; }
.head-nav { display: none; }
.cart-label { display: none; }
.hero { padding: 44px 18px 28px; }
.grid { padding: 16px 16px 70px; grid-template-columns: 1fr; gap: 20px; }
.drawer { width: 100vw; }
.card-foot { flex-direction: column; align-items: stretch; }
.add-btn { width: 100%; }
}/* Aperture Editions — fine art print store
Vanilla JS, single in-memory state array, re-render on change. */
(function () {
"use strict";
/* ---------- Data ---------- */
const SIZES = [
{ id: "a4", label: "A4", dim: "21×30cm", mult: 1 },
{ id: "a3", label: "A3", dim: "30×42cm", mult: 1.7 },
{ id: "a2", label: "A2", dim: "42×59cm", mult: 2.6 },
{ id: "a1", label: "A1", dim: "59×84cm", mult: 4.1 },
];
const FRAMES = [
{ id: "none", label: "Unframed", add: 0 },
{ id: "oak", label: "Natural Oak", add: 65 },
{ id: "black", label: "Matte Black", add: 55 },
];
const PRINTS = [
{ id: "dunes", title: "Saharan Dunes", author: "Lina Marchetti", badge: "New", edition: "Edition of 50", base: 48,
img: "https://images.unsplash.com/photo-1509316785289-025f5b846b35?auto=format&fit=crop&w=800&q=80" },
{ id: "fjord", title: "Nordic Fjord", author: "Kasper Holt", badge: "Bestseller", edition: "Edition of 35", base: 62,
img: "https://images.unsplash.com/photo-1483347756197-71ef80e95f73?auto=format&fit=crop&w=800&q=80" },
{ id: "canyon", title: "Red Canyon Light", author: "Sofia Reyes", badge: "Limited", edition: "Edition of 25", base: 74,
img: "https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?auto=format&fit=crop&w=800&q=80" },
{ id: "mist", title: "Forest in Mist", author: "Jonas Frei", badge: "New", edition: "Edition of 50", base: 45,
img: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=800&q=80" },
{ id: "wave", title: "Pacific Break", author: "Ana Okoye", badge: "Bestseller", edition: "Edition of 40", base: 58,
img: "https://images.unsplash.com/photo-1505142468610-359e7d316be0?auto=format&fit=crop&w=800&q=80" },
{ id: "peaks", title: "Alpine Silence", author: "Rúben Costa", badge: "Limited", edition: "Edition of 20", base: 80,
img: "https://images.unsplash.com/photo-1454496522488-7a8e488e8606?auto=format&fit=crop&w=800&q=80" },
];
const SHIP_FRAMED = 18;
const PROMOS = { GALLERY10: 0.10, STUDIO20: 0.20 };
/* per-card current selection */
const selection = {};
PRINTS.forEach((p) => { selection[p.id] = { size: "a3", frame: "none" }; });
/* cart line items */
let cart = [];
let promo = null;
/* ---------- Helpers ---------- */
const $ = (sel, root) => (root || document).querySelector(sel);
const money = (n) => "$" + Math.round(n).toLocaleString("en-US");
function unitPrice(print, sizeId, frameId) {
const size = SIZES.find((s) => s.id === sizeId);
const frame = FRAMES.find((f) => f.id === frameId);
return print.base * size.mult + frame.add;
}
let toastTimer;
function toast(msg) {
const el = $("#toast");
el.textContent = msg;
el.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove("show"), 2200);
}
/* ---------- Render catalogue ---------- */
function renderGrid() {
const grid = $("#grid");
grid.innerHTML = "";
PRINTS.forEach((print) => {
const sel = selection[print.id];
const card = document.createElement("article");
card.className = "card";
const sizePills = SIZES.map((s) =>
`<button class="pill" role="button" data-kind="size" data-print="${print.id}" data-val="${s.id}" aria-pressed="${s.id === sel.size}">${s.label}<small>${s.dim}</small></button>`
).join("");
const framePills = FRAMES.map((f) =>
`<button class="pill" data-kind="frame" data-print="${print.id}" data-val="${f.id}" aria-pressed="${f.id === sel.frame}">${f.label}${f.add ? `<small>+${money(f.add)}</small>` : `<small>incl.</small>`}</button>`
).join("");
card.innerHTML = `
<div class="card-media" style="background-image:url('${print.img}')">
<span class="card-badge">${print.badge}</span>
<span class="card-edition">${print.edition}</span>
</div>
<div class="card-body">
<div>
<h3 class="card-title">${print.title}</h3>
<p class="card-meta">by ${print.author}</p>
</div>
<div>
<p class="opt-label">Size</p>
<div class="pill-row" data-row="size">${sizePills}</div>
</div>
<div>
<p class="opt-label">Framing</p>
<div class="pill-row" data-row="frame">${framePills}</div>
</div>
<div class="card-foot">
<div class="price"><span class="price-cur">$</span><span data-price="${print.id}"></span></div>
<button class="add-btn" data-add="${print.id}">Add to cart</button>
</div>
</div>`;
grid.appendChild(card);
updatePrice(print.id);
});
}
function updatePrice(printId) {
const print = PRINTS.find((p) => p.id === printId);
const sel = selection[printId];
const el = document.querySelector(`[data-price="${printId}"]`);
if (el) el.textContent = Math.round(unitPrice(print, sel.size, sel.frame)).toLocaleString("en-US");
}
/* ---------- Grid interactions ---------- */
$("#grid").addEventListener("click", (e) => {
const pill = e.target.closest(".pill");
if (pill) {
const { kind, print, val } = pill.dataset;
selection[print][kind] = val;
const row = pill.parentElement;
row.querySelectorAll(".pill").forEach((p) => p.setAttribute("aria-pressed", p === pill ? "true" : "false"));
updatePrice(print);
return;
}
const addBtn = e.target.closest("[data-add]");
if (addBtn) addToCart(addBtn.dataset.add);
});
/* ---------- Cart logic ---------- */
function addToCart(printId) {
const print = PRINTS.find((p) => p.id === printId);
const sel = selection[printId];
const key = `${printId}-${sel.size}-${sel.frame}`;
const existing = cart.find((l) => l.key === key);
if (existing) {
existing.qty += 1;
} else {
cart.push({
key, printId, img: print.img, title: print.title,
size: sel.size, frame: sel.frame,
unit: unitPrice(print, sel.size, sel.frame), qty: 1,
});
}
const size = SIZES.find((s) => s.id === sel.size);
toast(`${print.title} · ${size.label} added`);
bumpCount();
renderCart();
openDrawer();
}
function bumpCount() {
const badge = $("#cartCount");
badge.classList.remove("bump");
void badge.offsetWidth;
badge.classList.add("bump");
}
function renderCart() {
const wrap = $("#cartItems");
const empty = $("#cartEmpty");
const foot = $("#cartFoot");
const count = cart.reduce((n, l) => n + l.qty, 0);
$("#cartCount").textContent = count;
if (!cart.length) {
wrap.innerHTML = "";
empty.hidden = false;
foot.hidden = true;
return;
}
empty.hidden = true;
foot.hidden = false;
wrap.innerHTML = cart.map((l) => {
const size = SIZES.find((s) => s.id === l.size);
const frame = FRAMES.find((f) => f.id === l.frame);
return `
<div class="line">
<div class="line-thumb" style="background-image:url('${l.img}')"></div>
<div class="line-info">
<span class="line-title">${l.title}</span>
<span class="line-spec">${size.label} · ${size.dim} · ${frame.label}</span>
<div class="line-ctrls">
<div class="stepper" role="group" aria-label="Quantity for ${l.title}">
<button data-step="dec" data-key="${l.key}" aria-label="Decrease quantity">−</button>
<span>${l.qty}</span>
<button data-step="inc" data-key="${l.key}" aria-label="Increase quantity">+</button>
</div>
<div class="line-right">
<span class="line-total">${money(l.unit * l.qty)}</span>
<button class="line-remove" data-remove="${l.key}">Remove</button>
</div>
</div>
</div>
</div>`;
}).join("");
renderTotals();
}
function renderTotals() {
const subtotal = cart.reduce((n, l) => n + l.unit * l.qty, 0);
const hasFramed = cart.some((l) => l.frame !== "none");
const shipping = hasFramed ? SHIP_FRAMED : 0;
const discRow = $("#discountRow");
let discount = 0;
if (promo && PROMOS[promo]) {
discount = subtotal * PROMOS[promo];
discRow.hidden = false;
$("#promoTag").textContent = promo;
$("#discount").textContent = "-" + money(discount);
} else {
discRow.hidden = true;
}
$("#subtotal").textContent = money(subtotal);
$("#shipping").textContent = shipping ? money(shipping) : "Free";
$("#grandTotal").textContent = money(subtotal + shipping - discount);
}
$("#cartItems").addEventListener("click", (e) => {
const step = e.target.closest("[data-step]");
if (step) {
const line = cart.find((l) => l.key === step.dataset.key);
if (!line) return;
line.qty += step.dataset.step === "inc" ? 1 : -1;
if (line.qty <= 0) cart = cart.filter((l) => l !== line);
renderCart();
return;
}
const rm = e.target.closest("[data-remove]");
if (rm) {
cart = cart.filter((l) => l.key !== rm.dataset.remove);
toast("Removed from cart");
renderCart();
}
});
/* ---------- Promo ---------- */
$("#applyPromo").addEventListener("click", () => {
const code = $("#promoInput").value.trim().toUpperCase();
if (!cart.length) { toast("Cart is empty"); return; }
if (PROMOS[code]) {
promo = code;
toast(`Promo ${code} applied · ${PROMOS[code] * 100}% off`);
} else {
promo = null;
toast("Invalid promo code");
}
$("#promoInput").value = "";
renderTotals();
});
$("#checkout").addEventListener("click", () => {
toast("Demo checkout — order placed. Thank you!");
});
/* ---------- Drawer ---------- */
const drawer = $("#cartDrawer");
const scrim = $("#scrim");
function openDrawer() {
scrim.hidden = false;
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
$("#closeCart").focus();
}
function closeDrawer() {
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
scrim.hidden = true;
$("#cartBtn").focus();
}
$("#cartBtn").addEventListener("click", openDrawer);
$("#closeCart").addEventListener("click", closeDrawer);
scrim.addEventListener("click", closeDrawer);
$("#emptyBrowse").addEventListener("click", () => {
closeDrawer();
document.getElementById("grid").scrollIntoView();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && drawer.classList.contains("open")) closeDrawer();
});
/* ---------- Init ---------- */
renderGrid();
renderCart();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aperture Editions — Fine Art Prints</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 class="site-head">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◐</span>
<span class="brand-name">Aperture Editions</span>
</div>
<nav class="head-nav" aria-label="Primary">
<a href="#grid">Prints</a>
<a href="#grid">Series</a>
<a href="#grid">About</a>
</nav>
<button class="cart-btn" id="cartBtn" aria-haspopup="dialog" aria-controls="cartDrawer">
<span class="cart-ico" aria-hidden="true">▤</span>
<span class="cart-label">Cart</span>
<span class="cart-count" id="cartCount" aria-live="polite">0</span>
</button>
</header>
<section class="hero">
<p class="eyebrow">Fine Art Editions · Archival Giclée</p>
<h1>Prints for walls that mean something.</h1>
<p class="hero-sub">Museum-grade pigment prints on 310gsm cotton rag. Choose a size, pick a frame, and we hand-finish each edition in the studio.</p>
</section>
<main class="grid" id="grid" aria-label="Print catalogue"></main>
<div class="scrim" id="scrim" hidden></div>
<aside class="drawer" id="cartDrawer" role="dialog" aria-modal="true" aria-label="Shopping cart" aria-hidden="true">
<div class="drawer-head">
<div>
<p class="eyebrow">Your Selection</p>
<h2 id="drawerTitle">Cart</h2>
</div>
<button class="icon-close" id="closeCart" aria-label="Close cart">✕</button>
</div>
<div class="drawer-body" id="cartItems"></div>
<div class="drawer-empty" id="cartEmpty">
<span class="empty-ico" aria-hidden="true">◇</span>
<p>Your cart is empty.</p>
<button class="ghost-btn" id="emptyBrowse">Browse prints</button>
</div>
<div class="drawer-foot" id="cartFoot" hidden>
<div class="promo">
<input type="text" id="promoInput" placeholder="Promo code" aria-label="Promo code" />
<button class="ghost-btn" id="applyPromo">Apply</button>
</div>
<dl class="totals">
<div class="totals-row"><dt>Subtotal</dt><dd id="subtotal">$0</dd></div>
<div class="totals-row"><dt>Framed shipping</dt><dd id="shipping">$0</dd></div>
<div class="totals-row discount" id="discountRow" hidden><dt>Promo <span id="promoTag"></span></dt><dd id="discount">-$0</dd></div>
<div class="totals-row grand"><dt>Total</dt><dd id="grandTotal">$0</dd></div>
</dl>
<button class="checkout-btn" id="checkout">Checkout</button>
<p class="foot-note">Free returns within 30 days · Ships in 3–5 days</p>
</div>
</aside>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A dark, image-first print shop built for a fictional fine-art studio. The landing grid shows full-bleed photographs, each with its own inline configurator: a row of size pills (A4 through A1) and a framing toggle (unframed, natural oak, matte black). Choosing a size or frame recomputes the displayed price in real time, so shoppers always see exactly what a given configuration costs before committing.
The add-to-cart flow opens a slide-in drawer from the right. Each line item carries a thumbnail, its chosen size and frame, quantity steppers, and a per-line total. The drawer keeps a running subtotal, adds a flat framed-shipping estimate, and applies a demo promo code, updating every figure as quantities change or items are removed. A floating cart button in the header shows a live count badge.
Everything runs on vanilla JavaScript with no dependencies. State lives in a single in-memory array; the UI re-renders from that state on every change. Interactions include keyboard-accessible controls, focus-visible outlines, a lightweight toast helper for confirmations, and a scrim that closes the drawer on click or Escape — all wrapped in the collection’s gallery-dark palette with generous whitespace and thin uppercase labels.