Salon — Loyalty & Referral Card
A boutique salon loyalty and referral panel with a gold-accented points progress bar climbing toward the next reward tier, badge milestones from Petal to Lumiere, a punch-style visit stamp card, a list of redeemable perks whose Redeem buttons deduct points live, and a referral block with a copy-to-clipboard code plus running invites-sent and joined counts. Editorial serif display, clean sans body, soft toasts.
MCP
Kod
:root {
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--gold: #b08d57;
--gold-d: #8c6d3f;
--gold-soft: #efe2cf;
--rose: #c9a78f;
--rose-soft: #f3e6dc;
--ink: #1c1814;
--ink-2: #3d362f;
--muted: #8a7d70;
--cream: #f7f1e8;
--bg: #faf6ef;
--white: #ffffff;
--line: rgba(28, 24, 20, 0.1);
--line-2: rgba(28, 24, 20, 0.18);
--ok: #5f8a6b;
--warn: #c08a3e;
--danger: #b3503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--shadow-sm: 0 1px 2px rgba(28, 24, 20, 0.05), 0 1px 1px rgba(28, 24, 20, 0.04);
--shadow-md: 0 14px 34px -18px rgba(28, 24, 20, 0.32), 0 2px 8px rgba(28, 24, 20, 0.05);
--shadow-lg: 0 40px 80px -38px rgba(28, 24, 20, 0.42);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
font-family: var(--sans);
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(120% 80% at 100% 0%, var(--rose-soft) 0%, transparent 45%),
radial-gradient(120% 90% at 0% 100%, var(--gold-soft) 0%, transparent 40%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: flex;
justify-content: center;
padding: 40px 20px;
}
h1,
h2 {
font-family: var(--serif);
font-weight: 600;
margin: 0;
letter-spacing: 0.2px;
}
.wrap {
width: 100%;
max-width: 540px;
}
/* ---------- card shell ---------- */
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-lg);
overflow: hidden;
}
.card__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 26px 28px 22px;
background: linear-gradient(180deg, var(--cream), var(--white));
border-bottom: 1px solid var(--line);
}
.brand {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.brand__mark {
flex: none;
width: 46px;
height: 46px;
border-radius: 50%;
display: grid;
place-items: center;
font-family: var(--serif);
font-weight: 700;
font-size: 18px;
letter-spacing: 0.5px;
color: var(--white);
background: linear-gradient(140deg, var(--gold), var(--gold-d));
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.4), var(--shadow-sm);
}
.eyebrow {
margin: 0 0 2px;
font-size: 10.5px;
font-weight: 600;
letter-spacing: 1.6px;
text-transform: uppercase;
color: var(--gold-d);
}
#loyalty-title {
font-size: 26px;
line-height: 1.1;
color: var(--ink);
}
.member {
display: flex;
align-items: center;
gap: 12px;
flex: none;
}
.member__meta {
text-align: right;
}
.member__name {
display: block;
font-size: 13.5px;
font-weight: 600;
color: var(--ink-2);
}
.tier-pill {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 4px;
padding: 3px 9px;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.4px;
color: var(--gold-d);
background: var(--gold-soft);
border: 1px solid rgba(176, 141, 87, 0.3);
}
.tier-pill__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--gold);
box-shadow: 0 0 0 3px rgba(176, 141, 87, 0.18);
}
.member__avatar {
flex: none;
width: 42px;
height: 42px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 13px;
font-weight: 600;
color: var(--gold-d);
background: var(--rose-soft);
border: 1px solid rgba(201, 167, 143, 0.5);
}
/* ---------- panels ---------- */
.panel {
padding: 24px 28px;
border-bottom: 1px solid var(--line);
}
.panel:last-child {
border-bottom: 0;
}
.panel__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
}
.panel__title {
font-size: 20px;
color: var(--ink);
}
.panel__hint {
margin: 0;
font-size: 12px;
color: var(--muted);
}
.label {
margin: 0 0 4px;
font-size: 10.5px;
font-weight: 600;
letter-spacing: 1.4px;
text-transform: uppercase;
color: var(--muted);
}
/* ---------- progress ---------- */
.progress__top {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.points {
margin: 0;
font-family: var(--serif);
font-weight: 700;
font-size: 42px;
line-height: 1;
color: var(--ink);
}
.points__unit {
font-family: var(--sans);
font-size: 14px;
font-weight: 600;
color: var(--muted);
margin-left: 6px;
}
.progress__goal {
text-align: right;
}
.goal-name {
margin: 0;
font-family: var(--serif);
font-size: 19px;
font-weight: 600;
color: var(--gold-d);
}
.bar {
height: 12px;
border-radius: 999px;
background: var(--gold-soft);
border: 1px solid rgba(176, 141, 87, 0.22);
overflow: hidden;
box-shadow: inset 0 1px 2px rgba(28, 24, 20, 0.08);
}
.bar__fill {
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--rose), var(--gold) 60%, var(--gold-d));
box-shadow: 0 0 12px rgba(176, 141, 87, 0.45);
transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.bar__caption {
margin: 10px 0 0;
font-size: 12.5px;
color: var(--ink-2);
}
.bar__caption span {
font-weight: 600;
color: var(--gold-d);
}
.tiers {
list-style: none;
margin: 20px 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.tier {
position: relative;
text-align: center;
padding: 12px 4px 10px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--cream);
}
.tier__badge {
display: grid;
place-items: center;
width: 30px;
height: 30px;
margin: 0 auto 7px;
border-radius: 50%;
font-size: 13px;
color: var(--muted);
background: var(--white);
border: 1px solid var(--line-2);
transition: all 0.25s ease;
}
.tier__name {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
}
.tier__at {
display: block;
font-size: 10.5px;
color: var(--muted);
margin-top: 2px;
}
.tier.is-done .tier__badge {
color: var(--white);
background: linear-gradient(140deg, var(--gold), var(--gold-d));
border-color: var(--gold-d);
}
.tier.is-current {
border-color: var(--gold);
background: var(--gold-soft);
box-shadow: 0 0 0 1px rgba(176, 141, 87, 0.35), var(--shadow-sm);
}
.tier.is-current .tier__badge {
color: var(--gold-d);
background: var(--white);
border-color: var(--gold);
box-shadow: 0 0 0 4px rgba(176, 141, 87, 0.16);
}
/* ---------- stamps ---------- */
.stamp-row {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 10px;
}
.stamp {
aspect-ratio: 1;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 15px;
color: var(--muted);
background: var(--cream);
border: 1px dashed var(--line-2);
}
.stamp.is-punched {
color: var(--white);
background: linear-gradient(140deg, var(--rose), var(--gold));
border: 1px solid var(--gold-d);
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.4), var(--shadow-sm);
}
.stamp.is-free {
color: var(--gold-d);
background: var(--white);
border: 1px solid var(--gold);
box-shadow: 0 0 0 3px rgba(176, 141, 87, 0.14);
}
.stamp.is-free::after {
content: "FREE";
font-family: var(--sans);
font-size: 7px;
font-weight: 700;
letter-spacing: 0.6px;
}
/* ---------- rewards ---------- */
.reward-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 12px;
}
.reward {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 16px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--white);
box-shadow: var(--shadow-sm);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}
.reward:hover {
border-color: rgba(176, 141, 87, 0.4);
box-shadow: var(--shadow-md);
}
.reward__icon {
flex: none;
width: 44px;
height: 44px;
border-radius: var(--r-sm);
display: grid;
place-items: center;
font-size: 20px;
background: var(--gold-soft);
border: 1px solid rgba(176, 141, 87, 0.28);
}
.reward__body {
flex: 1;
min-width: 0;
}
.reward__name {
margin: 0;
font-size: 14.5px;
font-weight: 600;
color: var(--ink);
}
.reward__desc {
margin: 2px 0 0;
font-size: 12px;
color: var(--muted);
}
.reward__cost {
flex: none;
text-align: center;
font-size: 13px;
font-weight: 600;
color: var(--gold-d);
white-space: nowrap;
}
.reward__cost small {
display: block;
font-size: 9.5px;
font-weight: 600;
letter-spacing: 0.8px;
text-transform: uppercase;
color: var(--muted);
}
/* ---------- buttons ---------- */
.btn {
font-family: var(--sans);
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.2px;
border-radius: 999px;
border: 1px solid transparent;
cursor: pointer;
transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
white-space: nowrap;
}
.btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.btn:active {
transform: translateY(1px);
}
.btn--solid {
padding: 11px 20px;
color: var(--white);
background: linear-gradient(140deg, var(--gold), var(--gold-d));
box-shadow: var(--shadow-sm);
}
.btn--solid:hover {
box-shadow: 0 8px 20px -8px rgba(140, 109, 63, 0.6);
}
.btn--ghost {
padding: 8px 14px;
color: var(--gold-d);
background: var(--white);
border-color: var(--line-2);
}
.btn--ghost:hover {
background: var(--cream);
border-color: var(--gold);
}
.reward__redeem {
flex: none;
padding: 9px 16px;
color: var(--gold-d);
background: var(--gold-soft);
border: 1px solid rgba(176, 141, 87, 0.4);
border-radius: 999px;
font-family: var(--sans);
font-size: 12.5px;
font-weight: 600;
cursor: pointer;
transition: all 0.18s ease;
}
.reward__redeem:hover:not(:disabled) {
color: var(--white);
background: linear-gradient(140deg, var(--gold), var(--gold-d));
border-color: var(--gold-d);
}
.reward__redeem:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.reward__redeem:disabled {
cursor: not-allowed;
color: var(--muted);
background: var(--cream);
border-color: var(--line);
opacity: 0.85;
}
.reward.is-locked {
opacity: 0.72;
}
/* ---------- referral ---------- */
.referral {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
align-items: center;
background: linear-gradient(160deg, var(--cream), var(--rose-soft));
}
.referral__copy .panel__title {
font-size: 22px;
margin-top: 2px;
}
.referral__sub {
margin: 8px 0 0;
font-size: 12.5px;
color: var(--ink-2);
}
.referral__code {
display: grid;
gap: 10px;
}
.code-field {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 9px 9px 9px 14px;
background: var(--white);
border: 1px dashed var(--gold);
border-radius: var(--r-md);
}
.code-field code {
font-family: var(--sans);
font-size: 15px;
font-weight: 700;
letter-spacing: 1px;
color: var(--ink);
}
.copy {
display: inline-flex;
align-items: center;
gap: 6px;
}
.copy.is-copied {
color: var(--ok);
border-color: var(--ok);
background: rgba(95, 138, 107, 0.1);
}
.referral__stats {
display: flex;
align-items: center;
gap: 8px;
font-size: 12.5px;
color: var(--muted);
}
.referral__stats strong {
color: var(--ink);
}
.referral__stats .dot {
color: var(--line-2);
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
max-width: 90vw;
padding: 12px 20px;
border-radius: 999px;
font-size: 13px;
font-weight: 500;
color: var(--cream);
background: var(--ink);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
z-index: 50;
}
.toast.is-on {
opacity: 1;
transform: translate(-50%, 0);
}
.toast::before {
content: "✦";
color: var(--gold);
margin-right: 8px;
}
@keyframes pop {
0% { transform: scale(0.6); opacity: 0; }
60% { transform: scale(1.12); }
100% { transform: scale(1); opacity: 1; }
}
.stamp.just-punched {
animation: pop 0.4s ease;
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
body {
padding: 18px 12px;
}
.card__head {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 22px 20px 20px;
}
.member {
width: 100%;
justify-content: flex-start;
flex-direction: row-reverse;
}
.member__meta {
text-align: left;
}
#loyalty-title {
font-size: 24px;
}
.panel {
padding: 20px;
}
.progress__top {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.progress__goal {
text-align: left;
}
.tiers {
grid-template-columns: repeat(2, 1fr);
}
.stamp-row {
grid-template-columns: repeat(4, 1fr);
}
.reward {
flex-wrap: wrap;
}
.reward__cost {
order: 2;
text-align: left;
}
.reward__redeem {
order: 3;
margin-left: auto;
}
.referral {
grid-template-columns: 1fr;
gap: 18px;
}
}(function () {
"use strict";
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
var state = {
points: 320,
stamps: 5,
stampGoal: 8,
invitesSent: 7,
invitesJoined: 3,
refCode: "ARIA-LUM50",
};
var TIERS = [
{ name: "Petal", at: 0 },
{ name: "Gilded", at: 250 },
{ name: "Aurum", at: 500 },
{ name: "Lumière", at: 900 },
];
var REWARDS = [
{ id: "blowdry", icon: "💨", name: "Free blow-dry & style", desc: "Glossy finish with our signature serum", cost: 500 },
{ id: "treatment", icon: "🧴", name: "Bond-repair treatment", desc: "Deep-conditioning ritual add-on", cost: 350 },
{ id: "manicure", icon: "💅", name: "Express gel manicure", desc: "Shape, buff and a season shade", cost: 280 },
{ id: "facial", icon: "🌿", name: "15-min glow facial", desc: "Cleanse, mask and lift", cost: 220 },
];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
var $ = function (sel) { return document.querySelector(sel); };
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-on");
}, 2600);
}
function currentTierIndex(pts) {
var idx = 0;
for (var i = 0; i < TIERS.length; i++) {
if (pts >= TIERS[i].at) idx = i;
}
return idx;
}
function nextTier(pts) {
for (var i = 0; i < TIERS.length; i++) {
if (pts < TIERS[i].at) return TIERS[i];
}
return null; // maxed out
}
function cheapestUnlockableReward(pts) {
var affordable = REWARDS.filter(function (r) { return r.cost > pts; })
.sort(function (a, b) { return a.cost - b.cost; });
return affordable.length ? affordable[0] : REWARDS.slice().sort(function (a, b) { return a.cost - b.cost; })[0];
}
// ---------------------------------------------------------------------------
// Renderers
// ---------------------------------------------------------------------------
function renderProgress() {
var pts = state.points;
$("#ptsValue").textContent = pts;
var cur = TIERS[currentTierIndex(pts)];
$("#tierName").textContent = cur.name;
var next = nextTier(pts);
var floor = cur.at;
var ceil = next ? next.at : cur.at;
var span = ceil - floor || 1;
var pct = next ? Math.min(100, Math.round(((pts - floor) / span) * 100)) : 100;
var fill = $("#barFill");
fill.style.width = pct + "%";
var bar = $("#bar");
bar.setAttribute("aria-valuemin", String(floor));
bar.setAttribute("aria-valuemax", String(ceil));
bar.setAttribute("aria-valuenow", String(pts));
var remaining = next ? next.at - pts : 0;
$("#ptsRemaining").textContent = remaining;
// Reward goal label — the cheapest reward still out of reach.
var goal = cheapestUnlockableReward(pts);
$("#goalName").textContent = goal.name.replace(/^Free /, "");
var caption = $(".bar__caption");
if (next) {
caption.innerHTML = '<span>' + remaining + '</span> pts to ' + next.name + ' · ' +
'<span>' + Math.max(0, goal.cost - pts) + '</span> to ' + goal.name.toLowerCase();
} else {
caption.innerHTML = "You've reached <span>Lumière</span> — our highest tier. Merci!";
}
// Tier badge states
var curIdx = currentTierIndex(pts);
var nodes = document.querySelectorAll(".tier");
nodes.forEach(function (node, i) {
node.classList.remove("is-done", "is-current");
if (i < curIdx) node.classList.add("is-done");
else if (i === curIdx) node.classList.add("is-current");
});
}
function renderStamps(animateLast) {
var row = $("#stampRow");
row.innerHTML = "";
for (var i = 1; i <= state.stampGoal; i++) {
var li = document.createElement("li");
li.className = "stamp";
if (i === state.stampGoal) {
li.classList.add("is-free");
li.setAttribute("aria-label", "Visit " + i + " — free");
} else if (i <= state.stamps) {
li.classList.add("is-punched");
li.innerHTML = "✦";
li.setAttribute("aria-label", "Visit " + i + " — stamped");
if (animateLast && i === state.stamps) li.classList.add("just-punched");
} else {
li.textContent = i;
li.setAttribute("aria-label", "Visit " + i + " — upcoming");
}
row.appendChild(li);
}
$("#stampCount").textContent = state.stamps;
}
function renderRewards() {
var list = $("#rewardList");
list.innerHTML = "";
REWARDS.forEach(function (r) {
var locked = state.points < r.cost;
var li = document.createElement("li");
li.className = "reward" + (locked ? " is-locked" : "");
li.innerHTML =
'<span class="reward__icon" aria-hidden="true">' + r.icon + "</span>" +
'<div class="reward__body">' +
'<p class="reward__name">' + r.name + "</p>" +
'<p class="reward__desc">' + r.desc + "</p>" +
"</div>" +
'<div class="reward__cost">' + r.cost + "<small>points</small></div>";
var btn = document.createElement("button");
btn.type = "button";
btn.className = "reward__redeem";
btn.textContent = locked ? "Locked" : "Redeem";
btn.disabled = locked;
btn.setAttribute("aria-label", (locked ? "Locked — " : "Redeem ") + r.name + " for " + r.cost + " points");
btn.addEventListener("click", function () { redeem(r); });
li.appendChild(btn);
list.appendChild(li);
});
}
// ---------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------
function redeem(reward) {
if (state.points < reward.cost) {
toast("You need " + (reward.cost - state.points) + " more points for " + reward.name + ".");
return;
}
state.points -= reward.cost;
renderProgress();
renderRewards();
toast(reward.name + " redeemed — see you at the chair!");
}
function copyCode() {
var code = state.refCode;
var btn = $("#copyBtn");
function done() {
btn.classList.add("is-copied");
var txt = btn.querySelector(".copy__txt");
if (txt) txt.textContent = "Copied";
toast("Referral code " + code + " copied to clipboard.");
setTimeout(function () {
btn.classList.remove("is-copied");
if (txt) txt.textContent = "Copy";
}, 1900);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code).then(done).catch(fallbackCopy);
} else {
fallbackCopy();
}
function fallbackCopy() {
var ta = document.createElement("textarea");
ta.value = code;
ta.setAttribute("readonly", "");
ta.style.position = "absolute";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch (e) { /* noop */ }
document.body.removeChild(ta);
done();
}
}
function sendInvite() {
state.invitesSent += 1;
$("#invitesSent").textContent = state.invitesSent;
// Simulate one in three invites converting to a booking + reward.
if (state.invitesSent % 3 === 0) {
state.invitesJoined += 1;
state.points += 50;
$("#invitesJoined").textContent = state.invitesJoined;
renderProgress();
renderRewards();
renderStamps(false);
toast("A friend booked! +50 points added to your balance.");
} else {
toast("Invite sent — they'll get 20% off their first visit.");
}
}
// ---------------------------------------------------------------------------
// Wire up
// ---------------------------------------------------------------------------
function init() {
renderProgress();
renderStamps(false);
renderRewards();
$("#copyBtn").addEventListener("click", copyCode);
$("#inviteBtn").addEventListener("click", sendInvite);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Loyalty & Rewards · Maison Lumière Salon</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=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<section class="card" aria-labelledby="loyalty-title">
<!-- Header -->
<header class="card__head">
<div class="brand">
<span class="brand__mark" aria-hidden="true">ML</span>
<div class="brand__txt">
<p class="eyebrow">Maison Lumière · Membership</p>
<h1 id="loyalty-title">Loyalty & Rewards</h1>
</div>
</div>
<div class="member" aria-label="Member">
<div class="member__meta">
<span class="member__name">Aria Vance</span>
<span class="tier-pill" id="tierPill">
<span class="tier-pill__dot" aria-hidden="true"></span>
<span id="tierName">Gilded</span>
</span>
</div>
<div class="member__avatar" aria-hidden="true">AV</div>
</div>
</header>
<!-- Points / progress -->
<section class="panel progress" aria-labelledby="progress-h">
<div class="progress__top">
<div>
<p class="label" id="progress-h">Points balance</p>
<p class="points"><span id="ptsValue">320</span><span class="points__unit">pts</span></p>
</div>
<div class="progress__goal">
<p class="label">Next reward</p>
<p class="goal-name" id="goalName">Free blow-dry</p>
</div>
</div>
<div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="500" aria-valuenow="320" aria-label="Progress to next reward tier" id="bar">
<div class="bar__fill" id="barFill"></div>
</div>
<p class="bar__caption"><span id="ptsRemaining">180</span> pts to your next reward</p>
<ol class="tiers" aria-label="Membership tiers">
<li class="tier is-done" data-tier="Petal" data-at="0">
<span class="tier__badge" aria-hidden="true">✦</span>
<span class="tier__name">Petal</span>
<span class="tier__at">0</span>
</li>
<li class="tier is-current" data-tier="Gilded" data-at="250">
<span class="tier__badge" aria-hidden="true">✦</span>
<span class="tier__name">Gilded</span>
<span class="tier__at">250</span>
</li>
<li class="tier" data-tier="Aurum" data-at="500">
<span class="tier__badge" aria-hidden="true">✦</span>
<span class="tier__name">Aurum</span>
<span class="tier__at">500</span>
</li>
<li class="tier" data-tier="Lumière" data-at="900">
<span class="tier__badge" aria-hidden="true">✦</span>
<span class="tier__name">Lumière</span>
<span class="tier__at">900</span>
</li>
</ol>
</section>
<!-- Stamp card -->
<section class="panel stamps" aria-labelledby="stamps-h">
<div class="panel__head">
<h2 class="panel__title" id="stamps-h">Visit stamps</h2>
<p class="panel__hint"><span id="stampCount">5</span> of 8 · 8th visit is on us</p>
</div>
<ul class="stamp-row" id="stampRow" aria-label="Visit stamp card">
<!-- stamps injected by script -->
</ul>
</section>
<!-- Rewards -->
<section class="panel rewards" aria-labelledby="rewards-h">
<div class="panel__head">
<h2 class="panel__title" id="rewards-h">Redeemable rewards</h2>
<p class="panel__hint">Spend points on salon perks</p>
</div>
<ul class="reward-list" id="rewardList" aria-label="Rewards you can redeem">
<!-- reward cards injected by script -->
</ul>
</section>
<!-- Referral -->
<section class="panel referral" aria-labelledby="referral-h">
<div class="referral__copy">
<p class="eyebrow">Share the glow</p>
<h2 class="panel__title" id="referral-h">Refer a friend, earn 50 pts</h2>
<p class="referral__sub">They get 20% off their first visit. You earn points when they book.</p>
</div>
<div class="referral__code">
<span class="label">Your code</span>
<div class="code-field">
<code id="refCode">ARIA-LUM50</code>
<button class="btn btn--ghost copy" id="copyBtn" type="button" aria-label="Copy referral code">
<span class="copy__icon" aria-hidden="true">❏</span>
<span class="copy__txt">Copy</span>
</button>
</div>
<div class="referral__stats">
<span class="stat"><strong id="invitesSent">7</strong> invites sent</span>
<span class="dot" aria-hidden="true">·</span>
<span class="stat"><strong id="invitesJoined">3</strong> joined</span>
</div>
<button class="btn btn--solid" id="inviteBtn" type="button">Send an invite</button>
</div>
</section>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Loyalty & Referral Card
A members-only loyalty panel for Maison Lumière Salon, built for a guest like Aria Vance. A serif display headline sits above a points balance that fills a gilded progress bar toward the next reward, with four tier badges — Petal, Gilded, Aurum and Lumière — lighting up as the balance climbs. A row of eight visit stamps shows the punch-card toward a complimentary eighth visit, the final spot reserved as a free reward.
Beneath it, a list of redeemable perks — a free blow-dry, a bond-repair treatment, an express manicure, a glow facial — each shows its point cost and a Redeem button. Redeeming deducts the points, re-renders the bar and tiers, locks any rewards now out of reach, and raises a soft toast. The referral block carries a copyable code that writes to the clipboard, plus live invites-sent and joined counts that update as you send invitations, occasionally crediting bonus points when a friend books.
Everything is vanilla JavaScript with no dependencies: live point math, animated progress, clipboard copy with a graceful fallback, and an accessible toast helper. The palette leans on rose-gold, cream and matte black with thin gold hairlines, and the whole card stays legible and tappable down to a 360px viewport.