Shop — Collection / Lookbook
A magazine-meets-shop collection page for a fictional coastal label. An editorial gradient hero opens onto a styled lookbook of large CSS scenes with pulsing shoppable hotspots that pop a product card with a swatch gallery, rating, stock chip, and add-to-bag. Below sit a six-piece product grid and a shop-the-look capsule that bundles three signature items at a calculated discount. A slide-out bag with quantity steppers and live subtotal ties it all together.
MCP
Code
:root {
--bg: #ffffff;
--paper: #faf7f1;
--ink: #1b1d22;
--muted: #6b6f78;
--brand: #2f5d50;
--brand-d: #224639;
--sand: #e7ddcd;
--sale: #c14a3a;
--ok: #1f9d55;
--line: rgba(27, 29, 34, .1);
--line-2: rgba(27, 29, 34, .16);
--shadow: 0 14px 40px rgba(27, 29, 34, .14);
--shadow-sm: 0 4px 16px rgba(27, 29, 34, .1);
--radius: 16px;
--serif: "Fraunces", Georgia, "Times New Roman", serif;
--sans: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--sans);
color: var(--ink);
background: var(--bg);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; cursor: pointer; }
a { color: inherit; }
.wrap { width: min(1160px, 92vw); margin-inline: auto; }
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 4px;
}
.skip-link {
position: absolute; left: 12px; top: -48px;
background: var(--ink); color: #fff; padding: 8px 14px;
border-radius: 8px; z-index: 80; transition: top .18s ease;
}
.skip-link:focus { top: 12px; }
/* ---------- Buttons ---------- */
.btn {
display: inline-flex; align-items: center; justify-content: center;
gap: 8px; padding: 12px 22px; border-radius: 999px;
font-weight: 600; font-size: .95rem; text-decoration: none;
border: 1px solid transparent; transition: transform .12s ease, background .15s ease, box-shadow .15s ease;
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--brand); color: #fff; }
.btn-primary:hover { background: var(--brand-d); box-shadow: var(--shadow-sm); }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line-2); }
.btn-ghost:hover { background: rgba(27, 29, 34, .04); }
.btn-block { width: 100%; }
/* ---------- Announcement ---------- */
.announce {
background: var(--ink); color: #f5f1e9;
font-size: .82rem; text-align: center; padding: 8px 12px;
}
.announce strong { color: #fff; }
/* ---------- Header ---------- */
.site-header {
position: sticky; top: 0; z-index: 40;
background: rgba(255, 255, 255, .86);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.header-inner {
display: flex; align-items: center; gap: 20px;
padding: 14px 0;
}
.logo { display: inline-flex; align-items: center; gap: 9px; text-decoration: none; color: var(--brand); }
.logo-text { font-family: var(--serif); font-weight: 600; font-size: 1.2rem; letter-spacing: .2px; color: var(--ink); }
.site-nav { display: flex; gap: 26px; margin-left: 14px; }
.site-nav a { text-decoration: none; color: var(--muted); font-size: .92rem; font-weight: 500; transition: color .15s ease; }
.site-nav a:hover { color: var(--ink); }
.cart-btn {
position: relative; margin-left: auto;
display: inline-flex; align-items: center; gap: 7px;
background: var(--paper); border: 1px solid var(--line);
padding: 9px 16px; border-radius: 999px; font-weight: 600; font-size: .9rem;
transition: border-color .15s ease, background .15s ease;
}
.cart-btn:hover { border-color: var(--line-2); }
.cart-count {
min-width: 20px; height: 20px; padding: 0 5px;
display: inline-grid; place-items: center;
background: var(--brand); color: #fff;
border-radius: 999px; font-size: .72rem; font-weight: 700;
}
.cart-count.pulse { animation: pop .4s ease; }
@keyframes pop { 0% { transform: scale(1); } 45% { transform: scale(1.4); } 100% { transform: scale(1); } }
/* ---------- Hero ---------- */
.hero { position: relative; overflow: hidden; background: var(--paper); border-bottom: 1px solid var(--line); }
.hero-art { position: absolute; inset: 0; z-index: 0; }
.hero-sun {
position: absolute; right: 8%; top: 12%;
width: 200px; height: 200px; border-radius: 50%;
background: radial-gradient(circle at 40% 40%, #f4d9a8, #e8b974 60%, #d99e4f);
filter: blur(.5px); opacity: .9;
}
.hero-wave {
position: absolute; left: -10%; right: -10%; height: 240px;
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
}
.hero-wave-1 { bottom: -120px; background: linear-gradient(180deg, #cfe0db, #a9c6bd); opacity: .9; }
.hero-wave-2 { bottom: -160px; background: linear-gradient(180deg, #a9c6bd, #7faa9c); opacity: .85; }
.hero-wave-3 { bottom: -200px; background: linear-gradient(180deg, #5f8e7f, var(--brand)); opacity: .9; }
.hero-inner { position: relative; z-index: 1; padding: 84px 0 96px; max-width: 640px; }
.eyebrow {
margin: 0 0 14px; text-transform: uppercase; letter-spacing: .18em;
font-size: .74rem; font-weight: 700; color: var(--brand);
}
.hero h1 {
font-family: var(--serif); font-weight: 600;
font-size: clamp(2.6rem, 7vw, 4.6rem); line-height: 1.02;
margin: 0 0 18px; letter-spacing: -.5px;
}
.hero-lede { font-size: 1.12rem; color: #3b3e45; max-width: 480px; margin: 0 0 26px; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 38px; }
.hero-meta { display: flex; gap: 40px; margin: 0; }
.hero-meta dt { font-size: .72rem; text-transform: uppercase; letter-spacing: .12em; color: var(--muted); margin-bottom: 2px; }
.hero-meta dd { margin: 0; font-family: var(--serif); font-size: 1.35rem; font-weight: 600; }
/* ---------- Section heads ---------- */
.section-head { max-width: 560px; margin: 0 0 38px; }
.section-head h2 { font-family: var(--serif); font-weight: 600; font-size: clamp(1.7rem, 4vw, 2.5rem); margin: 8px 0 10px; letter-spacing: -.3px; }
.section-sub { color: var(--muted); margin: 0; font-size: 1.02rem; }
/* ---------- Story panels ---------- */
.story { padding: 80px 0; }
.story-grid {
display: grid; gap: 18px;
grid-template-columns: 1.15fr 1fr;
grid-template-rows: auto auto;
}
.panel {
position: relative; margin: 0; border-radius: var(--radius);
overflow: hidden; min-height: 280px;
box-shadow: var(--shadow-sm);
}
.panel-tall { grid-row: span 2; min-height: 100%; }
.panel-art { position: absolute; inset: 0; }
.panel-art svg { width: 100%; height: 100%; }
.art-dune { background: linear-gradient(160deg, #e9c899, #cf9d63 55%, #9b6f3e); }
.art-harbour { background: linear-gradient(160deg, #9db8c4, #5d7f93 60%, #324a5b); }
.art-kitchen { background: linear-gradient(160deg, #e3d6c4, #c9b59a 60%, #9c8366); }
.panel-cap {
position: absolute; left: 18px; bottom: 16px; right: 18px; z-index: 2;
color: #fff; text-shadow: 0 1px 8px rgba(0, 0, 0, .35);
display: flex; align-items: baseline; gap: 10px;
}
.panel-num { font-family: var(--serif); font-size: 1.5rem; font-weight: 600; opacity: .85; }
.panel-title { font-family: var(--serif); font-size: 1.25rem; font-weight: 600; }
/* Hotspots */
.hotspot {
position: absolute; z-index: 3; width: 34px; height: 34px;
border: 0; background: transparent; padding: 0;
display: grid; place-items: center; transform: translate(-50%, -50%);
}
.dot {
width: 16px; height: 16px; border-radius: 50%;
background: #fff; box-shadow: 0 0 0 4px rgba(255, 255, 255, .35), 0 2px 8px rgba(0, 0, 0, .3);
position: relative; transition: transform .15s ease;
}
.dot::after {
content: ""; position: absolute; inset: -6px; border-radius: 50%;
border: 2px solid rgba(255, 255, 255, .7);
animation: pulse-ring 2.2s ease-out infinite;
}
.hotspot:hover .dot, .hotspot:focus-visible .dot { transform: scale(1.25); }
.hotspot[aria-expanded="true"] .dot { background: var(--brand); }
@keyframes pulse-ring {
0% { transform: scale(.7); opacity: .9; }
100% { transform: scale(1.8); opacity: 0; }
}
/* ---------- Product grid ---------- */
.grid-section { padding: 24px 0 80px; }
.product-grid {
display: grid; gap: 20px;
grid-template-columns: repeat(3, 1fr);
}
.card {
border: 1px solid var(--line); border-radius: var(--radius);
overflow: hidden; background: var(--bg);
display: flex; flex-direction: column;
transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow); border-color: transparent; }
.card-art { position: relative; aspect-ratio: 4 / 5; }
.card-art svg { width: 100%; height: 100%; }
.card-badge {
position: absolute; left: 12px; top: 12px;
background: var(--sale); color: #fff;
font-size: .68rem; font-weight: 700; letter-spacing: .04em;
padding: 4px 9px; border-radius: 999px; text-transform: uppercase;
}
.card-body { padding: 16px 16px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
.card-tag { font-size: .72rem; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); }
.card-name { font-family: var(--serif); font-size: 1.12rem; font-weight: 600; margin: 0; }
.card-rating { display: flex; align-items: center; gap: 6px; font-size: .8rem; color: var(--muted); }
.stars { color: #e0a128; letter-spacing: 1px; }
.card-foot { margin-top: auto; display: flex; align-items: center; justify-content: space-between; padding-top: 8px; }
.price { font-weight: 700; font-size: 1.06rem; }
.price .was { color: var(--muted); font-weight: 500; text-decoration: line-through; margin-right: 6px; font-size: .9rem; }
.card-add {
background: var(--ink); color: #fff; border: 0;
width: 38px; height: 38px; border-radius: 50%;
display: grid; place-items: center; font-size: 1.3rem; line-height: 1;
transition: background .15s ease, transform .12s ease;
}
.card-add:hover { background: var(--brand); }
.card-add:active { transform: scale(.92); }
/* ---------- Bundle / shop the look ---------- */
.bundle { padding: 0 0 90px; }
.bundle-inner {
display: grid; grid-template-columns: .9fr 1.1fr; gap: 40px; align-items: center;
background: var(--paper); border: 1px solid var(--line);
border-radius: 24px; padding: 40px; overflow: hidden;
}
.bundle-art { display: grid; place-items: center; min-height: 300px; }
.bundle-stack { position: relative; width: 240px; height: 260px; }
.bundle-chip {
position: absolute; width: 150px; height: 190px; border-radius: 14px;
box-shadow: var(--shadow); border: 4px solid #fff;
}
.bundle-chip[data-b="linen-shirt"] { left: 0; top: 30px; transform: rotate(-7deg); background: linear-gradient(160deg, #ece3d2, #cdbf9f); }
.bundle-chip[data-b="wool-knit"] { left: 45px; top: 0; transform: rotate(2deg); background: linear-gradient(160deg, #b6c7c0, #6f9286); z-index: 2; }
.bundle-chip[data-b="canvas-tote"] { left: 95px; top: 40px; transform: rotate(9deg); background: linear-gradient(160deg, #d8c7a6, #ab8d5d); }
.bundle-copy h2 { font-family: var(--serif); font-weight: 600; font-size: clamp(1.7rem, 4vw, 2.4rem); margin: 8px 0 8px; }
.bundle-list { list-style: none; padding: 0; margin: 18px 0; display: flex; flex-direction: column; gap: 10px; }
.bundle-list li { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: #fff; border: 1px solid var(--line); border-radius: 12px; }
.bundle-swatch { width: 36px; height: 44px; border-radius: 8px; flex: none; }
.bundle-li-name { font-weight: 600; font-size: .95rem; }
.bundle-li-tag { font-size: .78rem; color: var(--muted); }
.bundle-li-price { margin-left: auto; font-weight: 700; }
.bundle-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 16px; margin-top: 8px; }
.bundle-price { display: flex; align-items: baseline; gap: 10px; }
.bundle-was { color: var(--muted); text-decoration: line-through; font-size: 1rem; }
.bundle-now { font-family: var(--serif); font-size: 1.8rem; font-weight: 600; }
.bundle-save { background: rgba(31, 157, 85, .12); color: var(--ok); font-size: .78rem; font-weight: 700; padding: 3px 9px; border-radius: 999px; }
/* ---------- Footer ---------- */
.site-footer { background: var(--ink); color: #d9d4ca; padding: 36px 0; }
.footer-inner { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 12px; }
.footer-brand { font-family: var(--serif); font-size: 1.3rem; color: #fff; margin: 0; }
.footer-note { margin: 0; font-size: .85rem; }
.footer-note span { opacity: .7; }
/* ---------- Product popover ---------- */
.popover-overlay, .drawer-overlay {
position: fixed; inset: 0; z-index: 60;
background: rgba(20, 22, 27, .5); backdrop-filter: blur(2px);
animation: fade .2s ease;
}
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.popover {
position: fixed; z-index: 70; left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: min(680px, 92vw); max-height: 90vh; overflow: auto;
background: var(--bg); border-radius: 20px; box-shadow: var(--shadow);
display: grid; grid-template-columns: 1fr 1fr;
animation: rise .22s cubic-bezier(.2, .8, .3, 1);
}
@keyframes rise { from { opacity: 0; transform: translate(-50%, -46%); } to { opacity: 1; transform: translate(-50%, -50%); } }
.pop-close {
position: absolute; right: 12px; top: 10px; z-index: 2;
width: 34px; height: 34px; border-radius: 50%; border: 0;
background: rgba(255, 255, 255, .8); font-size: 1.5rem; line-height: 1;
display: grid; place-items: center;
}
.pop-close:hover { background: #fff; }
.pop-gallery { padding: 16px; background: var(--paper); border-radius: 20px 0 0 20px; display: flex; flex-direction: column; gap: 12px; }
.pop-art { aspect-ratio: 4 / 5; border-radius: 12px; overflow: hidden; }
.pop-art svg { width: 100%; height: 100%; }
.pop-thumbs { display: flex; gap: 8px; }
.pop-thumb {
width: 46px; height: 56px; border-radius: 8px; border: 2px solid transparent;
padding: 0; overflow: hidden; background: #fff;
}
.pop-thumb svg { width: 100%; height: 100%; }
.pop-thumb[aria-selected="true"] { border-color: var(--brand); }
.pop-body { padding: 28px 26px; display: flex; flex-direction: column; }
.pop-tag { font-size: .74rem; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); margin: 0 0 4px; }
.pop-body h3 { font-family: var(--serif); font-weight: 600; font-size: 1.6rem; margin: 0 0 8px; }
.pop-rating { font-size: .85rem; color: var(--muted); margin-bottom: 10px; }
.pop-price { font-weight: 700; font-size: 1.4rem; margin: 0 0 12px; }
.pop-price .was { color: var(--muted); text-decoration: line-through; font-weight: 500; font-size: 1rem; margin-right: 8px; }
.pop-desc { color: #45474d; font-size: .95rem; margin: 0 0 14px; }
.pop-stock { font-size: .85rem; font-weight: 600; color: var(--ok); margin: 0 0 18px; display: flex; align-items: center; gap: 7px; }
.pop-stock::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--ok); }
.pop-stock.low { color: var(--sale); }
.pop-stock.low::before { background: var(--sale); }
.pop-add { margin-top: auto; }
/* ---------- Cart drawer ---------- */
.drawer {
position: fixed; z-index: 70; top: 0; right: 0; height: 100%;
width: min(420px, 100vw); background: var(--bg);
box-shadow: -10px 0 40px rgba(0, 0, 0, .18);
display: flex; flex-direction: column;
animation: slide-in .24s cubic-bezier(.2, .8, .3, 1);
}
@keyframes slide-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
.drawer-head { display: flex; align-items: center; justify-content: space-between; padding: 20px 22px; border-bottom: 1px solid var(--line); }
.drawer-head h2 { font-family: var(--serif); font-weight: 600; font-size: 1.4rem; margin: 0; }
.drawer-close { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--line); background: var(--bg); font-size: 1.5rem; line-height: 1; }
.drawer-close:hover { background: var(--paper); }
.drawer-body { flex: 1; overflow: auto; padding: 14px 22px; }
.drawer-empty { color: var(--muted); text-align: center; padding: 60px 10px; }
.line {
display: grid; grid-template-columns: 56px 1fr auto; gap: 12px;
padding: 14px 0; border-bottom: 1px solid var(--line); align-items: center;
}
.line-art { width: 56px; height: 68px; border-radius: 10px; }
.line-name { font-weight: 600; font-size: .92rem; }
.line-tag { font-size: .76rem; color: var(--muted); }
.qty { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px; border: 1px solid var(--line); border-radius: 999px; padding: 2px; }
.qty button { width: 26px; height: 26px; border: 0; background: transparent; border-radius: 50%; font-size: 1.1rem; line-height: 1; }
.qty button:hover { background: var(--paper); }
.qty span { min-width: 20px; text-align: center; font-weight: 600; font-size: .9rem; }
.line-right { text-align: right; }
.line-price { font-weight: 700; }
.line-remove { background: none; border: 0; color: var(--muted); font-size: .76rem; text-decoration: underline; padding: 4px 0; }
.line-remove:hover { color: var(--sale); }
.drawer-foot { border-top: 1px solid var(--line); padding: 18px 22px 22px; }
.drawer-sub { display: flex; justify-content: space-between; font-size: 1.05rem; margin-bottom: 4px; }
.drawer-sub strong { font-family: var(--serif); font-size: 1.3rem; }
.drawer-ship { color: var(--muted); font-size: .82rem; margin: 0 0 14px; }
.drawer-secure { display: flex; align-items: center; justify-content: center; gap: 6px; color: var(--muted); font-size: .8rem; margin: 12px 0 0; }
/* ---------- Toast ---------- */
.toast-host { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 90; display: flex; flex-direction: column; gap: 8px; align-items: center; }
.toast {
background: var(--ink); color: #fff; padding: 11px 18px; border-radius: 999px;
font-size: .88rem; font-weight: 500; box-shadow: var(--shadow);
animation: toast-in .22s ease; max-width: 90vw;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
.toast.out { animation: toast-out .25s ease forwards; }
@keyframes toast-out { to { opacity: 0; transform: translateY(10px); } }
/* ---------- Responsive ---------- */
@media (max-width: 920px) {
.product-grid { grid-template-columns: repeat(2, 1fr); }
.bundle-inner { grid-template-columns: 1fr; gap: 28px; padding: 28px; }
.bundle-art { min-height: 230px; }
}
@media (max-width: 760px) {
.site-nav { display: none; }
.story-grid { grid-template-columns: 1fr; }
.panel-tall { grid-row: auto; min-height: 300px; }
.popover { grid-template-columns: 1fr; max-width: 440px; }
.pop-gallery { border-radius: 20px 20px 0 0; }
.hero-inner { padding: 60px 0 80px; }
}
@media (max-width: 480px) {
.product-grid { grid-template-columns: 1fr; }
.hero-meta { gap: 24px; }
.header-inner { gap: 10px; }
.cart-btn span:not(.cart-count) { display: none; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: .001ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto; transition-duration: .001ms !important; }
}(function () {
"use strict";
/* ---------- Inline SVG "product photography" ---------- */
function svgGarment(stops) {
return (
'<svg viewBox="0 0 200 250" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg" role="img">' +
'<defs><linearGradient id="g' + stops.id + '" x1="0" y1="0" x2="1" y2="1">' +
'<stop offset="0" stop-color="' + stops.a + '"/><stop offset="1" stop-color="' + stops.b + '"/></linearGradient></defs>' +
'<rect width="200" height="250" fill="url(#g' + stops.id + ')"/>' +
'<g fill="rgba(255,255,255,.5)" stroke="rgba(27,29,34,.18)" stroke-width="1.5">' +
stops.shape +
"</g></svg>"
);
}
var SHAPES = {
shirt:
'<path d="M70 70 55 84 45 74 70 56h60l25 18-10 10-15-14v110H70Z"/>',
knit:
'<path d="M64 64 50 82l12 12 8-8v100h60V98l8 8 12-12-14-18h-26l-6 8h-8l-6-8Z"/>',
tote:
'<rect x="62" y="92" width="76" height="96" rx="8"/><path d="M82 92c0-22 36-22 36 0" fill="none" stroke-width="6"/>',
mug:
'<rect x="62" y="96" width="58" height="78" rx="10"/><path d="M120 116c26 0 26 42 0 42" fill="none" stroke-width="8"/>',
apron:
'<path d="M78 70h44l4 18-8 6v94H82V94l-8-6Z"/><path d="M88 70 100 56l12 14" fill="none"/>',
};
/* ---------- Catalog ---------- */
var PRODUCTS = {
"linen-shirt": {
name: "Salt Linen Overshirt", tag: "Tops · Unisex", price: 148, was: 178,
rating: 4.8, reviews: 126, stock: "In stock", low: false,
desc: "A breezy washed-linen overshirt with a relaxed shoulder and shell buttons. Throws over everything, gets softer with every wash.",
shape: "shirt", ramps: [["#ece3d2", "#cdbf9f"], ["#e7d8bf", "#bda77f"], ["#f1ead9", "#d8c8a3"]],
},
"wool-knit": {
name: "Fisher Wool Knit", tag: "Knitwear · Unisex", price: 192, was: null,
rating: 4.9, reviews: 88, stock: "Only 4 left", low: true,
desc: "Chunky lambswool sweater knit on the Portuguese coast. Ribbed cuffs, dropped shoulders, the kind of warm you keep forever.",
shape: "knit", ramps: [["#b6c7c0", "#6f9286"], ["#a8bdb4", "#5d8074"], ["#c2d1cb", "#7fa093"]],
},
"canvas-tote": {
name: "Harbour Canvas Tote", tag: "Bags", price: 64, was: null,
rating: 4.7, reviews: 211, stock: "In stock", low: false,
desc: "Heavyweight cotton canvas with a flat leather base and an inner pocket. Carries the market haul and the beach towels alike.",
shape: "tote", ramps: [["#d8c7a6", "#ab8d5d"], ["#e0d2b3", "#b89a6a"], ["#cdb992", "#9c7e4f"]],
},
"stoneware-mug": {
name: "Tide Stoneware Mug", tag: "Home · Ceramics", price: 28, was: 36,
rating: 4.9, reviews: 304, stock: "In stock", low: false,
desc: "Hand-thrown stoneware in a speckled tide glaze, no two alike. Holds a generous pour and feels good in cold hands.",
shape: "mug", ramps: [["#cfe0db", "#7faa9c"], ["#d8e6e1", "#8db5a8"], ["#c3d6d0", "#6f9b8d"]],
},
"linen-apron": {
name: "Baker's Linen Apron", tag: "Home · Kitchen", price: 54, was: null,
rating: 4.6, reviews: 73, stock: "Only 6 left", low: true,
desc: "Full-length linen apron with a roomy front pocket and crossed back straps. Flour-dusted by design.",
shape: "apron", ramps: [["#e3d6c4", "#c9b59a"], ["#ecdfcd", "#d3bfa3"], ["#dccdb8", "#bda484"]],
},
"wool-scarf": {
name: "Headland Wool Scarf", tag: "Accessories", price: 72, was: 88,
rating: 4.8, reviews: 142, stock: "In stock", low: false,
desc: "A double-faced merino scarf in coastal heather, long enough to wrap twice against the wind.",
shape: "knit", ramps: [["#c9b6c4", "#8a6f86"], ["#d2c0cd", "#9b819a"], ["#bda6ba", "#7c6478"]],
},
};
// Grid order
var GRID_ORDER = ["linen-shirt", "wool-knit", "canvas-tote", "stoneware-mug", "linen-apron", "wool-scarf"];
// Bundle / shop the look
var LOOK = ["linen-shirt", "wool-knit", "canvas-tote"];
var LOOK_DISCOUNT = 0.15;
var money = function (n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
var stars = function (r) {
var full = Math.round(r);
return "★★★★★".slice(0, full) + "☆☆☆☆☆".slice(0, 5 - full);
};
/* ---------- Toast ---------- */
var toastHost = document.getElementById("toastHost");
function toast(msg) {
var t = document.createElement("div");
t.className = "toast";
t.textContent = msg;
toastHost.appendChild(t);
setTimeout(function () {
t.classList.add("out");
t.addEventListener("animationend", function () { t.remove(); });
}, 2200);
}
/* ---------- Cart state ---------- */
var cart = {}; // id -> qty
var cartBtn = document.getElementById("cartBtn");
var cartCount = document.getElementById("cartCount");
function cartItemCount() {
return Object.keys(cart).reduce(function (s, id) { return s + cart[id]; }, 0);
}
function cartSubtotal() {
return Object.keys(cart).reduce(function (s, id) { return s + PRODUCTS[id].price * cart[id]; }, 0);
}
function updateCount() {
var n = cartItemCount();
cartCount.textContent = n;
cartCount.setAttribute("aria-label", n + (n === 1 ? " item" : " items") + " in bag");
cartCount.classList.remove("pulse");
void cartCount.offsetWidth;
cartCount.classList.add("pulse");
}
function addToCart(id, qty) {
qty = qty || 1;
cart[id] = (cart[id] || 0) + qty;
updateCount();
renderDrawer();
}
/* ---------- Product grid ---------- */
var grid = document.getElementById("productGrid");
GRID_ORDER.forEach(function (id) {
var p = PRODUCTS[id];
var card = document.createElement("article");
card.className = "card";
var saleBadge = p.was ? '<span class="card-badge">Sale</span>' : "";
var wasHtml = p.was ? '<span class="was">' + money(p.was) + "</span>" : "";
var cardArt = svgGarment({ id: "card-" + id, a: p.ramps[0][0], b: p.ramps[0][1], shape: SHAPES[p.shape] });
card.innerHTML =
'<div class="card-art">' + saleBadge + cardArt + "</div>" +
'<div class="card-body">' +
'<span class="card-tag">' + p.tag + "</span>" +
'<h3 class="card-name">' + p.name + "</h3>" +
'<div class="card-rating"><span class="stars" aria-hidden="true">' + stars(p.rating) + "</span>" +
"<span>" + p.rating.toFixed(1) + " · " + p.reviews + " reviews</span></div>" +
'<div class="card-foot"><span class="price">' + wasHtml + money(p.price) + "</span>" +
'<button class="card-add" data-add="' + id + '" aria-label="Add ' + p.name + ' to bag">+</button></div>' +
"</div>";
grid.appendChild(card);
card.querySelector(".card-name").addEventListener("click", function () { openPopover(id, card.querySelector(".card-name")); });
card.querySelector(".card-art").style.cursor = "pointer";
card.querySelector(".card-art").addEventListener("click", function () { openPopover(id, card.querySelector(".card-art")); });
});
grid.addEventListener("click", function (e) {
var btn = e.target.closest("[data-add]");
if (!btn) return;
var id = btn.getAttribute("data-add");
addToCart(id, 1);
toast(PRODUCTS[id].name + " added to bag");
});
/* ---------- Product popover ---------- */
var popOverlay = document.getElementById("popOverlay");
var popover = document.getElementById("popover");
var popArt = document.getElementById("popArt");
var popThumbs = document.getElementById("popThumbs");
var popName = document.getElementById("popName");
var popTag = document.getElementById("popTag");
var popRating = document.getElementById("popRating");
var popPrice = document.getElementById("popPrice");
var popDesc = document.getElementById("popDesc");
var popStock = document.getElementById("popStock");
var popAdd = document.getElementById("popAdd");
var popClose = document.getElementById("popClose");
var popTrigger = null;
var popCurrent = null;
function setPopArt(id, idx) {
var p = PRODUCTS[id];
var ramp = p.ramps[idx] || p.ramps[0];
popArt.innerHTML = svgGarment({ id: "pop", a: ramp[0], b: ramp[1], shape: SHAPES[p.shape] });
Array.prototype.forEach.call(popThumbs.children, function (t, i) {
t.setAttribute("aria-selected", String(i === idx));
});
}
function openPopover(id, trigger) {
var p = PRODUCTS[id];
popCurrent = id;
popTrigger = trigger || null;
popTag.textContent = p.tag;
popName.textContent = p.name;
popRating.innerHTML = '<span class="stars" aria-hidden="true">' + stars(p.rating) + "</span> " +
p.rating.toFixed(1) + " · " + p.reviews + " reviews";
popPrice.innerHTML = (p.was ? '<span class="was">' + money(p.was) + "</span>" : "") + money(p.price);
popDesc.textContent = p.desc;
popStock.textContent = p.stock;
popStock.className = "pop-stock" + (p.low ? " low" : "");
// thumbs
popThumbs.innerHTML = "";
p.ramps.forEach(function (ramp, i) {
var b = document.createElement("button");
b.className = "pop-thumb";
b.setAttribute("role", "tab");
b.setAttribute("aria-label", "View " + (i + 1));
b.innerHTML = svgGarment({ id: "th" + i, a: ramp[0], b: ramp[1], shape: SHAPES[p.shape] });
b.addEventListener("click", function () { setPopArt(id, i); });
popThumbs.appendChild(b);
});
setPopArt(id, 0);
popOverlay.hidden = false;
popover.hidden = false;
document.body.style.overflow = "hidden";
if (popHotspot) popHotspot.setAttribute("aria-expanded", "true");
popClose.focus();
document.addEventListener("keydown", onPopKey);
}
var popHotspot = null;
function closePopover() {
popOverlay.hidden = true;
popover.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", onPopKey);
if (popHotspot) { popHotspot.setAttribute("aria-expanded", "false"); popHotspot = null; }
if (popTrigger && popTrigger.focus) popTrigger.focus();
popCurrent = null;
}
function onPopKey(e) {
if (e.key === "Escape") closePopover();
if (e.key === "Tab") trapFocus(e, popover);
}
popClose.addEventListener("click", closePopover);
popOverlay.addEventListener("click", closePopover);
popAdd.addEventListener("click", function () {
if (!popCurrent) return;
addToCart(popCurrent, 1);
toast(PRODUCTS[popCurrent].name + " added to bag");
closePopover();
});
/* ---------- Hotspots in the story ---------- */
Array.prototype.forEach.call(document.querySelectorAll(".hotspot"), function (h) {
h.setAttribute("aria-expanded", "false");
h.addEventListener("click", function () {
var id = h.getAttribute("data-product");
popHotspot = h;
openPopover(id, h);
});
});
/* ---------- Cart drawer ---------- */
var drawer = document.getElementById("cartDrawer");
var drawerOverlay = document.getElementById("drawerOverlay");
var drawerBody = document.getElementById("drawerBody");
var drawerSubtotal = document.getElementById("drawerSubtotal");
var drawerClose = document.getElementById("drawerClose");
function renderDrawer() {
var ids = Object.keys(cart).filter(function (id) { return cart[id] > 0; });
if (!ids.length) {
drawerBody.innerHTML = '<p class="drawer-empty">Your bag is empty.<br/>Add something from the collection.</p>';
drawerSubtotal.textContent = money(0);
return;
}
drawerBody.innerHTML = "";
ids.forEach(function (id) {
var p = PRODUCTS[id];
var q = cart[id];
var line = document.createElement("div");
line.className = "line";
var ramp = p.ramps[0];
line.innerHTML =
'<div class="line-art" style="background:linear-gradient(160deg,' + ramp[0] + "," + ramp[1] + ')"></div>' +
"<div><div class=\"line-name\">" + p.name + "</div>" +
'<div class="line-tag">' + p.tag + "</div>" +
'<div class="qty"><button data-dec="' + id + '" aria-label="Decrease quantity">−</button>' +
"<span>" + q + "</span>" +
'<button data-inc="' + id + '" aria-label="Increase quantity">+</button></div></div>' +
'<div class="line-right"><div class="line-price">' + money(p.price * q) + "</div>" +
'<button class="line-remove" data-rm="' + id + '">Remove</button></div>';
drawerBody.appendChild(line);
});
drawerSubtotal.textContent = money(cartSubtotal());
}
drawerBody.addEventListener("click", function (e) {
var inc = e.target.closest("[data-inc]");
var dec = e.target.closest("[data-dec]");
var rm = e.target.closest("[data-rm]");
if (inc) { cart[inc.getAttribute("data-inc")]++; updateCount(); renderDrawer(); }
else if (dec) {
var idd = dec.getAttribute("data-dec");
cart[idd] = Math.max(0, cart[idd] - 1);
if (cart[idd] === 0) delete cart[idd];
updateCount(); renderDrawer();
} else if (rm) {
var idr = rm.getAttribute("data-rm");
delete cart[idr];
updateCount(); renderDrawer();
toast("Removed from bag");
}
});
var drawerTrigger = null;
function openDrawer() {
drawerTrigger = document.activeElement;
renderDrawer();
drawerOverlay.hidden = false;
drawer.hidden = false;
document.body.style.overflow = "hidden";
drawerClose.focus();
document.addEventListener("keydown", onDrawerKey);
}
function closeDrawer() {
drawerOverlay.hidden = true;
drawer.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", onDrawerKey);
if (drawerTrigger && drawerTrigger.focus) drawerTrigger.focus();
}
function onDrawerKey(e) {
if (e.key === "Escape") closeDrawer();
if (e.key === "Tab") trapFocus(e, drawer);
}
cartBtn.addEventListener("click", openDrawer);
drawerClose.addEventListener("click", closeDrawer);
drawerOverlay.addEventListener("click", closeDrawer);
document.getElementById("checkoutBtn").addEventListener("click", function () {
if (cartItemCount() === 0) { toast("Your bag is empty"); return; }
toast("Demo only — no real checkout. Thanks for browsing!");
});
/* ---------- Shop the look bundle ---------- */
var bundleList = document.getElementById("bundleList");
LOOK.forEach(function (id) {
var p = PRODUCTS[id];
var ramp = p.ramps[0];
var li = document.createElement("li");
li.innerHTML =
'<span class="bundle-swatch" style="background:linear-gradient(160deg,' + ramp[0] + "," + ramp[1] + ')"></span>' +
"<span><span class=\"bundle-li-name\">" + p.name + "</span><br/>" +
'<span class="bundle-li-tag">' + p.tag + "</span></span>" +
'<span class="bundle-li-price">' + money(p.price) + "</span>";
bundleList.appendChild(li);
});
var lookFull = LOOK.reduce(function (s, id) { return s + PRODUCTS[id].price; }, 0);
var lookNow = Math.round(lookFull * (1 - LOOK_DISCOUNT) * 100) / 100;
document.getElementById("bundleWas").textContent = money(lookFull);
document.getElementById("bundleNow").textContent = money(lookNow);
document.getElementById("bundleSave").textContent = "Save " + money(lookFull - lookNow);
document.getElementById("addLook").addEventListener("click", function () {
LOOK.forEach(function (id) { addToCart(id, 1); });
toast("The Coastline Capsule added — 3 pieces");
});
/* ---------- Focus trap util ---------- */
function trapFocus(e, container) {
var f = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
f = Array.prototype.filter.call(f, function (el) { return !el.disabled && el.offsetParent !== null; });
if (!f.length) return;
var first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
// init
updateCount();
renderDrawer();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Atelier Nord — The Coastline Collection</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,[email protected],400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<!-- Announcement -->
<div class="announce" role="region" aria-label="Store announcement">
<p>Free shipping over <strong>$120</strong> · Complimentary gift wrap on every order</p>
</div>
<!-- Header -->
<header class="site-header">
<div class="wrap header-inner">
<a class="logo" href="#main" aria-label="Atelier Nord home">
<span class="logo-mark" aria-hidden="true">
<svg viewBox="0 0 28 28" width="26" height="26" fill="none">
<path d="M4 22 14 6l10 16" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>
<path d="M9 22l5-8 5 8" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/>
</svg>
</span>
<span class="logo-text">Atelier Nord</span>
</a>
<nav class="site-nav" aria-label="Primary">
<a href="#story">Lookbook</a>
<a href="#grid">Shop the collection</a>
<a href="#bundle">Shop the look</a>
</nav>
<button class="cart-btn" id="cartBtn" aria-haspopup="dialog" aria-controls="cartDrawer">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
<path d="M6 7h12l-1 12H7L6 7Z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
<path d="M9 7a3 3 0 0 1 6 0" stroke="currentColor" stroke-width="1.6"/>
</svg>
<span>Bag</span>
<span class="cart-count" id="cartCount" aria-label="0 items in bag">0</span>
</button>
</div>
</header>
<main id="main">
<!-- Editorial hero -->
<section class="hero" aria-labelledby="heroTitle">
<div class="hero-art" aria-hidden="true">
<div class="hero-sun"></div>
<div class="hero-wave hero-wave-1"></div>
<div class="hero-wave hero-wave-2"></div>
<div class="hero-wave hero-wave-3"></div>
</div>
<div class="wrap hero-inner">
<p class="eyebrow">Collection №07 · Spring</p>
<h1 id="heroTitle">The Coastline<br/>Collection</h1>
<p class="hero-lede">Salt-washed linen, weathered ceramics and slow-made knits — a wardrobe for grey mornings and golden afternoons by the sea.</p>
<div class="hero-actions">
<a class="btn btn-primary" href="#grid">Shop the collection</a>
<a class="btn btn-ghost" href="#story">View lookbook</a>
</div>
<dl class="hero-meta">
<div><dt>Pieces</dt><dd>18</dd></div>
<div><dt>Made in</dt><dd>Portugal</dd></div>
<div><dt>Edition</dt><dd>Limited</dd></div>
</dl>
</div>
</section>
<!-- Editorial story with shoppable panels -->
<section id="story" class="story" aria-labelledby="storyTitle">
<div class="wrap">
<header class="section-head">
<p class="eyebrow">The Lookbook</p>
<h2 id="storyTitle">Tap the dots to shop each look</h2>
<p class="section-sub">Three styled scenes, twelve pieces. Every marker opens the product behind it — add it to your bag without leaving the story.</p>
</header>
</div>
<div class="wrap story-grid">
<!-- Panel 1 -->
<figure class="panel panel-tall" data-panel="dune">
<div class="panel-art art-dune" aria-hidden="true">
<svg viewBox="0 0 200 280" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<rect width="200" height="280" fill="none"/>
<path d="M0 200 Q60 160 110 190 T200 175V280H0Z" fill="rgba(255,255,255,.18)"/>
<ellipse cx="100" cy="120" rx="34" ry="60" fill="rgba(255,255,255,.22)"/>
</svg>
</div>
<figcaption class="panel-cap">
<span class="panel-num">01</span>
<span class="panel-title">Morning on the dunes</span>
</figcaption>
<button class="hotspot" style="left:54%;top:42%" data-product="linen-shirt" aria-haspopup="dialog" aria-label="Shop the Salt Linen Overshirt">
<span class="dot"></span>
</button>
<button class="hotspot" style="left:38%;top:70%" data-product="canvas-tote" aria-haspopup="dialog" aria-label="Shop the Harbour Canvas Tote">
<span class="dot"></span>
</button>
</figure>
<!-- Panel 2 -->
<figure class="panel" data-panel="harbour">
<div class="panel-art art-harbour" aria-hidden="true">
<svg viewBox="0 0 200 180" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<path d="M0 110h200v70H0z" fill="rgba(13,30,48,.16)"/>
<path d="M40 110 60 70 80 110Z" fill="rgba(255,255,255,.25)"/>
<path d="M120 110 138 60 156 110Z" fill="rgba(255,255,255,.18)"/>
</svg>
</div>
<figcaption class="panel-cap">
<span class="panel-num">02</span>
<span class="panel-title">The harbour walk</span>
</figcaption>
<button class="hotspot" style="left:46%;top:48%" data-product="wool-knit" aria-haspopup="dialog" aria-label="Shop the Fisher Wool Knit">
<span class="dot"></span>
</button>
</figure>
<!-- Panel 3 -->
<figure class="panel" data-panel="kitchen">
<div class="panel-art art-kitchen" aria-hidden="true">
<svg viewBox="0 0 200 180" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<ellipse cx="70" cy="120" rx="30" ry="14" fill="rgba(255,255,255,.3)"/>
<rect x="120" y="70" width="38" height="60" rx="6" fill="rgba(255,255,255,.22)"/>
</svg>
</div>
<figcaption class="panel-cap">
<span class="panel-num">03</span>
<span class="panel-title">Slow kitchen mornings</span>
</figcaption>
<button class="hotspot" style="left:35%;top:62%" data-product="stoneware-mug" aria-haspopup="dialog" aria-label="Shop the Tide Stoneware Mug">
<span class="dot"></span>
</button>
<button class="hotspot" style="left:68%;top:50%" data-product="linen-apron" aria-haspopup="dialog" aria-label="Shop the Baker's Linen Apron">
<span class="dot"></span>
</button>
</figure>
</div>
</section>
<!-- Product grid -->
<section id="grid" class="grid-section" aria-labelledby="gridTitle">
<div class="wrap">
<header class="section-head">
<p class="eyebrow">Shop the collection</p>
<h2 id="gridTitle">All 6 pieces</h2>
<p class="section-sub">Curated for the season and made to last well beyond it.</p>
</header>
<div class="product-grid" id="productGrid"><!-- cards injected by JS --></div>
</div>
</section>
<!-- Shop the look bundle -->
<section id="bundle" class="bundle" aria-labelledby="bundleTitle">
<div class="wrap bundle-inner">
<div class="bundle-art" aria-hidden="true">
<div class="bundle-stack">
<span class="bundle-chip" data-b="linen-shirt"></span>
<span class="bundle-chip" data-b="wool-knit"></span>
<span class="bundle-chip" data-b="canvas-tote"></span>
</div>
</div>
<div class="bundle-copy">
<p class="eyebrow">Shop the look</p>
<h2 id="bundleTitle">The Coastline Capsule</h2>
<p class="section-sub">Three signature pieces, styled together. Pick all three and the capsule discount applies automatically at checkout.</p>
<ul class="bundle-list" id="bundleList" aria-label="Pieces in this look"></ul>
<div class="bundle-footer">
<div class="bundle-price">
<span class="bundle-was" id="bundleWas"></span>
<span class="bundle-now" id="bundleNow"></span>
<span class="bundle-save" id="bundleSave"></span>
</div>
<button class="btn btn-primary" id="addLook">Add all 3 to bag</button>
</div>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="wrap footer-inner">
<p class="footer-brand">Atelier Nord</p>
<p class="footer-note">Slow-made goods · Shipped from Porto · <span>Illustrative demo store</span></p>
</div>
</footer>
<!-- Product popover (used by hotspots) -->
<div class="popover-overlay" id="popOverlay" hidden></div>
<div class="popover" id="popover" role="dialog" aria-modal="true" aria-labelledby="popName" hidden>
<button class="pop-close" id="popClose" aria-label="Close product">×</button>
<div class="pop-gallery">
<div class="pop-art" id="popArt" aria-hidden="true"></div>
<div class="pop-thumbs" id="popThumbs" role="tablist" aria-label="Product views"></div>
</div>
<div class="pop-body">
<p class="pop-tag" id="popTag"></p>
<h3 id="popName"></h3>
<div class="pop-rating" id="popRating"></div>
<p class="pop-price" id="popPrice"></p>
<p class="pop-desc" id="popDesc"></p>
<p class="pop-stock" id="popStock"></p>
<button class="btn btn-primary pop-add" id="popAdd">Add to bag</button>
</div>
</div>
<!-- Cart drawer -->
<div class="drawer-overlay" id="drawerOverlay" hidden></div>
<aside class="drawer" id="cartDrawer" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" hidden>
<header class="drawer-head">
<h2 id="drawerTitle">Your bag</h2>
<button class="drawer-close" id="drawerClose" aria-label="Close bag">×</button>
</header>
<div class="drawer-body" id="drawerBody"></div>
<footer class="drawer-foot">
<div class="drawer-sub">
<span>Subtotal</span>
<strong id="drawerSubtotal">$0.00</strong>
</div>
<p class="drawer-ship">Shipping & capsule discounts calculated at checkout.</p>
<button class="btn btn-primary btn-block" id="checkoutBtn">Secure checkout</button>
<p class="drawer-secure">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" aria-hidden="true"><path d="M6 11V8a6 6 0 0 1 12 0v3" stroke="currentColor" stroke-width="1.6"/><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="1.6"/></svg>
Encrypted & secure
</p>
</footer>
</aside>
<div class="toast-host" id="toastHost" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Collection / Lookbook
An editorial collection page for the fictional label “Atelier Nord” and its Coastline Collection. The hero is pure CSS — a layered sunset, stacked waves, and a serif display headline — paired with collection metadata and dual CTAs. Below it, a three-panel lookbook tells a styled story across gradient “photo” scenes (the dunes, the harbour, slow kitchen mornings). Each scene carries pulsing shoppable hotspots: tap one and a product popover rises with an inline-SVG gallery you can switch between swatches, a star rating, a price (with strike-through where on sale), a live stock chip, and an add-to-bag button.
The page then opens up into a six-piece product grid built entirely from inline-SVG garment silhouettes on soft tinted tiles — no external images anywhere. Clicking a card’s art or title opens the same popover; the round ”+” button drops the item straight into the bag. A shop-the-look capsule bundles three signature pieces, lists them with swatches and prices, and shows the original total, the discounted capsule price, and the savings — adding all three in one click.
Every interaction works. The bag is a focus-trapped slide-out drawer with quantity steppers, per-line totals, remove buttons, and a live subtotal; the header badge pulses on each add. Popover and drawer both close on Escape or overlay click and restore focus to their trigger, toasts confirm actions, and the layout collapses gracefully to a single column down to ~360px with visible focus rings, landmark roles, and reduced-motion support.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.