Shop — Storefront Home
A conversion-focused e-commerce storefront home page with a sticky header, live cart count, hero banner, featured collection tiles, a horizontally scrolling trending product grid, and a flash-sale strip with a ticking countdown. Add-to-cart updates the header badge, fires a toast, and fills a slide-out cart drawer with working quantity steppers and a live subtotal. Includes search filtering, wishlist toggles, trust badges, a newsletter signup, and a tidy footer.
MCP
Code
/* ===== Nimbus storefront ===== */
:root {
--bg: #ffffff;
--bg-soft: #f6f7fb;
--ink: #16181d;
--muted: #6b7280;
--brand: #3457ff;
--brand-d: #2742d6;
--sale: #e0245e;
--ok: #1f9d55;
--line: rgba(16, 18, 29, .1);
--line-2: rgba(16, 18, 29, .06);
--shadow: 0 1px 2px rgba(16, 18, 29, .04), 0 8px 24px rgba(16, 18, 29, .06);
--shadow-lg: 0 18px 48px rgba(16, 18, 29, .14);
--radius: 16px;
--radius-sm: 10px;
--wrap: 1200px;
--tile-a: linear-gradient(135deg, #e7ecff, #c9d6ff);
--tile-b: linear-gradient(135deg, #ffe9d6, #ffd0b3);
--tile-c: linear-gradient(135deg, #d9f3ea, #b6e7d6);
--tile-d: linear-gradient(135deg, #f3e0ff, #e0c6ff);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
color: var(--ink);
background: var(--bg);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { line-height: 1.15; margin: 0; letter-spacing: -.02em; }
p { margin: 0; }
a { color: inherit; text-decoration: none; }
img, svg { display: block; }
ul { list-style: none; margin: 0; padding: 0; }
.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding-inline: 20px; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.skip-link {
position: absolute; left: 12px; top: -60px; z-index: 100;
background: var(--ink); color: #fff; padding: 10px 16px; border-radius: 8px;
transition: top .2s;
}
.skip-link:focus { top: 12px; }
:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; border-radius: 6px; }
/* ===== Buttons ===== */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
font: inherit; font-weight: 600; cursor: pointer;
border: 1px solid transparent; border-radius: 999px;
padding: 12px 22px; transition: transform .12s, background .18s, box-shadow .18s, color .18s;
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--brand); color: #fff; box-shadow: 0 6px 18px rgba(52, 87, 255, .28); }
.btn-primary:hover { background: var(--brand-d); }
.btn-ghost { background: #fff; color: var(--ink); border-color: var(--line); }
.btn-ghost:hover { border-color: var(--ink); }
.btn-block { width: 100%; }
/* ===== Announcement ===== */
.announce {
background: var(--ink); color: #fff; text-align: center;
font-size: .82rem; padding: 9px 16px;
}
.announce strong { color: #cdd6ff; }
/* ===== Header ===== */
.site-header {
position: sticky; top: 0; z-index: 40;
background: rgba(255, 255, 255, .9);
backdrop-filter: saturate(160%) blur(12px);
border-bottom: 1px solid var(--line);
}
.header-inner {
display: grid; align-items: center; gap: 16px;
grid-template-columns: auto auto 1fr auto;
min-height: 68px;
}
.logo { display: inline-flex; align-items: center; gap: 9px; color: var(--ink); }
.logo-mark { color: var(--brand); display: inline-flex; }
.logo-text { font-weight: 800; font-size: 1.25rem; letter-spacing: -.03em; }
.primary-nav { display: flex; gap: 22px; }
.primary-nav a { font-weight: 500; color: var(--muted); padding: 6px 2px; position: relative; }
.primary-nav a::after {
content: ""; position: absolute; left: 0; bottom: -2px; height: 2px; width: 0;
background: var(--brand); transition: width .18s;
}
.primary-nav a:hover { color: var(--ink); }
.primary-nav a:hover::after { width: 100%; }
.search {
position: relative; display: flex; align-items: center;
background: var(--bg-soft); border: 1px solid var(--line); border-radius: 999px;
max-width: 360px; justify-self: end; width: 100%; transition: border-color .18s, background .18s;
}
.search:focus-within { border-color: var(--brand); background: #fff; }
.search-icon { position: absolute; left: 14px; color: var(--muted); display: inline-flex; }
.search input {
border: 0; background: transparent; outline: none; font: inherit;
padding: 10px 16px 10px 40px; width: 100%; border-radius: 999px; color: var(--ink);
}
.header-actions { display: flex; align-items: center; gap: 4px; }
.icon-btn {
display: inline-flex; align-items: center; justify-content: center;
width: 42px; height: 42px; border-radius: 12px; border: 0; background: transparent;
color: var(--ink); cursor: pointer; transition: background .15s;
}
.icon-btn:hover { background: var(--bg-soft); }
.cart-btn { position: relative; }
.cart-count {
position: absolute; top: 4px; right: 4px;
background: var(--brand); color: #fff; font-size: .68rem; font-weight: 700;
min-width: 18px; height: 18px; padding: 0 4px; border-radius: 999px;
display: grid; place-items: center; transition: transform .2s;
}
.cart-count.pulse { animation: pulse .4s ease; }
@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.45); } 100% { transform: scale(1); } }
/* ===== Hero ===== */
.hero {
display: grid; grid-template-columns: 1.1fr 1fr; gap: 32px; align-items: center;
padding-block: clamp(28px, 6vw, 64px);
}
.hero-copy { order: 1; }
.hero-art { order: 2; position: relative; min-height: 320px; display: grid; place-items: center; }
.eyebrow {
display: inline-block; font-size: .78rem; font-weight: 700; letter-spacing: .08em;
text-transform: uppercase; color: var(--brand); margin-bottom: 14px;
}
.hero h1 { font-size: clamp(2.2rem, 5.5vw, 3.6rem); font-weight: 800; }
.hero h1 .hl { color: var(--brand); }
.hero-copy p { color: var(--muted); font-size: 1.05rem; margin: 18px 0 26px; max-width: 46ch; }
.hero-cta { display: flex; gap: 12px; flex-wrap: wrap; }
.hero-meta {
display: flex; gap: 26px; margin-top: 30px; flex-wrap: wrap;
padding-top: 22px; border-top: 1px solid var(--line);
}
.hero-meta li { font-size: .92rem; color: var(--muted); }
.hero-meta strong { color: var(--ink); }
.hero-art .hero-object { position: relative; z-index: 2; filter: drop-shadow(0 24px 48px rgba(16,18,29,.16)); }
.hero-blob { position: absolute; border-radius: 50%; filter: blur(8px); z-index: 1; }
.blob-a { width: 280px; height: 280px; background: radial-gradient(circle at 30% 30%, #cdd9ff, #93acff); opacity: .55; top: 8%; left: 6%; }
.blob-b { width: 200px; height: 200px; background: radial-gradient(circle at 60% 40%, #ffd9c2, #ffb38f); opacity: .5; bottom: 4%; right: 8%; }
/* ===== Sections ===== */
.section { padding-block: clamp(28px, 5vw, 52px); }
.section-head { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
.section-head h2 { font-size: clamp(1.4rem, 3vw, 1.9rem); font-weight: 700; }
.link-more { color: var(--brand); font-weight: 600; }
.link-more:hover { color: var(--brand-d); }
/* ===== Collections ===== */
.collections { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.coll-tile {
position: relative; border-radius: var(--radius); padding: 20px; min-height: 200px;
display: flex; flex-direction: column; justify-content: flex-end;
overflow: hidden; border: 1px solid var(--line-2); transition: transform .2s, box-shadow .2s;
}
.coll-tile::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(to top, rgba(16,18,29,.18), transparent 55%);
}
.coll-tile:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.tile-1 { background: var(--tile-a); }
.tile-2 { background: var(--tile-b); }
.tile-3 { background: var(--tile-c); }
.tile-4 { background: var(--tile-d); }
.coll-tag {
position: relative; z-index: 1; align-self: flex-start;
background: rgba(255,255,255,.85); color: var(--ink); font-size: .72rem; font-weight: 600;
padding: 4px 10px; border-radius: 999px; margin-bottom: auto;
}
.coll-name { position: relative; z-index: 1; color: #fff; font-weight: 700; font-size: 1.2rem; }
/* ===== Deal strip ===== */
.deal-strip {
display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap;
background: linear-gradient(120deg, #1b1f3a, #2a2f5e);
color: #fff; border-radius: 22px; padding: clamp(22px, 4vw, 36px);
}
.deal-flag {
display: inline-block; background: var(--sale); color: #fff; font-size: .72rem; font-weight: 700;
letter-spacing: .06em; text-transform: uppercase; padding: 5px 12px; border-radius: 999px; margin-bottom: 12px;
}
.deal-copy h2 { font-size: clamp(1.4rem, 3.5vw, 2.1rem); }
.deal-copy p { color: rgba(255,255,255,.74); margin-top: 8px; max-width: 44ch; }
.countdown { display: flex; align-items: center; gap: 8px; }
.cd-unit {
background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.18);
border-radius: 14px; padding: 12px 6px; min-width: 72px; text-align: center;
}
.cd-num { display: block; font-size: clamp(1.6rem, 4vw, 2.2rem); font-weight: 800; font-variant-numeric: tabular-nums; }
.cd-lbl { display: block; font-size: .68rem; text-transform: uppercase; letter-spacing: .08em; color: rgba(255,255,255,.6); }
.cd-sep { font-size: 1.6rem; font-weight: 700; color: rgba(255,255,255,.4); }
/* ===== Product rail ===== */
.carousel-ctrls { display: flex; gap: 8px; }
.round-btn {
width: 42px; height: 42px; border-radius: 50%; border: 1px solid var(--line);
background: #fff; color: var(--ink); font-size: 1.4rem; line-height: 1; cursor: pointer;
display: grid; place-items: center; transition: background .15s, border-color .15s, transform .12s;
}
.round-btn:hover { background: var(--ink); color: #fff; border-color: var(--ink); }
.round-btn:active { transform: scale(.94); }
.product-rail {
display: grid; grid-auto-flow: column; grid-auto-columns: minmax(248px, 1fr);
gap: 18px; overflow-x: auto; scroll-snap-type: x mandatory;
padding-bottom: 8px; scroll-behavior: smooth; scrollbar-width: thin;
}
.product-rail::-webkit-scrollbar { height: 8px; }
.product-rail::-webkit-scrollbar-thumb { background: var(--line); border-radius: 999px; }
.card {
scroll-snap-align: start;
background: #fff; border: 1px solid var(--line-2); border-radius: var(--radius);
overflow: hidden; display: flex; flex-direction: column; transition: transform .2s, box-shadow .2s, border-color .2s;
}
.card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); border-color: var(--line); }
.card-media { position: relative; aspect-ratio: 4 / 3; display: grid; place-items: center; }
.card-media svg { width: 64%; height: 64%; }
.badge {
position: absolute; top: 12px; left: 12px; font-size: .68rem; font-weight: 700;
padding: 4px 9px; border-radius: 999px; letter-spacing: .03em;
}
.badge.sale { background: var(--sale); color: #fff; }
.badge.new { background: var(--ink); color: #fff; }
.badge.low { background: #fff7e6; color: #b76e00; border: 1px solid #ffe1a8; }
.wish {
position: absolute; top: 10px; right: 10px; width: 34px; height: 34px; border-radius: 50%;
border: 0; background: rgba(255,255,255,.9); color: var(--muted); cursor: pointer;
display: grid; place-items: center; transition: color .15s, transform .12s, background .15s;
}
.wish:hover { color: var(--sale); background: #fff; }
.wish.on { color: var(--sale); }
.wish:active { transform: scale(.9); }
.card-body { padding: 14px 16px 16px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
.card-cat { font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--brand); }
.card-name { font-weight: 600; font-size: 1rem; }
.rating { display: flex; align-items: center; gap: 6px; font-size: .82rem; color: var(--muted); }
.stars { color: #f5a623; letter-spacing: 1px; }
.price-row { display: flex; align-items: baseline; gap: 8px; margin-top: auto; padding-top: 6px; }
.price { font-weight: 700; font-size: 1.12rem; }
.price.is-sale { color: var(--sale); }
.price-was { color: var(--muted); text-decoration: line-through; font-size: .88rem; }
.add-btn {
margin-top: 12px; width: 100%; border: 1px solid var(--brand); background: #fff; color: var(--brand);
font: inherit; font-weight: 600; padding: 10px; border-radius: 12px; cursor: pointer;
transition: background .15s, color .15s, transform .12s;
}
.add-btn:hover { background: var(--brand); color: #fff; }
.add-btn:active { transform: translateY(1px); }
.add-btn.added { background: var(--ok); border-color: var(--ok); color: #fff; }
/* ===== Trust ===== */
.trust-row {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;
background: var(--bg-soft); border: 1px solid var(--line-2); border-radius: 22px; padding: 26px;
}
.trust-item { display: flex; align-items: center; gap: 14px; }
.trust-ic { font-size: 1.6rem; }
.trust-item strong { display: block; font-size: .96rem; }
.trust-item span { color: var(--muted); font-size: .85rem; }
/* ===== Footer ===== */
.site-footer { background: var(--ink); color: rgba(255,255,255,.78); margin-top: 40px; }
.footer-inner { display: grid; grid-template-columns: 1.4fr 2fr; gap: 36px; padding-block: 48px; }
.footer-brand .logo-text { color: #fff; }
.footer-brand p { margin: 12px 0 18px; max-width: 36ch; }
.news { display: flex; gap: 8px; max-width: 340px; }
.news input {
flex: 1; border: 1px solid rgba(255,255,255,.2); background: rgba(255,255,255,.06);
color: #fff; border-radius: 999px; padding: 11px 16px; font: inherit; outline: none;
}
.news input::placeholder { color: rgba(255,255,255,.5); }
.news input:focus { border-color: var(--brand); }
.footer-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; }
.footer-cols h3 { font-size: .82rem; text-transform: uppercase; letter-spacing: .08em; color: #fff; margin-bottom: 12px; }
.footer-cols a { display: block; color: rgba(255,255,255,.7); padding: 5px 0; transition: color .15s; }
.footer-cols a:hover { color: #fff; }
.footer-bottom {
display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;
border-top: 1px solid rgba(255,255,255,.12); padding-block: 18px; font-size: .82rem; color: rgba(255,255,255,.6);
}
/* ===== Cart drawer ===== */
.drawer-overlay {
position: fixed; inset: 0; background: rgba(16,18,29,.5); z-index: 60;
opacity: 0; transition: opacity .25s;
}
.drawer-overlay.show { opacity: 1; }
.cart-drawer {
position: fixed; top: 0; right: 0; height: 100%; width: min(400px, 92vw);
background: #fff; z-index: 70; box-shadow: var(--shadow-lg);
transform: translateX(100%); transition: transform .3s cubic-bezier(.4,0,.2,1);
display: flex; flex-direction: column;
}
.cart-drawer.open { 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-size: 1.2rem; }
.drawer-items { flex: 1; overflow-y: auto; padding: 12px 22px; display: flex; flex-direction: column; gap: 14px; }
.drawer-empty { padding: 40px 22px; text-align: center; color: var(--muted); }
.d-item { display: grid; grid-template-columns: 56px 1fr auto; gap: 12px; align-items: center; }
.d-thumb { width: 56px; height: 56px; border-radius: 12px; display: grid; place-items: center; }
.d-thumb svg { width: 60%; height: 60%; }
.d-info { min-width: 0; }
.d-name { font-weight: 600; font-size: .92rem; }
.d-price { color: var(--muted); font-size: .85rem; }
.qty { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px; }
.qty button {
width: 24px; height: 24px; border-radius: 7px; border: 1px solid var(--line); background: #fff;
cursor: pointer; font-weight: 700; line-height: 1; color: var(--ink);
}
.qty button:hover { background: var(--bg-soft); }
.qty span { min-width: 18px; text-align: center; font-weight: 600; font-size: .9rem; }
.d-remove { background: none; border: 0; color: var(--muted); cursor: pointer; font-size: .8rem; text-decoration: underline; }
.d-remove:hover { color: var(--sale); }
.d-line { font-weight: 700; font-size: .92rem; }
.drawer-foot { padding: 18px 22px 22px; border-top: 1px solid var(--line); }
.drawer-total { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 14px; font-size: 1.05rem; }
.drawer-total strong { font-size: 1.3rem; }
.drawer-note { text-align: center; color: var(--muted); font-size: .78rem; margin-top: 12px; }
/* ===== Toast ===== */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 140%);
background: var(--ink); color: #fff; padding: 13px 20px; border-radius: 12px;
font-weight: 500; font-size: .92rem; box-shadow: var(--shadow-lg); z-index: 90;
transition: transform .35s cubic-bezier(.2,.9,.3,1.2); max-width: 90vw;
}
.toast.show { transform: translate(-50%, 0); }
/* ===== Responsive ===== */
@media (max-width: 960px) {
.hero { grid-template-columns: 1fr; }
.hero-art { order: -1; min-height: 240px; }
.collections { grid-template-columns: repeat(2, 1fr); }
.trust-row { grid-template-columns: repeat(2, 1fr); }
.footer-inner { grid-template-columns: 1fr; gap: 28px; }
}
@media (max-width: 760px) {
.primary-nav { display: none; }
.header-inner { grid-template-columns: auto 1fr auto; }
.search { max-width: none; }
}
@media (max-width: 480px) {
.collections { grid-template-columns: 1fr; }
.trust-row { grid-template-columns: 1fr; }
.deal-strip { flex-direction: column; align-items: flex-start; }
.footer-cols { grid-template-columns: 1fr 1fr; }
.footer-bottom { flex-direction: column; align-items: flex-start; }
.cd-unit { min-width: 60px; }
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; animation: none !important; scroll-behavior: auto !important; }
}/* ===== Nimbus storefront — vanilla JS ===== */
(function () {
"use strict";
const money = (n) =>
"$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
/* Product silhouettes as inline SVG (no external images) */
const SHAPES = {
mug: '<svg viewBox="0 0 64 64" fill="none"><rect x="14" y="20" width="28" height="30" rx="6" fill="#16181d" opacity=".82"/><path d="M42 26h6a7 7 0 0 1 0 14h-6" stroke="#16181d" stroke-width="4" opacity=".82"/></svg>',
lamp: '<svg viewBox="0 0 64 64" fill="none"><path d="M22 14h20l6 14H16l6-14Z" fill="#16181d" opacity=".82"/><rect x="30" y="28" width="4" height="20" fill="#16181d" opacity=".82"/><rect x="20" y="48" width="24" height="5" rx="2.5" fill="#16181d" opacity=".82"/></svg>',
bottle: '<svg viewBox="0 0 64 64" fill="none"><rect x="26" y="10" width="12" height="8" rx="2" fill="#16181d" opacity=".82"/><path d="M24 18h16v30a8 8 0 0 1-8 8 8 8 0 0 1-8-8V18Z" fill="#16181d" opacity=".82"/></svg>',
headphones: '<svg viewBox="0 0 64 64" fill="none"><path d="M14 36v-4a18 18 0 0 1 36 0v4" stroke="#16181d" stroke-width="4" opacity=".82"/><rect x="10" y="34" width="10" height="16" rx="5" fill="#16181d" opacity=".82"/><rect x="44" y="34" width="10" height="16" rx="5" fill="#16181d" opacity=".82"/></svg>',
pot: '<svg viewBox="0 0 64 64" fill="none"><path d="M18 28h28l-3 20a4 4 0 0 1-4 4H25a4 4 0 0 1-4-4l-3-20Z" fill="#16181d" opacity=".82"/><path d="M28 28c0-8 8-8 8-16" stroke="#1f9d55" stroke-width="4" opacity=".82" fill="none"/></svg>',
notebook: '<svg viewBox="0 0 64 64" fill="none"><rect x="18" y="12" width="28" height="40" rx="4" fill="#16181d" opacity=".82"/><rect x="14" y="16" width="6" height="32" rx="3" fill="#3457ff" opacity=".82"/></svg>',
clock: '<svg viewBox="0 0 64 64" fill="none"><circle cx="32" cy="34" r="18" fill="#16181d" opacity=".82"/><path d="M32 34V24M32 34l8 5" stroke="#fff" stroke-width="3"/></svg>',
speaker: '<svg viewBox="0 0 64 64" fill="none"><rect x="20" y="12" width="24" height="40" rx="6" fill="#16181d" opacity=".82"/><circle cx="32" cy="40" r="7" fill="#fff" opacity=".4"/><circle cx="32" cy="22" r="3" fill="#fff" opacity=".4"/></svg>',
};
const TINTS = ["#e7ecff", "#ffe9d6", "#d9f3ea", "#f3e0ff", "#e6f0ff", "#fff0e0"];
const PRODUCTS = [
{ id: "p1", name: "Aero Ceramic Mug", cat: "Kitchen", shape: "mug", price: 24, was: 32, rating: 4.8, reviews: 412, badge: "sale", stock: "ok" },
{ id: "p2", name: "Halo Desk Lamp", cat: "Lighting", shape: "lamp", price: 89, was: null, rating: 4.9, reviews: 238, badge: "new", stock: "ok" },
{ id: "p3", name: "Trail Steel Bottle", cat: "Everyday", shape: "bottle", price: 29, was: null, rating: 4.7, reviews: 956, badge: null, stock: "low" },
{ id: "p4", name: "Drift Wireless Headphones", cat: "Audio", shape: "headphones", price: 129, was: 169, rating: 4.6, reviews: 188, badge: "sale", stock: "ok" },
{ id: "p5", name: "Sprout Planter Set", cat: "Home", shape: "pot", price: 42, was: null, rating: 4.9, reviews: 321, badge: "new", stock: "ok" },
{ id: "p6", name: "Field Lined Notebook", cat: "Desk", shape: "notebook", price: 18, was: 24, rating: 4.8, reviews: 740, badge: "sale", stock: "ok" },
{ id: "p7", name: "Orbit Desk Clock", cat: "Desk", shape: "clock", price: 56, was: null, rating: 4.7, reviews: 154, badge: null, stock: "low" },
{ id: "p8", name: "Pulse Mini Speaker", cat: "Audio", shape: "speaker", price: 74, was: 94, rating: 4.5, reviews: 209, badge: "sale", stock: "ok" },
];
/* ---------- Toast ---------- */
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"), 2400);
}
/* ---------- Cart state ---------- */
const cart = new Map(); // id -> qty
const cartCountEl = document.getElementById("cartCount");
const cartBtn = document.getElementById("cartBtn");
const drawer = document.getElementById("drawer");
const overlay = document.getElementById("overlay");
const drawerItems = document.getElementById("drawerItems");
const drawerEmpty = document.getElementById("drawerEmpty");
const drawerTotal = document.getElementById("drawerTotal");
const findProduct = (id) => PRODUCTS.find((p) => p.id === id);
function cartQty() {
let n = 0;
cart.forEach((q) => (n += q));
return n;
}
function cartSubtotal() {
let t = 0;
cart.forEach((q, id) => (t += findProduct(id).price * q));
return t;
}
function syncHeader() {
const n = cartQty();
cartCountEl.textContent = n;
cartBtn.setAttribute("aria-label", `Open cart, ${n} item${n === 1 ? "" : "s"}`);
cartCountEl.classList.remove("pulse");
// force reflow to restart animation
void cartCountEl.offsetWidth;
cartCountEl.classList.add("pulse");
}
function addToCart(id) {
cart.set(id, (cart.get(id) || 0) + 1);
syncHeader();
renderDrawer();
toast(`${findProduct(id).name} added to cart`);
}
function setQty(id, delta) {
const next = (cart.get(id) || 0) + delta;
if (next <= 0) cart.delete(id);
else cart.set(id, next);
syncHeader();
renderDrawer();
}
function removeItem(id) {
cart.delete(id);
syncHeader();
renderDrawer();
}
function renderDrawer() {
drawerItems.innerHTML = "";
if (cart.size === 0) {
drawerEmpty.hidden = false;
} else {
drawerEmpty.hidden = true;
let i = 0;
cart.forEach((qty, id) => {
const p = findProduct(id);
const li = document.createElement("li");
li.className = "d-item";
li.innerHTML = `
<span class="d-thumb" style="background:${TINTS[i % TINTS.length]}">${SHAPES[p.shape]}</span>
<div class="d-info">
<div class="d-name">${p.name}</div>
<div class="d-price">${money(p.price)} each</div>
<div class="qty">
<button type="button" data-act="dec" aria-label="Decrease quantity of ${p.name}">−</button>
<span aria-label="Quantity">${qty}</span>
<button type="button" data-act="inc" aria-label="Increase quantity of ${p.name}">+</button>
<button type="button" class="d-remove" data-act="rm">Remove</button>
</div>
</div>
<div class="d-line">${money(p.price * qty)}</div>`;
li.querySelector('[data-act="dec"]').addEventListener("click", () => setQty(id, -1));
li.querySelector('[data-act="inc"]').addEventListener("click", () => setQty(id, 1));
li.querySelector('[data-act="rm"]').addEventListener("click", () => removeItem(id));
drawerItems.appendChild(li);
i++;
});
}
drawerTotal.textContent = money(cartSubtotal());
}
/* ---------- Drawer open/close ---------- */
let lastFocus = null;
function openCart() {
lastFocus = document.activeElement;
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
overlay.hidden = false;
requestAnimationFrame(() => overlay.classList.add("show"));
drawer.focus();
}
function closeCart() {
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
overlay.classList.remove("show");
setTimeout(() => (overlay.hidden = true), 260);
if (lastFocus) lastFocus.focus();
}
cartBtn.addEventListener("click", openCart);
document.getElementById("closeCart").addEventListener("click", closeCart);
overlay.addEventListener("click", closeCart);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && drawer.classList.contains("open")) closeCart();
});
document.getElementById("checkoutBtn").addEventListener("click", () => {
if (cart.size === 0) {
toast("Your cart is empty");
return;
}
toast("Checkout is disabled in this demo");
});
/* ---------- Render product cards ---------- */
const rail = document.getElementById("rail");
PRODUCTS.forEach((p, idx) => {
const li = document.createElement("li");
li.className = "card";
const onSale = p.was && p.was > p.price;
let badge = "";
if (p.badge === "sale" && onSale) {
const pct = Math.round((1 - p.price / p.was) * 100);
badge = `<span class="badge sale">−${pct}%</span>`;
} else if (p.badge === "new") {
badge = `<span class="badge new">New</span>`;
}
const stockChip = p.stock === "low" ? `<span class="badge low">Low stock</span>` : "";
const stars = "★★★★★".slice(0, Math.round(p.rating)) + "☆☆☆☆☆".slice(0, 5 - Math.round(p.rating));
li.innerHTML = `
<div class="card-media" style="background:${TINTS[idx % TINTS.length]}">
${badge || stockChip}
<button class="wish" type="button" aria-label="Add ${p.name} to wishlist" aria-pressed="false">♥</button>
${SHAPES[p.shape]}
</div>
<div class="card-body">
<span class="card-cat">${p.cat}</span>
<h3 class="card-name">${p.name}</h3>
<div class="rating">
<span class="stars" aria-hidden="true">${stars}</span>
<span>${p.rating.toFixed(1)} · ${p.reviews} reviews</span>
</div>
<div class="price-row">
<span class="price ${onSale ? "is-sale" : ""}">${money(p.price)}</span>
${onSale ? `<span class="price-was">${money(p.was)}</span>` : ""}
</div>
<button class="add-btn" type="button">Add to cart</button>
</div>`;
const addBtn = li.querySelector(".add-btn");
addBtn.addEventListener("click", () => {
addToCart(p.id);
addBtn.classList.add("added");
addBtn.textContent = "Added ✓";
setTimeout(() => {
addBtn.classList.remove("added");
addBtn.textContent = "Add to cart";
}, 1100);
});
const wish = li.querySelector(".wish");
wish.addEventListener("click", () => {
const on = wish.classList.toggle("on");
wish.setAttribute("aria-pressed", String(on));
toast(on ? `Saved ${p.name} to wishlist` : `Removed ${p.name} from wishlist`);
});
rail.appendChild(li);
});
/* ---------- Carousel scroll ---------- */
function railStep() {
const card = rail.querySelector(".card");
if (!card) return 280;
const gap = parseFloat(getComputedStyle(rail).columnGap || "18") || 18;
return card.getBoundingClientRect().width + gap;
}
document.getElementById("prevBtn").addEventListener("click", () => {
rail.scrollBy({ left: -railStep() * 2, behavior: "smooth" });
});
document.getElementById("nextBtn").addEventListener("click", () => {
rail.scrollBy({ left: railStep() * 2, behavior: "smooth" });
});
/* ---------- Countdown ---------- */
const cdH = document.getElementById("cdH");
const cdM = document.getElementById("cdM");
const cdS = document.getElementById("cdS");
// End 8h 45m from load — a moving but stable target for the session.
let remaining = 8 * 3600 + 45 * 60 + 12;
const pad = (n) => String(n).padStart(2, "0");
function tickCountdown() {
if (remaining <= 0) remaining = 8 * 3600 + 45 * 60; // loop the sale
const h = Math.floor(remaining / 3600);
const m = Math.floor((remaining % 3600) / 60);
const s = remaining % 60;
cdH.textContent = pad(h);
cdM.textContent = pad(m);
cdS.textContent = pad(s);
remaining--;
}
tickCountdown();
setInterval(tickCountdown, 1000);
/* ---------- Search (filters the rail by name/category) ---------- */
const searchInput = document.getElementById("search");
searchInput.addEventListener("input", () => {
const q = searchInput.value.trim().toLowerCase();
let shown = 0;
rail.querySelectorAll(".card").forEach((card, idx) => {
const p = PRODUCTS[idx];
const match = !q || p.name.toLowerCase().includes(q) || p.cat.toLowerCase().includes(q);
card.style.display = match ? "" : "none";
if (match) shown++;
});
if (q && shown === 0) toast(`No products match "${searchInput.value.trim()}"`);
});
/* ---------- Newsletter ---------- */
document.getElementById("newsForm").addEventListener("submit", (e) => {
e.preventDefault();
const email = document.getElementById("newsEmail");
toast(`Thanks — ${email.value} is on the list`);
email.value = "";
});
/* init */
renderDrawer();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nimbus — Modern Goods for Everyday Life</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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<!-- Announcement bar -->
<div class="announce" role="region" aria-label="Store announcement">
<p>Free carbon-neutral shipping on orders over <strong>$50</strong> · Use code <strong>NIMBUS10</strong> for 10% off</p>
</div>
<!-- Header -->
<header class="site-header">
<div class="wrap header-inner">
<a class="logo" href="#" aria-label="Nimbus home">
<span class="logo-mark" aria-hidden="true">
<svg viewBox="0 0 32 32" width="28" height="28" fill="none">
<path d="M9 21a6 6 0 0 1 .6-11.97A8 8 0 0 1 25 12.5 5.5 5.5 0 0 1 23.5 23H9Z" fill="currentColor"/>
</svg>
</span>
<span class="logo-text">Nimbus</span>
</a>
<nav class="primary-nav" aria-label="Primary">
<a href="#collections">Shop</a>
<a href="#trending">New In</a>
<a href="#deals">Deals</a>
<a href="#trust">About</a>
</nav>
<form class="search" role="search" aria-label="Search products">
<span class="search-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
</span>
<input type="search" id="search" name="q" placeholder="Search the store…" autocomplete="off" />
</form>
<div class="header-actions">
<button class="icon-btn" type="button" aria-label="Wishlist">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 21s-7.5-4.6-10-9.3C.4 8.3 2 4.6 5.5 4.6c2 0 3.4 1.1 4.5 2.6 1.1-1.5 2.5-2.6 4.5-2.6 3.5 0 5.1 3.7 3.5 7.1C19.5 16.4 12 21 12 21Z"/></svg>
</button>
<button class="icon-btn cart-btn" type="button" id="cartBtn" aria-label="Open cart, 0 items">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6h15l-1.5 9H8L6 3H3"/><circle cx="9" cy="20" r="1.4"/><circle cx="18" cy="20" r="1.4"/></svg>
<span class="cart-count" id="cartCount" aria-hidden="true">0</span>
</button>
</div>
</div>
</header>
<main id="main">
<!-- Hero -->
<section class="hero wrap" aria-labelledby="heroTitle">
<div class="hero-art" aria-hidden="true">
<div class="hero-blob blob-a"></div>
<div class="hero-blob blob-b"></div>
<svg class="hero-object" viewBox="0 0 240 240" width="280" height="280">
<defs>
<linearGradient id="hg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ffffff" stop-opacity=".9"/>
<stop offset="1" stop-color="#dfe6ff" stop-opacity=".6"/>
</linearGradient>
</defs>
<rect x="48" y="40" width="144" height="160" rx="22" fill="url(#hg)" stroke="rgba(255,255,255,.6)"/>
<circle cx="120" cy="108" r="42" fill="#3457ff" opacity=".18"/>
<path d="M96 132c8 12 40 12 48 0" stroke="#16181d" stroke-width="5" stroke-linecap="round" fill="none" opacity=".5"/>
<circle cx="104" cy="100" r="6" fill="#16181d" opacity=".6"/>
<circle cx="136" cy="100" r="6" fill="#16181d" opacity=".6"/>
</svg>
</div>
<div class="hero-copy">
<span class="eyebrow">Summer Drop · 2026</span>
<h1 id="heroTitle">Goods that feel<br><span class="hl">good to own.</span></h1>
<p>Thoughtfully designed essentials for your desk, your kitchen, and everywhere in between — built to last and priced to love.</p>
<div class="hero-cta">
<a class="btn btn-primary" href="#trending">Shop new arrivals</a>
<a class="btn btn-ghost" href="#collections">Browse collections</a>
</div>
<ul class="hero-meta">
<li><strong>4.9</strong> ★ avg rating</li>
<li><strong>30-day</strong> returns</li>
<li><strong>120k+</strong> happy customers</li>
</ul>
</div>
</section>
<!-- Featured collections -->
<section class="section wrap" id="collections" aria-labelledby="collTitle">
<div class="section-head">
<h2 id="collTitle">Shop by collection</h2>
<a class="link-more" href="#trending">View all →</a>
</div>
<div class="collections">
<a class="coll-tile tile-1" href="#trending">
<span class="coll-tag">42 items</span>
<span class="coll-name">Desk & Office</span>
</a>
<a class="coll-tile tile-2" href="#trending">
<span class="coll-tag">28 items</span>
<span class="coll-name">Kitchen</span>
</a>
<a class="coll-tile tile-3" href="#trending">
<span class="coll-tag">17 items</span>
<span class="coll-name">Lighting</span>
</a>
<a class="coll-tile tile-4" href="#trending">
<span class="coll-tag">35 items</span>
<span class="coll-name">Everyday Carry</span>
</a>
</div>
</section>
<!-- Deals strip with countdown -->
<section class="section wrap" id="deals" aria-labelledby="dealTitle">
<div class="deal-strip">
<div class="deal-copy">
<span class="deal-flag">Flash Sale</span>
<h2 id="dealTitle">Up to 40% off select goods</h2>
<p>Limited stock. The clock is real — when it hits zero, prices snap back.</p>
</div>
<div class="countdown" id="countdown" role="timer" aria-live="polite" aria-label="Time remaining in flash sale">
<div class="cd-unit"><span class="cd-num" id="cdH">00</span><span class="cd-lbl">hrs</span></div>
<span class="cd-sep" aria-hidden="true">:</span>
<div class="cd-unit"><span class="cd-num" id="cdM">00</span><span class="cd-lbl">min</span></div>
<span class="cd-sep" aria-hidden="true">:</span>
<div class="cd-unit"><span class="cd-num" id="cdS">00</span><span class="cd-lbl">sec</span></div>
</div>
</div>
</section>
<!-- Trending carousel -->
<section class="section wrap" id="trending" aria-labelledby="trendTitle">
<div class="section-head">
<h2 id="trendTitle">Trending now</h2>
<div class="carousel-ctrls">
<button class="round-btn" type="button" id="prevBtn" aria-label="Scroll products left">‹</button>
<button class="round-btn" type="button" id="nextBtn" aria-label="Scroll products right">›</button>
</div>
</div>
<ul class="product-rail" id="rail" aria-label="Trending products">
<!-- product cards injected by script.js -->
</ul>
</section>
<!-- Trust badges -->
<section class="section wrap" id="trust" aria-labelledby="trustTitle">
<h2 id="trustTitle" class="sr-only">Why shop with Nimbus</h2>
<ul class="trust-row">
<li class="trust-item">
<span class="trust-ic" aria-hidden="true">🚚</span>
<div><strong>Free shipping</strong><span>On orders over $50</span></div>
</li>
<li class="trust-item">
<span class="trust-ic" aria-hidden="true">🔒</span>
<div><strong>Secure checkout</strong><span>256-bit encryption</span></div>
</li>
<li class="trust-item">
<span class="trust-ic" aria-hidden="true">↩️</span>
<div><strong>Easy returns</strong><span>30 days, no questions</span></div>
</li>
<li class="trust-item">
<span class="trust-ic" aria-hidden="true">🌱</span>
<div><strong>Carbon neutral</strong><span>Every order offset</span></div>
</li>
</ul>
</section>
</main>
<footer class="site-footer">
<div class="wrap footer-inner">
<div class="footer-brand">
<span class="logo-text">Nimbus</span>
<p>Modern goods for everyday life. Designed in Lisbon, shipped worldwide.</p>
<form class="news" id="newsForm" aria-label="Newsletter signup">
<input type="email" id="newsEmail" name="email" placeholder="you@email.com" aria-label="Email address" required />
<button class="btn btn-primary" type="submit">Join</button>
</form>
</div>
<nav class="footer-cols" aria-label="Footer">
<div>
<h3>Shop</h3>
<a href="#collections">New arrivals</a>
<a href="#deals">Deals</a>
<a href="#collections">Collections</a>
</div>
<div>
<h3>Support</h3>
<a href="#trust">Shipping</a>
<a href="#trust">Returns</a>
<a href="#main">Contact</a>
</div>
<div>
<h3>Company</h3>
<a href="#trust">About</a>
<a href="#main">Careers</a>
<a href="#main">Press</a>
</div>
</nav>
</div>
<div class="wrap footer-bottom">
<p>© 2026 Nimbus Goods Co. Fictional storefront demo.</p>
<p class="pay-badges" aria-label="Accepted payment methods">Visa · Mastercard · Amex · PayPal</p>
</div>
</footer>
<!-- Cart drawer -->
<div class="drawer-overlay" id="overlay" hidden></div>
<aside class="cart-drawer" id="drawer" aria-label="Shopping cart" aria-hidden="true" tabindex="-1">
<div class="drawer-head">
<h2>Your cart</h2>
<button class="icon-btn" type="button" id="closeCart" aria-label="Close cart">✕</button>
</div>
<ul class="drawer-items" id="drawerItems" aria-live="polite"></ul>
<p class="drawer-empty" id="drawerEmpty">Your cart is empty — add something lovely.</p>
<div class="drawer-foot">
<div class="drawer-total"><span>Subtotal</span><strong id="drawerTotal">$0.00</strong></div>
<button class="btn btn-primary btn-block" type="button" id="checkoutBtn">Secure checkout</button>
<p class="drawer-note">🔒 Encrypted & safe · Free returns within 30 days</p>
</div>
</aside>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Storefront Home
A polished retail landing for the fictional “Nimbus Goods Co.” It opens with an announcement bar and a sticky, blurred header carrying the logo, primary nav, a search field, and a cart button with a live item-count badge. The hero pairs bold marketing copy and dual CTAs with a CSS-gradient “product” illustration, backed by trust metrics. Below it sit four gradient collection tiles, a dark flash-sale strip with a real ticking countdown, and a “Trending now” rail of product cards built entirely from inline-SVG silhouettes on soft tinted tiles — no external images anywhere.
Every interaction works. Clicking Add to cart bumps the header count with a pulse animation, shows a toast, and pushes the item into a slide-out cart drawer where quantity steppers, line totals, item removal, and a live subtotal all update instantly. The drawer traps focus, closes on Escape or overlay click, and restores focus to its trigger. Carousel arrows scroll the product rail with snap points, the search box filters products by name or category in real time, wishlist hearts toggle their pressed state, and the footer newsletter validates and confirms via toast.
The layout is responsive down to ~360px — collection and trust grids collapse, the nav hides on narrow screens, and the deal strip stacks — with WCAG-AA contrast, visible focus rings, landmark roles, and reduced-motion support throughout.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.