Shop — Checkout
A trustworthy multi-step storefront checkout that walks shoppers from contact to shipping, payment, and a final review with a progress indicator and per-step validation. It auto-formats card number, expiry, CVC, phone, and ZIP, offers priced shipping methods with delivery estimates, applies a promo code, and keeps a live order-summary sidebar in sync before a simulated place-order success state with order number and arrival date.
MCP
Code
:root {
--bg: #ffffff;
--surface: #ffffff;
--tint: #f6f7fb;
--ink: #16181d;
--muted: #6b7280;
--brand: #3457ff;
--brand-d: #2742d6;
--brand-soft: #eef1ff;
--sale: #e0245e;
--ok: #1f9d55;
--ok-soft: #e7f6ee;
--line: rgba(16, 18, 29, .1);
--line-strong: rgba(16, 18, 29, .18);
--err: #d12953;
--err-soft: #fdecf1;
--shadow-sm: 0 1px 2px rgba(16, 18, 29, .06), 0 1px 3px rgba(16, 18, 29, .05);
--shadow-md: 0 8px 24px rgba(16, 18, 29, .08);
--shadow-lg: 0 24px 60px rgba(16, 18, 29, .18);
--radius: 14px;
--radius-sm: 10px;
}
* { 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;
background:
radial-gradient(120% 60% at 100% 0%, #eef1ff 0%, rgba(238, 241, 255, 0) 55%),
radial-gradient(120% 60% at 0% 0%, #f3fbf6 0%, rgba(243, 251, 246, 0) 50%),
var(--tint);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.wrap { width: min(1120px, 100% - 40px); margin-inline: auto; }
.skip {
position: absolute;
left: -999px;
top: 8px;
z-index: 50;
background: var(--ink);
color: #fff;
padding: 10px 16px;
border-radius: 8px;
}
.skip:focus { left: 16px; }
.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;
}
:focus-visible {
outline: 3px solid color-mix(in srgb, var(--brand) 55%, white);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- TOP BAR ---------- */
.topbar {
background: rgba(255, 255, 255, .85);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
}
.topbar-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
text-decoration: none;
color: var(--ink);
font-weight: 800;
letter-spacing: -.02em;
font-size: 1.15rem;
}
.brand-mark {
display: grid;
place-items: center;
width: 34px; height: 34px;
border-radius: 10px;
color: #fff;
background: linear-gradient(135deg, var(--brand), #6f56ff);
box-shadow: var(--shadow-sm);
}
.secure-pill {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: .82rem;
font-weight: 600;
color: var(--ok);
background: var(--ok-soft);
padding: 7px 12px;
border-radius: 999px;
}
/* ---------- LAYOUT ---------- */
.layout {
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(320px, 1fr);
gap: 40px;
align-items: start;
padding: 32px 0 64px;
}
.page-title {
font-size: clamp(1.6rem, 4vw, 2.1rem);
letter-spacing: -.03em;
margin: 0 0 22px;
}
/* ---------- STEPPER ---------- */
.steps {
display: flex;
list-style: none;
margin: 0 0 28px;
padding: 0;
counter-reset: none;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1;
position: relative;
font-size: .82rem;
font-weight: 600;
color: var(--muted);
text-align: center;
}
.step::before {
content: "";
position: absolute;
top: 17px;
left: -50%;
width: 100%;
height: 2px;
background: var(--line);
z-index: 0;
}
.step:first-child::before { display: none; }
.step-dot {
position: relative;
z-index: 1;
width: 36px; height: 36px;
border-radius: 50%;
display: grid;
place-items: center;
background: #fff;
border: 2px solid var(--line-strong);
color: var(--muted);
font-weight: 700;
transition: background .25s, border-color .25s, color .25s, transform .25s;
}
.step-check { display: none; }
.step.is-active { color: var(--ink); }
.step.is-active .step-dot {
border-color: var(--brand);
color: var(--brand);
box-shadow: 0 0 0 5px var(--brand-soft);
}
.step.is-done .step-dot {
background: var(--brand);
border-color: var(--brand);
color: #fff;
}
.step.is-done .step-num { display: none; }
.step.is-done .step-check { display: block; }
.step.is-done::before { background: var(--brand); }
/* ---------- PANELS ---------- */
.flow > form {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.panel {
border: 0;
margin: 0;
padding: 28px;
min-inline-size: 0;
}
.panel[hidden] { display: none; }
.panel.is-active { animation: slideIn .35s ease both; }
@keyframes slideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.panel-title {
font-size: 1.15rem;
font-weight: 700;
letter-spacing: -.02em;
padding: 0;
margin: 0 0 4px;
}
.panel-hint {
color: var(--muted);
font-size: .88rem;
margin: 0 0 20px;
}
.secure-line {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--ok);
background: var(--ok-soft);
padding: 8px 12px;
border-radius: 8px;
font-weight: 500;
}
.sub-title {
font-size: .82rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted);
padding: 0;
margin: 0 0 12px;
}
/* ---------- FIELDS ---------- */
.field { margin-bottom: 16px; }
.field label {
display: block;
font-size: .85rem;
font-weight: 600;
margin-bottom: 6px;
}
.field .opt { color: var(--muted); font-weight: 400; }
.field input,
.field select {
width: 100%;
font: inherit;
color: var(--ink);
padding: 12px 14px;
border: 1.5px solid var(--line-strong);
border-radius: var(--radius-sm);
background: #fff;
transition: border-color .18s, box-shadow .18s;
}
.field input::placeholder { color: #aab0bd; }
.field input:hover,
.field select:hover { border-color: var(--brand); }
.field input:focus,
.field select:focus {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 4px var(--brand-soft);
}
.field.invalid input,
.field.invalid select {
border-color: var(--err);
background: var(--err-soft);
}
.field.invalid input:focus,
.field.invalid select:focus {
box-shadow: 0 0 0 4px var(--err-soft);
}
.error {
color: var(--err);
font-size: .78rem;
font-weight: 600;
margin: 6px 0 0;
min-height: 0;
}
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.grid-3 { display: grid; grid-template-columns: 1.4fr .8fr .9fr; gap: 16px; }
.check {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: .88rem;
color: var(--ink);
cursor: pointer;
user-select: none;
}
.check input {
width: 18px; height: 18px;
margin: 1px 0 0;
accent-color: var(--brand);
flex: none;
}
.check a { color: var(--brand); font-weight: 600; }
.terms { margin-top: 16px; }
/* ---------- SHIPPING METHODS ---------- */
.ship-methods {
border: 0;
padding: 0;
margin: 24px 0 0;
}
.ship-opt {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border: 1.5px solid var(--line-strong);
border-radius: var(--radius-sm);
margin-bottom: 10px;
cursor: pointer;
transition: border-color .18s, background .18s, box-shadow .18s;
}
.ship-opt:hover { border-color: var(--brand); }
.ship-opt input {
width: 18px; height: 18px;
accent-color: var(--brand);
flex: none;
}
.ship-opt.is-selected {
border-color: var(--brand);
background: var(--brand-soft);
box-shadow: 0 0 0 1px var(--brand) inset;
}
.ship-body { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.ship-name { font-weight: 700; display: flex; align-items: center; gap: 8px; }
.ship-eta { font-size: .82rem; color: var(--muted); }
.ship-price { font-weight: 700; font-variant-numeric: tabular-nums; }
.free-chip {
font-size: .68rem;
font-weight: 800;
letter-spacing: .04em;
color: var(--ok);
background: var(--ok-soft);
padding: 2px 7px;
border-radius: 999px;
}
/* ---------- CARD PREVIEW ---------- */
.card-preview {
position: relative;
aspect-ratio: 1.6 / 1;
max-width: 340px;
margin: 0 0 22px;
border-radius: 16px;
padding: 20px;
color: #fff;
background:
radial-gradient(120% 120% at 0% 0%, #6f56ff 0%, rgba(111, 86, 255, 0) 55%),
linear-gradient(135deg, #2742d6, #16181d 90%);
box-shadow: var(--shadow-md);
overflow: hidden;
display: grid;
grid-template-rows: auto 1fr auto;
}
.card-preview::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(80% 120% at 120% 120%, rgba(224, 36, 94, .35), transparent 60%);
pointer-events: none;
}
.card-brand {
font-weight: 800;
letter-spacing: .14em;
font-size: .82rem;
align-self: start;
justify-self: end;
}
.card-chip {
width: 42px; height: 30px;
border-radius: 6px;
background: linear-gradient(135deg, #f4d58d, #d9a441);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .3);
margin-top: 6px;
}
.card-number {
font-size: 1.2rem;
letter-spacing: .12em;
font-variant-numeric: tabular-nums;
font-weight: 600;
align-self: end;
}
.card-meta {
display: flex;
justify-content: space-between;
font-size: .78rem;
letter-spacing: .04em;
text-transform: uppercase;
color: rgba(255, 255, 255, .85);
margin-top: 12px;
}
/* ---------- REVIEW ---------- */
.review-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.review-card {
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 14px 16px;
background: var(--tint);
}
.review-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.review-head h3 {
font-size: .72rem;
text-transform: uppercase;
letter-spacing: .07em;
color: var(--muted);
margin: 0;
}
.review-body {
margin: 0;
font-size: .9rem;
font-weight: 500;
white-space: pre-line;
}
.link-btn {
border: 0;
background: none;
color: var(--brand);
font: inherit;
font-weight: 700;
font-size: .8rem;
cursor: pointer;
padding: 2px 4px;
border-radius: 6px;
}
.link-btn:hover { text-decoration: underline; }
/* ---------- ACTIONS ---------- */
.actions {
display: flex;
align-items: center;
gap: 12px;
padding: 20px 28px 26px;
border-top: 1px solid var(--line);
background: var(--tint);
}
.btn {
font: inherit;
font-weight: 700;
border: 0;
border-radius: var(--radius-sm);
padding: 13px 20px;
cursor: pointer;
transition: background .18s, transform .12s, box-shadow .18s, color .18s;
display: inline-flex;
align-items: center;
gap: 8px;
justify-content: center;
}
.btn:active { transform: translateY(1px); }
.btn.primary {
background: var(--brand);
color: #fff;
box-shadow: 0 6px 16px rgba(52, 87, 255, .28);
margin-left: auto;
}
.btn.primary:hover { background: var(--brand-d); }
.btn.place .lock-ic { display: inline-flex; }
.btn.ghost {
background: #fff;
color: var(--ink);
border: 1.5px solid var(--line-strong);
}
.btn.ghost:hover { border-color: var(--ink); }
.btn.small { padding: 12px 16px; font-size: .85rem; }
.btn.is-loading {
pointer-events: none;
position: relative;
color: transparent;
}
.btn.is-loading::after {
content: "";
position: absolute;
width: 18px; height: 18px;
border: 2.5px solid rgba(255, 255, 255, .45);
border-top-color: #fff;
border-radius: 50%;
animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ---------- SUMMARY SIDEBAR ---------- */
.summary {
position: sticky;
top: 88px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
overflow: hidden;
}
.summary-toggle {
display: none;
width: 100%;
align-items: center;
gap: 10px;
padding: 16px 18px;
background: var(--tint);
border: 0;
border-bottom: 1px solid var(--line);
font: inherit;
font-weight: 700;
cursor: pointer;
text-align: left;
}
.summary-toggle-total { margin-left: auto; font-variant-numeric: tabular-nums; }
.summary-toggle .chev { transition: transform .25s; flex: none; }
.summary-toggle[aria-expanded="true"] .chev { transform: rotate(180deg); }
.summary-body { padding: 22px; }
.summary-title {
font-size: 1.05rem;
letter-spacing: -.02em;
margin: 0 0 16px;
}
.cart { list-style: none; margin: 0 0 18px; padding: 0; }
.cart-item {
display: grid;
grid-template-columns: 56px 1fr auto;
gap: 12px;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid var(--line);
}
.cart-thumb {
width: 56px; height: 56px;
border-radius: 10px;
position: relative;
display: grid;
place-items: center;
font-size: 1.5rem;
border: 1px solid var(--line);
}
.cart-qty {
position: absolute;
top: -7px; right: -7px;
min-width: 20px; height: 20px;
padding: 0 5px;
border-radius: 999px;
background: var(--ink);
color: #fff;
font-size: .7rem;
font-weight: 800;
display: grid;
place-items: center;
}
.cart-info { min-width: 0; }
.cart-name {
font-weight: 600;
font-size: .9rem;
line-height: 1.3;
}
.cart-var { font-size: .78rem; color: var(--muted); }
.cart-price {
font-weight: 700;
font-variant-numeric: tabular-nums;
font-size: .9rem;
white-space: nowrap;
}
.promo {
display: flex;
gap: 8px;
margin: 4px 0 4px;
}
.promo input {
flex: 1;
min-width: 0;
font: inherit;
padding: 11px 13px;
border: 1.5px solid var(--line-strong);
border-radius: var(--radius-sm);
background: #fff;
}
.promo input:focus {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 4px var(--brand-soft);
}
.promo-msg {
font-size: .8rem;
font-weight: 600;
margin: 6px 0 14px;
min-height: 1.1em;
}
.promo-msg.ok { color: var(--ok); }
.promo-msg.bad { color: var(--err); }
.totals {
margin: 6px 0 0;
padding-top: 14px;
border-top: 1px solid var(--line);
}
.totals .row {
display: flex;
justify-content: space-between;
margin: 0 0 9px;
}
.totals dt { color: var(--muted); font-size: .9rem; margin: 0; }
.totals dd { margin: 0; font-weight: 600; font-variant-numeric: tabular-nums; }
.totals .discount dd { color: var(--ok); }
.totals .total {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid var(--line);
}
.totals .total dt { color: var(--ink); font-weight: 800; font-size: 1rem; }
.totals .total dd { font-size: 1.2rem; font-weight: 800; }
.trust {
list-style: none;
margin: 18px 0 0;
padding: 16px 0 0;
border-top: 1px solid var(--line);
display: grid;
gap: 9px;
}
.trust li {
display: flex;
align-items: center;
gap: 9px;
font-size: .82rem;
color: var(--muted);
font-weight: 500;
}
.trust svg { color: var(--ok); flex: none; }
/* ---------- SUCCESS ---------- */
.success {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
padding: 20px;
background: rgba(16, 18, 29, .55);
backdrop-filter: blur(4px);
animation: fade .3s ease both;
}
.success[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.success-card {
width: min(440px, 100%);
background: #fff;
border-radius: 20px;
padding: 36px 32px 30px;
text-align: center;
box-shadow: var(--shadow-lg);
animation: pop .4s cubic-bezier(.2, 1.2, .4, 1) both;
}
@keyframes pop {
from { opacity: 0; transform: translateY(14px) scale(.96); }
to { opacity: 1; transform: none; }
}
.success-burst {
width: 72px; height: 72px;
margin: 0 auto 18px;
border-radius: 50%;
display: grid;
place-items: center;
color: #fff;
background: linear-gradient(135deg, var(--ok), #16a34a);
box-shadow: 0 10px 26px rgba(31, 157, 85, .4);
animation: burst .5s cubic-bezier(.2, 1.4, .4, 1) both;
}
@keyframes burst {
0% { transform: scale(0); }
60% { transform: scale(1.12); }
100% { transform: scale(1); }
}
.success-card h2 {
font-size: 1.5rem;
letter-spacing: -.02em;
margin: 0 0 6px;
}
.success-sub { color: var(--muted); margin: 0 0 22px; }
.success-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
background: var(--tint);
border-radius: 12px;
padding: 16px 12px;
margin-bottom: 22px;
}
.success-meta > div { display: flex; flex-direction: column; gap: 3px; }
.success-k {
font-size: .68rem;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted);
font-weight: 700;
}
.success-v { font-weight: 700; font-size: .92rem; font-variant-numeric: tabular-nums; }
.success-card .btn.primary { width: 100%; margin: 0; }
/* ---------- TOAST ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 120%);
background: var(--ink);
color: #fff;
padding: 12px 20px;
border-radius: 999px;
font-weight: 600;
font-size: .9rem;
box-shadow: var(--shadow-lg);
z-index: 70;
transition: transform .35s cubic-bezier(.2, 1, .4, 1);
max-width: calc(100% - 32px);
}
.toast.show { transform: translate(-50%, 0); }
/* ---------- RESPONSIVE ---------- */
@media (max-width: 880px) {
.layout {
grid-template-columns: 1fr;
gap: 20px;
padding-top: 22px;
}
.summary {
position: static;
order: -1;
}
.summary-toggle { display: flex; }
.summary-body { display: none; }
.summary.is-open .summary-body { display: block; }
.summary-title { display: none; }
}
@media (max-width: 560px) {
.wrap { width: 100% - 32px; width: calc(100% - 32px); }
.grid-2, .grid-3 { grid-template-columns: 1fr; }
.review-grid { grid-template-columns: 1fr; }
.panel, .summary-body { padding: 20px; }
.actions { padding: 16px 20px 22px; flex-wrap: wrap; }
.btn.primary { width: 100%; margin-left: 0; }
.btn.ghost { width: 100%; }
.step-label { font-size: .72rem; }
.card-preview { max-width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
*, *::after, *::before {
animation-duration: .001ms !important;
transition-duration: .001ms !important;
}
}(function () {
"use strict";
/* ---------- Cart data (fictional) ---------- */
var CART = [
{ id: "p1", name: "Aurora Field Pack 32L", variant: "Slate / Regular", price: 168.0, qty: 1, emoji: "🎒", tint: "#eef1ff" },
{ id: "p2", name: "Drift Merino Crew", variant: "Moss / M", price: 64.0, qty: 2, emoji: "👕", tint: "#e7f6ee" },
{ id: "p3", name: "Trail Bottle 750ml", variant: "Ember", price: 28.0, qty: 1, emoji: "🍶", tint: "#fdecf1" }
];
var TAX_RATE = 0.0825;
var PROMO = { code: "TRAIL10", rate: 0.1 };
var state = { step: 1, ship: 0, shipName: "Standard", shipEta: "5–7 business days", discount: 0 };
/* ---------- Helpers ---------- */
var $ = function (s, c) { return (c || document).querySelector(s); };
var $$ = function (s, c) { return Array.prototype.slice.call((c || document).querySelectorAll(s)); };
var money = function (n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
/* ---------- Render cart + totals ---------- */
function subtotal() {
return CART.reduce(function (s, i) { return s + i.price * i.qty; }, 0);
}
function renderCart() {
var list = $("#cartList");
list.innerHTML = "";
CART.forEach(function (item) {
var li = document.createElement("li");
li.className = "cart-item";
li.innerHTML =
'<span class="cart-thumb" style="background:' + item.tint + '">' + item.emoji +
'<span class="cart-qty">' + item.qty + '</span></span>' +
'<span class="cart-info"><span class="cart-name">' + item.name + '</span>' +
'<span class="cart-var">' + item.variant + '</span></span>' +
'<span class="cart-price">' + money(item.price * item.qty) + '</span>';
list.appendChild(li);
});
}
function recalc() {
var sub = subtotal();
var disc = state.discount ? sub * state.discount : 0;
var taxable = sub - disc;
var tax = taxable * TAX_RATE;
var total = taxable + tax + state.ship;
$("#sumSubtotal").textContent = money(sub);
$("#sumShipping").textContent = state.ship === 0 ? "FREE" : money(state.ship);
$("#sumTax").textContent = money(tax);
$("#sumTotal").textContent = money(total);
$("#toggleTotal").textContent = money(total);
$("#placeTotal").textContent = money(total);
var dRow = $("#discountRow");
if (disc > 0) {
dRow.hidden = false;
$("#sumDiscount").textContent = "−" + money(disc);
} else {
dRow.hidden = true;
}
return total;
}
/* ---------- Validation ---------- */
function setError(id, msg) {
var input = $("#" + id);
var field = input.closest(".field");
var err = $("#" + id + "-err");
if (msg) {
if (field) field.classList.add("invalid");
if (err) err.textContent = msg;
input.setAttribute("aria-invalid", "true");
} else {
if (field) field.classList.remove("invalid");
if (err) err.textContent = "";
input.removeAttribute("aria-invalid");
}
}
var validators = {
1: function () {
var ok = true;
var email = $("#email").value.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) { setError("email", "Enter a valid email address."); ok = false; }
else setError("email", "");
var phone = $("#phone").value.replace(/\D/g, "");
if (phone.length < 10) { setError("phone", "Enter a 10-digit phone number."); ok = false; }
else setError("phone", "");
return ok;
},
2: function () {
var ok = true;
[["first", "First name is required."], ["last", "Last name is required."], ["address", "Street address is required."], ["city", "City is required."]].forEach(function (f) {
if (!$("#" + f[0]).value.trim()) { setError(f[0], f[1]); ok = false; } else setError(f[0], "");
});
if (!$("#state").value) { setError("state", "Select a state."); ok = false; } else setError("state", "");
if (!/^\d{5}$/.test($("#zip").value.trim())) { setError("zip", "Enter a 5-digit ZIP."); ok = false; } else setError("zip", "");
return ok;
},
3: function () {
var ok = true;
if (!$("#cardname").value.trim()) { setError("cardname", "Name on card is required."); ok = false; } else setError("cardname", "");
var num = $("#cardnum").value.replace(/\s/g, "");
if (num.length < 15 || !luhn(num)) { setError("cardnum", "Enter a valid card number."); ok = false; } else setError("cardnum", "");
var exp = $("#exp").value.trim();
if (!validExpiry(exp)) { setError("exp", "Enter a valid future expiry."); ok = false; } else setError("exp", "");
var cvc = $("#cvc").value.trim();
if (!/^\d{3,4}$/.test(cvc)) { setError("cvc", "3–4 digits."); ok = false; } else setError("cvc", "");
return ok;
},
4: function () {
if (!$("#terms").checked) { $("#terms-err").textContent = "Please accept the terms to continue."; return false; }
$("#terms-err").textContent = "";
return true;
}
};
function luhn(num) {
var sum = 0, alt = false;
for (var i = num.length - 1; i >= 0; i--) {
var d = parseInt(num.charAt(i), 10);
if (isNaN(d)) return false;
if (alt) { d *= 2; if (d > 9) d -= 9; }
sum += d; alt = !alt;
}
return sum % 10 === 0;
}
function validExpiry(v) {
var m = /^(\d{2})\/(\d{2})$/.exec(v);
if (!m) return false;
var mm = parseInt(m[1], 10), yy = parseInt(m[2], 10);
if (mm < 1 || mm > 12) return false;
var now = new Date();
var fullYear = 2000 + yy;
var curY = now.getFullYear(), curM = now.getMonth() + 1;
if (fullYear < curY || (fullYear === curY && mm < curM)) return false;
return true;
}
/* ---------- Input formatting ---------- */
$("#cardnum").addEventListener("input", function (e) {
var v = e.target.value.replace(/\D/g, "").slice(0, 16);
e.target.value = v.replace(/(.{4})/g, "$1 ").trim();
var prev = $("#cardNumPreview");
prev.textContent = (v + "••••••••••••••••").slice(0, 16).replace(/(.{4})/g, "$1 ").trim();
var brand = detectBrand(v);
$("#cardBrand").textContent = brand;
});
function detectBrand(v) {
if (/^4/.test(v)) return "VISA";
if (/^5[1-5]/.test(v) || /^2[2-7]/.test(v)) return "MASTERCARD";
if (/^3[47]/.test(v)) return "AMEX";
if (/^6/.test(v)) return "DISCOVER";
return "CARD";
}
$("#exp").addEventListener("input", function (e) {
var v = e.target.value.replace(/\D/g, "").slice(0, 4);
if (v.length >= 3) v = v.slice(0, 2) + "/" + v.slice(2);
e.target.value = v;
$("#cardExpPreview").textContent = v || "MM/YY";
});
$("#cvc").addEventListener("input", function (e) {
e.target.value = e.target.value.replace(/\D/g, "").slice(0, 4);
});
$("#cardname").addEventListener("input", function (e) {
$("#cardHolderPreview").textContent = (e.target.value.trim() || "YOUR NAME").toUpperCase();
});
$("#zip").addEventListener("input", function (e) {
e.target.value = e.target.value.replace(/\D/g, "").slice(0, 5);
});
$("#phone").addEventListener("input", function (e) {
var v = e.target.value.replace(/\D/g, "").slice(0, 10);
if (v.length > 6) e.target.value = "(" + v.slice(0, 3) + ") " + v.slice(3, 6) + "-" + v.slice(6);
else if (v.length > 3) e.target.value = "(" + v.slice(0, 3) + ") " + v.slice(3);
else if (v.length > 0) e.target.value = "(" + v;
else e.target.value = v;
});
/* ---------- Shipping radios ---------- */
$$('input[name="ship"]').forEach(function (r) {
r.addEventListener("change", function () {
$$(".ship-opt").forEach(function (o) { o.classList.remove("is-selected"); });
r.closest(".ship-opt").classList.add("is-selected");
state.ship = parseFloat(r.dataset.price);
state.shipName = r.closest(".ship-opt").querySelector(".ship-name").childNodes[0].textContent.trim();
state.shipEta = r.dataset.eta;
recalc();
});
});
/* ---------- Promo ---------- */
$("#applyPromo").addEventListener("click", function () {
var input = $("#promo");
var msg = $("#promoMsg");
var code = input.value.trim().toUpperCase();
if (!code) { msg.textContent = "Enter a code to apply."; msg.className = "promo-msg bad"; return; }
if (code === PROMO.code) {
state.discount = PROMO.rate;
msg.textContent = "✓ TRAIL10 applied — 10% off your gear.";
msg.className = "promo-msg ok";
toast("Promo code applied");
} else {
state.discount = 0;
msg.textContent = "That code isn't valid. Try TRAIL10.";
msg.className = "promo-msg bad";
}
recalc();
});
/* ---------- Step navigation ---------- */
var NEXT_LABEL = { 1: "Continue to shipping", 2: "Continue to payment", 3: "Review order" };
function goto(step) {
state.step = step;
$$(".panel").forEach(function (p) {
var n = parseInt(p.dataset.panel, 10);
p.hidden = n !== step;
p.classList.toggle("is-active", n === step);
});
$$(".step").forEach(function (s) {
var n = parseInt(s.dataset.step, 10);
s.classList.toggle("is-active", n === step);
s.classList.toggle("is-done", n < step);
if (n === step) s.setAttribute("aria-current", "step");
else s.removeAttribute("aria-current");
});
$("#backBtn").hidden = step === 1;
if (step === 4) {
$("#nextBtn").hidden = true;
$("#placeBtn").hidden = false;
fillReview();
} else {
$("#nextBtn").hidden = false;
$("#placeBtn").hidden = true;
$("#nextBtn").textContent = NEXT_LABEL[step];
}
var panel = $('.panel[data-panel="' + step + '"]');
var firstInput = panel.querySelector("input, select");
if (firstInput) firstInput.focus({ preventScroll: true });
window.scrollTo({ top: 0, behavior: "smooth" });
}
function fillReview() {
$("#rv-contact").textContent = $("#email").value + "\n" + $("#phone").value;
$("#rv-ship").textContent =
$("#first").value + " " + $("#last").value + "\n" +
$("#address").value + ($("#address2").value ? " " + $("#address2").value : "") + "\n" +
$("#city").value + ", " + $("#state").value + " " + $("#zip").value;
$("#rv-method").textContent = state.shipName + "\nArrives in " + state.shipEta + "\n" + (state.ship === 0 ? "FREE" : money(state.ship));
var num = $("#cardnum").value.replace(/\s/g, "");
$("#rv-pay").textContent = detectBrand(num) + " ending " + (num.slice(-4) || "····") + "\n" + ($("#cardname").value || "—");
}
$("#nextBtn").addEventListener("click", function () {
var ok = validators[state.step] ? validators[state.step]() : true;
if (!ok) {
var firstInvalid = $('.panel[data-panel="' + state.step + '"] .invalid input, .panel[data-panel="' + state.step + '"] .invalid select');
if (firstInvalid) firstInvalid.focus();
toast("Please fix the highlighted fields");
return;
}
if (state.step < 4) goto(state.step + 1);
});
$("#backBtn").addEventListener("click", function () {
if (state.step > 1) goto(state.step - 1);
});
$$('[data-goto]').forEach(function (b) {
b.addEventListener("click", function () { goto(parseInt(b.dataset.goto, 10)); });
});
/* Allow Enter on stepper steps that are already completed */
$$(".step").forEach(function (s) {
s.addEventListener("click", function () {
var n = parseInt(s.dataset.step, 10);
if (n < state.step) goto(n);
});
});
/* ---------- Place order ---------- */
$("#checkoutForm").addEventListener("submit", function (e) {
e.preventDefault();
if (!validators[4]()) { toast("Please accept the terms"); return; }
var btn = $("#placeBtn");
btn.classList.add("is-loading");
var total = recalc();
setTimeout(function () {
btn.classList.remove("is-loading");
showSuccess(total);
}, 1400);
});
function showSuccess(total) {
var order = "#NW-" + String(Math.floor(100000 + Math.random() * 899999));
var d = new Date();
var addDays = state.shipName === "Overnight" ? 1 : state.shipName === "Express" ? 2 : 6;
d.setDate(d.getDate() + addDays);
var eta = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
$("#successName").textContent = $("#first").value.trim() || "friend";
$("#successEmail").textContent = $("#email").value.trim() || "your inbox";
$("#successOrder").textContent = order;
$("#successTotal").textContent = money(total);
$("#successEta").textContent = eta;
var ov = $("#success");
ov.hidden = false;
$("#successDone").focus();
}
$("#successDone").addEventListener("click", function () {
$("#success").hidden = true;
toast("Demo reset — happy trails!");
goto(1);
$("#checkoutForm").reset();
state.ship = 0; state.discount = 0; state.shipName = "Standard"; state.shipEta = "5–7 business days";
$$(".ship-opt").forEach(function (o, i) { o.classList.toggle("is-selected", i === 0); });
$("#promoMsg").textContent = "";
$("#cardNumPreview").textContent = "•••• •••• •••• ••••";
$("#cardExpPreview").textContent = "MM/YY";
$("#cardHolderPreview").textContent = "YOUR NAME";
$("#cardBrand").textContent = "CARD";
recalc();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !$("#success").hidden) $("#successDone").click();
});
/* ---------- Mobile summary toggle ---------- */
$("#summaryToggle").addEventListener("click", function () {
var sum = this.closest(".summary");
var open = sum.classList.toggle("is-open");
this.setAttribute("aria-expanded", String(open));
});
/* ---------- Init ---------- */
renderCart();
recalc();
goto(1);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Checkout — Northwind Supply Co.</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" href="#checkout">Skip to checkout</a>
<header class="topbar" role="banner">
<div class="wrap topbar-inner">
<a class="brand" href="#" aria-label="Northwind Supply Co. home">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 4 7v10l8 5 8-5V7z"/><path d="M12 22V12"/><path d="m4 7 8 5 8-5"/></svg>
</span>
<span class="brand-name">Northwind</span>
</a>
<div class="secure-pill" role="note" aria-label="Secure checkout">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
<span>Secure checkout</span>
</div>
</div>
</header>
<main id="checkout" class="wrap layout" role="main">
<section class="flow" aria-label="Checkout steps">
<h1 class="page-title">Checkout</h1>
<!-- Progress indicator -->
<ol class="steps" id="stepper" aria-label="Checkout progress">
<li class="step is-active" data-step="1" aria-current="step">
<span class="step-dot"><span class="step-num">1</span><svg class="step-check" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></span>
<span class="step-label">Contact</span>
</li>
<li class="step" data-step="2">
<span class="step-dot"><span class="step-num">2</span><svg class="step-check" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></span>
<span class="step-label">Shipping</span>
</li>
<li class="step" data-step="3">
<span class="step-dot"><span class="step-num">3</span><svg class="step-check" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></span>
<span class="step-label">Payment</span>
</li>
<li class="step" data-step="4">
<span class="step-dot"><span class="step-num">4</span><svg class="step-check" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></span>
<span class="step-label">Review</span>
</li>
</ol>
<form id="checkoutForm" novalidate>
<!-- STEP 1 — CONTACT -->
<fieldset class="panel is-active" data-panel="1">
<legend class="panel-title">Contact information</legend>
<p class="panel-hint">We'll email your receipt and shipping updates here.</p>
<div class="field">
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email" inputmode="email" placeholder="[email protected]" aria-describedby="email-err" />
<p class="error" id="email-err" aria-live="polite"></p>
</div>
<div class="field">
<label for="phone">Phone (for delivery)</label>
<input id="phone" name="phone" type="tel" autocomplete="tel" inputmode="tel" placeholder="(555) 010-2384" aria-describedby="phone-err" />
<p class="error" id="phone-err" aria-live="polite"></p>
</div>
<label class="check">
<input type="checkbox" id="news" name="news" checked />
<span>Email me Northwind drops and gear deals (no spam)</span>
</label>
</fieldset>
<!-- STEP 2 — SHIPPING -->
<fieldset class="panel" data-panel="2" hidden>
<legend class="panel-title">Shipping address</legend>
<div class="grid-2">
<div class="field">
<label for="first">First name</label>
<input id="first" name="first" type="text" autocomplete="given-name" placeholder="Maya" aria-describedby="first-err" />
<p class="error" id="first-err" aria-live="polite"></p>
</div>
<div class="field">
<label for="last">Last name</label>
<input id="last" name="last" type="text" autocomplete="family-name" placeholder="Okafor" aria-describedby="last-err" />
<p class="error" id="last-err" aria-live="polite"></p>
</div>
</div>
<div class="field">
<label for="address">Street address</label>
<input id="address" name="address" type="text" autocomplete="address-line1" placeholder="418 Cedar Ridge Rd" aria-describedby="address-err" />
<p class="error" id="address-err" aria-live="polite"></p>
</div>
<div class="field">
<label for="address2">Apartment, suite <span class="opt">(optional)</span></label>
<input id="address2" name="address2" type="text" autocomplete="address-line2" placeholder="Unit 7B" />
</div>
<div class="grid-3">
<div class="field">
<label for="city">City</label>
<input id="city" name="city" type="text" autocomplete="address-level2" placeholder="Bozeman" aria-describedby="city-err" />
<p class="error" id="city-err" aria-live="polite"></p>
</div>
<div class="field">
<label for="state">State</label>
<select id="state" name="state" autocomplete="address-level1" aria-describedby="state-err">
<option value="">Select</option>
<option>CA</option><option>CO</option><option>MT</option><option>NY</option>
<option>OR</option><option>TX</option><option>WA</option>
</select>
<p class="error" id="state-err" aria-live="polite"></p>
</div>
<div class="field">
<label for="zip">ZIP</label>
<input id="zip" name="zip" type="text" inputmode="numeric" autocomplete="postal-code" maxlength="5" placeholder="59715" aria-describedby="zip-err" />
<p class="error" id="zip-err" aria-live="polite"></p>
</div>
</div>
<fieldset class="ship-methods" aria-label="Shipping method">
<legend class="sub-title">Shipping method</legend>
<label class="ship-opt is-selected">
<input type="radio" name="ship" value="standard" data-price="0" data-eta="5–7 business days" checked />
<span class="ship-body">
<span class="ship-name">Standard <span class="free-chip">FREE</span></span>
<span class="ship-eta">Arrives in 5–7 business days</span>
</span>
<span class="ship-price">$0.00</span>
</label>
<label class="ship-opt">
<input type="radio" name="ship" value="express" data-price="12.5" data-eta="2 business days" />
<span class="ship-body">
<span class="ship-name">Express</span>
<span class="ship-eta">Arrives in 2 business days</span>
</span>
<span class="ship-price">$12.50</span>
</label>
<label class="ship-opt">
<input type="radio" name="ship" value="overnight" data-price="24" data-eta="next business day" />
<span class="ship-body">
<span class="ship-name">Overnight</span>
<span class="ship-eta">Arrives next business day</span>
</span>
<span class="ship-price">$24.00</span>
</label>
</fieldset>
</fieldset>
<!-- STEP 3 — PAYMENT -->
<fieldset class="panel" data-panel="3" hidden>
<legend class="panel-title">Payment</legend>
<p class="panel-hint secure-line">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
Encrypted & PCI-DSS protected. This is a demo — never enter a real card.
</p>
<div class="card-preview" id="cardPreview" aria-hidden="true">
<div class="card-brand" id="cardBrand">CARD</div>
<div class="card-chip"></div>
<div class="card-number" id="cardNumPreview">•••• •••• •••• ••••</div>
<div class="card-meta">
<span class="card-holder" id="cardHolderPreview">YOUR NAME</span>
<span class="card-exp" id="cardExpPreview">MM/YY</span>
</div>
</div>
<div class="field">
<label for="cardname">Name on card</label>
<input id="cardname" name="cardname" type="text" autocomplete="cc-name" placeholder="Maya Okafor" aria-describedby="cardname-err" />
<p class="error" id="cardname-err" aria-live="polite"></p>
</div>
<div class="field">
<label for="cardnum">Card number</label>
<input id="cardnum" name="cardnum" type="text" inputmode="numeric" autocomplete="cc-number" maxlength="19" placeholder="4242 4242 4242 4242" aria-describedby="cardnum-err" />
<p class="error" id="cardnum-err" aria-live="polite"></p>
</div>
<div class="grid-2">
<div class="field">
<label for="exp">Expiry (MM/YY)</label>
<input id="exp" name="exp" type="text" inputmode="numeric" autocomplete="cc-exp" maxlength="5" placeholder="08/29" aria-describedby="exp-err" />
<p class="error" id="exp-err" aria-live="polite"></p>
</div>
<div class="field">
<label for="cvc">CVC</label>
<input id="cvc" name="cvc" type="text" inputmode="numeric" autocomplete="cc-csc" maxlength="4" placeholder="123" aria-describedby="cvc-err" />
<p class="error" id="cvc-err" aria-live="polite"></p>
</div>
</div>
<label class="check">
<input type="checkbox" id="samebill" name="samebill" checked />
<span>Billing address same as shipping</span>
</label>
</fieldset>
<!-- STEP 4 — REVIEW -->
<fieldset class="panel" data-panel="4" hidden>
<legend class="panel-title">Review your order</legend>
<p class="panel-hint">Double-check everything before placing your order.</p>
<div class="review-grid">
<div class="review-card">
<div class="review-head"><h3>Contact</h3><button class="link-btn" type="button" data-goto="1">Edit</button></div>
<p id="rv-contact" class="review-body">—</p>
</div>
<div class="review-card">
<div class="review-head"><h3>Ship to</h3><button class="link-btn" type="button" data-goto="2">Edit</button></div>
<p id="rv-ship" class="review-body">—</p>
</div>
<div class="review-card">
<div class="review-head"><h3>Method</h3><button class="link-btn" type="button" data-goto="2">Edit</button></div>
<p id="rv-method" class="review-body">—</p>
</div>
<div class="review-card">
<div class="review-head"><h3>Payment</h3><button class="link-btn" type="button" data-goto="3">Edit</button></div>
<p id="rv-pay" class="review-body">—</p>
</div>
</div>
<label class="check terms">
<input type="checkbox" id="terms" name="terms" />
<span>I agree to the <a href="#" onclick="return false">Terms of Sale</a> and <a href="#" onclick="return false">Return Policy</a></span>
</label>
<p class="error" id="terms-err" aria-live="polite"></p>
</fieldset>
<!-- Nav buttons -->
<div class="actions">
<button type="button" class="btn ghost" id="backBtn" hidden>← Back</button>
<button type="button" class="btn primary" id="nextBtn">Continue to shipping</button>
<button type="submit" class="btn primary place" id="placeBtn" hidden>
<span class="lock-ic" aria-hidden="true">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</span>
Place order · <span id="placeTotal">$0.00</span>
</button>
</div>
</form>
</section>
<!-- ORDER SUMMARY SIDEBAR -->
<aside class="summary" aria-label="Order summary">
<button class="summary-toggle" id="summaryToggle" type="button" aria-expanded="false" aria-controls="summaryBody">
<span>Order summary</span>
<span class="summary-toggle-total" id="toggleTotal">$0.00</span>
<svg class="chev" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div class="summary-body" id="summaryBody">
<h2 class="summary-title">Order summary</h2>
<ul class="cart" id="cartList" aria-label="Items in cart"></ul>
<div class="promo">
<label class="sr-only" for="promo">Promo code</label>
<input id="promo" type="text" placeholder="Promo code" autocomplete="off" />
<button class="btn small" type="button" id="applyPromo">Apply</button>
</div>
<p class="promo-msg" id="promoMsg" aria-live="polite"></p>
<dl class="totals">
<div class="row"><dt>Subtotal</dt><dd id="sumSubtotal">$0.00</dd></div>
<div class="row discount" id="discountRow" hidden><dt>Discount (TRAIL10)</dt><dd id="sumDiscount">−$0.00</dd></div>
<div class="row"><dt>Shipping</dt><dd id="sumShipping">FREE</dd></div>
<div class="row"><dt>Estimated tax</dt><dd id="sumTax">$0.00</dd></div>
<div class="row total"><dt>Total</dt><dd id="sumTotal">$0.00</dd></div>
</dl>
<ul class="trust">
<li><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> 256-bit SSL encryption</li>
<li><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg> 30-day free returns</li>
</ul>
</div>
</aside>
</main>
<!-- SUCCESS OVERLAY -->
<div class="success" id="success" role="dialog" aria-modal="true" aria-labelledby="successTitle" hidden>
<div class="success-card">
<div class="success-burst" aria-hidden="true">
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
</div>
<h2 id="successTitle">Order confirmed!</h2>
<p class="success-sub">Thanks, <span id="successName">friend</span>. A receipt is on its way to <span id="successEmail">your inbox</span>.</p>
<div class="success-meta">
<div><span class="success-k">Order</span><span class="success-v" id="successOrder">#NW-00000</span></div>
<div><span class="success-k">Total</span><span class="success-v" id="successTotal">$0.00</span></div>
<div><span class="success-k">Arrives</span><span class="success-v" id="successEta">—</span></div>
</div>
<button class="btn primary" type="button" id="successDone">Continue shopping</button>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Checkout
A four-step checkout (Contact → Shipping → Payment → Review) wrapped in a numbered progress
indicator that fills in as each step is validated. Every step gates the next: email and phone are
format-checked, the address form requires name, street, city, state, and a 5-digit ZIP, and the
payment step runs a Luhn check on the card number plus a future-dated expiry. Errors surface inline
with aria-live messages and the first invalid field takes focus.
The payment step renders a live gradient card preview that mirrors the typed name, formatted number,
detected brand (Visa, Mastercard, Amex, Discover), and expiry. Shipping methods are priced radios
with delivery ETAs, and a persistent order-summary sidebar recomputes subtotal, discount, shipping,
tax, and total on every change. A TRAIL10 promo code applies a 10% discount, and the final review
groups everything with quick Edit jumps before a simulated place-order spinner resolves into a
success dialog showing the order number, total, and estimated arrival date.
On small screens the sidebar collapses into a tappable total bar, the step labels and grids stack, and all controls stay keyboard-usable with visible focus rings.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.