Airline — Frequent Flyer Card
A polished frequent-flyer status card for a fictional airline loyalty program, showing tier, miles balance, animated progress toward the next tier, a curated benefits list, recent flight and redemption activity, and a digital membership QR. Switch between two tiers (Gold and Platinum) to see palette, miles, and perks update live. Tap the card to flip it and reveal the scannable QR, and redeem miles to watch the balance and progress bar respond with a confirmation toast.
MCP
Code
:root {
--sky: #0a66c2;
--sky-d: #084e95;
--sky-50: #e9f2fb;
--cloud: #f5f8fc;
--sunrise: #ff7a33;
--sunrise-50: #fff0e7;
--ink: #13233b;
--ink-2: #3a4d68;
--muted: #6b7c93;
--bg: #f5f8fc;
--surface: #ffffff;
--line: rgba(19, 35, 59, 0.1);
--line-2: rgba(19, 35, 59, 0.18);
--ok: #1f9d62;
--warn: #e0962a;
--danger: #d4493e;
--boarding: #1f9d62;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--shadow-sm: 0 1px 2px rgba(19, 35, 59, 0.06), 0 2px 8px rgba(19, 35, 59, 0.05);
--shadow-md: 0 8px 28px rgba(19, 35, 59, 0.12);
--shadow-lg: 0 18px 50px rgba(8, 78, 149, 0.28);
/* tier theming */
--tier-a: #b88a2e;
--tier-b: #e6c878;
--tier-ink: #3a2c08;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 460px at 50% -160px, var(--sky-50), transparent 70%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.tnum { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
.wrap {
max-width: 940px;
margin: 0 auto;
padding: 28px 20px 56px;
}
/* ---------- header ---------- */
.page-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border-radius: 12px;
background: linear-gradient(150deg, var(--sky), var(--sky-d));
color: #fff;
box-shadow: var(--shadow-sm);
}
.brand-name { margin: 0; font-weight: 800; font-size: 17px; letter-spacing: -0.01em; }
.brand-sub { margin: 0; font-size: 12.5px; color: var(--muted); font-weight: 500; }
.tier-toggle {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 4px;
box-shadow: var(--shadow-sm);
}
.tt-btn {
border: 0;
background: transparent;
font: inherit;
font-weight: 600;
font-size: 13.5px;
color: var(--ink-2);
padding: 7px 16px;
border-radius: 999px;
cursor: pointer;
transition: background 0.18s, color 0.18s;
}
.tt-btn:hover { color: var(--ink); }
.tt-btn.is-active {
background: linear-gradient(150deg, var(--sky), var(--sky-d));
color: #fff;
box-shadow: var(--shadow-sm);
}
.tt-btn:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
/* ---------- card / flip ---------- */
.stage { perspective: 1400px; margin-bottom: 22px; }
.flip {
max-width: 460px;
margin: 0 auto;
}
.flip-inner {
position: relative;
width: 100%;
aspect-ratio: 1.586 / 1;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
transform-style: preserve-3d;
transition: transform 0.7s cubic-bezier(0.4, 0.1, 0.2, 1);
border-radius: var(--r-lg);
font: inherit;
text-align: left;
display: block;
}
.flip.flipped .flip-inner { transform: rotateY(180deg); }
.flip-inner:focus-visible { outline: 3px solid var(--sky); outline-offset: 4px; }
.face {
position: absolute;
inset: 0;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
border-radius: var(--r-lg);
padding: 22px;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-lg);
overflow: hidden;
color: #fff;
}
/* front gradient driven by tier vars */
.front {
background:
radial-gradient(420px 220px at 88% -30%, rgba(255, 255, 255, 0.22), transparent 60%),
linear-gradient(140deg, var(--tier-a), var(--tier-b));
color: var(--tier-ink);
}
.front::after {
content: "";
position: absolute;
right: -40px;
bottom: -50px;
width: 220px;
height: 220px;
background: radial-gradient(circle, rgba(255, 255, 255, 0.16), transparent 70%);
pointer-events: none;
}
.front::before {
content: "✈";
position: absolute;
font-size: 150px;
right: -10px;
bottom: -48px;
opacity: 0.1;
transform: rotate(-18deg);
pointer-events: none;
}
.card-top { display: flex; align-items: center; justify-content: space-between; }
.card-brand { display: flex; align-items: center; gap: 8px; font-weight: 800; font-size: 15px; letter-spacing: 0.01em; }
.card-logo { font-size: 18px; }
.tier-pill {
font-size: 11.5px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 5px 11px;
border-radius: 999px;
background: rgba(19, 35, 59, 0.85);
color: #fff;
white-space: nowrap;
}
.miles { margin-top: auto; }
.miles-label { margin: 0; font-size: 12px; font-weight: 600; opacity: 0.78; letter-spacing: 0.03em; text-transform: uppercase; }
.miles-value { margin: 2px 0 0; font-size: 38px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; }
.miles-unit { font-size: 16px; font-weight: 700; margin-left: 6px; opacity: 0.7; }
.member {
display: flex;
gap: 18px;
margin-top: 16px;
flex-wrap: wrap;
}
.m-label { margin: 0; font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.62; }
.m-name, .m-num { margin: 2px 0 0; font-size: 13.5px; font-weight: 700; letter-spacing: 0.02em; }
.flip-hint {
position: absolute;
top: 22px;
right: 22px;
margin: 0;
font-size: 10.5px;
font-weight: 600;
opacity: 0.7;
letter-spacing: 0.02em;
}
/* back */
.back {
transform: rotateY(180deg);
background: linear-gradient(150deg, #16263f, #0c1a2e);
align-items: center;
justify-content: center;
text-align: center;
gap: 14px;
color: #fff;
}
.back-head {
position: absolute;
top: 18px;
left: 22px;
right: 22px;
display: flex;
align-items: center;
justify-content: space-between;
}
.back-id { font-size: 12.5px; font-weight: 700; opacity: 0.8; }
.qr {
width: 124px;
height: 124px;
border-radius: 10px;
background: #fff;
padding: 9px;
display: grid;
grid-template-columns: repeat(11, 1fr);
grid-template-rows: repeat(11, 1fr);
gap: 0;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}
.qr i { background: var(--ink); }
.qr-label { margin: 0; font-size: 12px; opacity: 0.78; max-width: 220px; }
/* ---------- progress ---------- */
.progress-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 20px;
box-shadow: var(--shadow-sm);
max-width: 460px;
margin: 0 auto 22px;
}
.progress-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 12px; }
.p-title { margin: 0; font-size: 14px; color: var(--ink-2); font-weight: 600; }
.p-title strong { color: var(--ink); font-weight: 800; }
.p-sub { margin: 0; font-size: 13px; font-weight: 700; color: var(--ink); }
.bar {
height: 12px;
border-radius: 999px;
background: var(--sky-50);
overflow: hidden;
border: 1px solid var(--line);
}
.bar-fill {
height: 100%;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--sky), var(--sunrise));
transition: width 1.1s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.p-foot { margin: 10px 0 0; font-size: 12.5px; color: var(--muted); }
.p-foot .tnum { color: var(--sunrise); font-weight: 700; }
/* ---------- columns ---------- */
.cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 20px;
box-shadow: var(--shadow-sm);
}
.panel-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 800;
letter-spacing: -0.01em;
}
.benefit-list { list-style: none; margin: 0 0 16px; padding: 0; display: grid; gap: 9px; }
.benefit-list li {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 13.5px;
color: var(--ink-2);
}
.benefit-list .chk {
flex: none;
width: 19px;
height: 19px;
border-radius: 6px;
background: var(--sky-50);
color: var(--sky);
display: grid;
place-items: center;
font-size: 12px;
font-weight: 800;
margin-top: 1px;
}
.redeem-btn {
width: 100%;
border: 0;
font: inherit;
font-weight: 700;
font-size: 13.5px;
color: #fff;
padding: 11px 14px;
border-radius: var(--r-sm);
background: linear-gradient(140deg, var(--sunrise), #f0631f);
cursor: pointer;
box-shadow: 0 6px 16px rgba(255, 122, 51, 0.32);
transition: transform 0.12s, box-shadow 0.18s, filter 0.18s;
}
.redeem-btn:hover { filter: brightness(1.04); box-shadow: 0 8px 22px rgba(255, 122, 51, 0.42); }
.redeem-btn:active { transform: translateY(1px) scale(0.995); }
.redeem-btn:focus-visible { outline: 2px solid var(--sky); outline-offset: 2px; }
.redeem-btn:disabled { background: var(--line-2); color: var(--muted); box-shadow: none; cursor: not-allowed; filter: none; }
.act-list { list-style: none; margin: 0; padding: 0; }
.act-list li {
display: flex;
align-items: center;
gap: 12px;
padding: 11px 0;
border-bottom: 1px solid var(--line);
}
.act-list li:last-child { border-bottom: 0; }
.act-icon {
flex: none;
width: 34px;
height: 34px;
border-radius: 9px;
background: var(--sky-50);
color: var(--sky);
display: grid;
place-items: center;
font-size: 15px;
}
.act-icon.redeem { background: var(--sunrise-50); color: var(--sunrise); }
.act-main { flex: 1; min-width: 0; }
.act-title { margin: 0; font-size: 13.5px; font-weight: 700; }
.act-meta { margin: 1px 0 0; font-size: 12px; color: var(--muted); }
.act-amt { font-size: 13.5px; font-weight: 800; white-space: nowrap; }
.act-amt.plus { color: var(--ok); }
.act-amt.minus { color: var(--danger); }
/* ---------- toast ---------- */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 50;
width: max-content;
max-width: 92vw;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
background: var(--ink);
color: #fff;
padding: 11px 16px;
border-radius: var(--r-sm);
font-size: 13.5px;
font-weight: 600;
box-shadow: var(--shadow-md);
transform: translateY(14px);
opacity: 0;
transition: transform 0.28s, opacity 0.28s;
}
.toast.show { transform: translateY(0); opacity: 1; }
.toast .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--sunrise); flex: none; }
.toast.ok .dot { background: var(--ok); }
/* ---------- responsive ---------- */
@media (max-width: 520px) {
.wrap { padding: 20px 14px 44px; }
.cols { grid-template-columns: 1fr; }
.page-head { gap: 12px; }
.miles-value { font-size: 34px; }
.member { gap: 14px; }
.flip-hint { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.flip-inner, .bar-fill, .toast { transition: none; }
}(function () {
"use strict";
var TIERS = {
gold: {
label: "Gold",
next: "Platinum",
miles: 58420,
goal: 75000,
from: "#b88a2e",
to: "#e6c878",
ink: "#3a2c08",
benefits: [
"Priority check-in & boarding (Zone 1)",
"2 free checked bags, up to 32 kg each",
"Lounge access on international routes",
"25% bonus miles on every flight",
"Complimentary seat selection",
],
},
platinum: {
label: "Platinum",
next: "Diamond",
miles: 96750,
goal: 125000,
from: "#4b5563",
to: "#aeb8c6",
ink: "#10151c",
benefits: [
"Guaranteed seat on sold-out flights",
"3 free checked bags, up to 32 kg each",
"Unlimited lounge access worldwide",
"75% bonus miles on every flight",
"Two annual upgrade-to-Business awards",
"Dedicated 24/7 concierge line",
],
},
};
var ACTIVITY = [
{ type: "flight", title: "SK 218 · JFK → LHR", meta: "18 Jun 2026 · Business", amt: 4120, plus: true },
{ type: "flight", title: "SK 661 · LHR → SIN", meta: "02 Jun 2026 · Economy", amt: 1880, plus: true },
{ type: "redeem", title: "Lounge day pass", meta: "21 May 2026 · CDG Terminal 2E", amt: 5000, plus: false },
{ type: "flight", title: "SK 110 · NRT → SFO", meta: "08 May 2026 · Premium Economy", amt: 2640, plus: true },
{ type: "redeem", title: "Seat upgrade · 14A", meta: "30 Apr 2026 · SK 218", amt: 8000, plus: false },
];
var REDEEM_COST = 25000;
var $ = function (id) { return document.getElementById(id); };
var fmt = function (n) { return n.toLocaleString("en-US"); };
var state = { tier: "gold" };
/* ---------- toast ---------- */
var toastWrap = $("toastWrap");
function toast(msg, ok) {
var el = document.createElement("div");
el.className = "toast" + (ok ? " ok" : "");
el.innerHTML = '<span class="dot"></span><span></span>';
el.querySelector("span:last-child").textContent = msg;
toastWrap.appendChild(el);
requestAnimationFrame(function () { el.classList.add("show"); });
setTimeout(function () {
el.classList.remove("show");
setTimeout(function () { el.remove(); }, 320);
}, 2600);
}
/* ---------- count-up ---------- */
function countUp(el, to, dur) {
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) { el.textContent = fmt(to); return; }
var start = performance.now();
function step(now) {
var p = Math.min(1, (now - start) / dur);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = fmt(Math.round(to * eased));
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
/* ---------- QR (deterministic faux pattern) ---------- */
function buildQR() {
var qr = $("qr");
qr.innerHTML = "";
var n = 11;
var seed = 0x9e3779b9;
function rnd() {
seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5;
return ((seed >>> 0) % 1000) / 1000;
}
function finder(r, c) {
return (r < 3 && c < 3) || (r < 3 && c > n - 4) || (r > n - 4 && c < 3);
}
for (var r = 0; r < n; r++) {
for (var c = 0; c < n; c++) {
var cell = document.createElement("i");
var on;
if (finder(r, c)) {
on = (r === 0 || r === 2 || c === 0 || c === 2 || (r === 1 && c === 1) ||
(r === 0 || r === 2 || c === 0 || c === 2));
} else {
on = rnd() > 0.5;
}
cell.style.background = on ? "var(--ink)" : "transparent";
qr.appendChild(cell);
}
}
}
/* ---------- render ---------- */
function setProgress(t) {
var pct = Math.min(100, Math.round((t.miles / t.goal) * 100));
var bar = $("bar");
$("nextTierName").textContent = t.next;
$("progGoal").textContent = fmt(t.goal);
$("progGoal").classList.add("tnum");
bar.setAttribute("aria-valuenow", String(pct));
countUp($("progNow"), t.miles, 1000);
countUp($("progRemain"), Math.max(0, t.goal - t.miles), 1000);
// animate fill after a tick so transition fires
var fill = $("barFill");
fill.style.width = "0%";
requestAnimationFrame(function () {
requestAnimationFrame(function () { fill.style.width = pct + "%"; });
});
}
function renderBenefits(t) {
var ul = $("benefitList");
ul.innerHTML = "";
t.benefits.forEach(function (b) {
var li = document.createElement("li");
li.innerHTML = '<span class="chk" aria-hidden="true">✓</span><span></span>';
li.querySelector("span:last-child").textContent = b;
ul.appendChild(li);
});
}
function renderActivity() {
var ul = $("actList");
ul.innerHTML = "";
ACTIVITY.forEach(function (a) {
var li = document.createElement("li");
var icon = a.type === "redeem" ? "★" : "✈";
var cls = a.type === "redeem" ? "act-icon redeem" : "act-icon";
var amtCls = a.plus ? "act-amt plus tnum" : "act-amt minus tnum";
var sign = a.plus ? "+" : "−";
li.innerHTML =
'<span class="' + cls + '" aria-hidden="true">' + icon + "</span>" +
'<div class="act-main"><p class="act-title"></p><p class="act-meta"></p></div>' +
'<span class="' + amtCls + '">' + sign + " " + fmt(a.amt) + "</span>";
li.querySelector(".act-title").textContent = a.title;
li.querySelector(".act-meta").textContent = a.meta;
ul.appendChild(li);
});
}
function applyTier(key) {
var t = TIERS[key];
state.tier = key;
document.documentElement.style.setProperty("--tier-a", t.from);
document.documentElement.style.setProperty("--tier-b", t.to);
document.documentElement.style.setProperty("--tier-ink", t.ink);
$("frontTier").textContent = t.label;
$("backTier").textContent = t.label;
$("milesNum").parentNode; // keep ref
countUp($("milesNum"), t.miles, 1100);
setProgress(t);
renderBenefits(t);
// toggle buttons
document.querySelectorAll(".tt-btn").forEach(function (b) {
var active = b.dataset.tier === key;
b.classList.toggle("is-active", active);
b.setAttribute("aria-pressed", String(active));
});
// redeem availability
var btn = $("redeemBtn");
if (t.miles < REDEEM_COST) {
btn.disabled = true;
btn.firstChild; // no-op
} else {
btn.disabled = false;
}
}
/* ---------- events ---------- */
document.querySelectorAll(".tt-btn").forEach(function (b) {
b.addEventListener("click", function () {
if (b.dataset.tier === state.tier) return;
applyTier(b.dataset.tier);
toast(TIERS[b.dataset.tier].label + " status loaded", true);
});
});
var flip = $("flip");
var flipInner = $("flipInner");
flipInner.addEventListener("click", function () {
var on = flip.classList.toggle("flipped");
flipInner.setAttribute("aria-pressed", String(on));
flipInner.setAttribute(
"aria-label",
on ? "Flip card to view membership details" : "Flip card to view membership QR"
);
});
$("redeemBtn").addEventListener("click", function (e) {
if (e.currentTarget.disabled) return;
var t = TIERS[state.tier];
var remaining = t.miles - REDEEM_COST;
countUp($("milesNum"), remaining, 800);
t.miles = remaining;
setProgress(t);
toast("Reward redeemed · " + fmt(REDEEM_COST) + " mi spent", true);
if (t.miles < REDEEM_COST) e.currentTarget.disabled = true;
});
/* ---------- init ---------- */
buildQR();
renderActivity();
applyTier("gold");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Skyward Club — Frequent Flyer Card</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>
<main class="wrap">
<header class="page-head">
<div class="brand">
<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="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3.5S18 4 16.5 5.5L13 9 4.8 7.2c-.3-.1-.7 0-.9.3l-.4.5c-.3.4-.1.9.3 1.1L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 2.8 5.2c.2.4.7.6 1.1.3l.5-.4c.3-.2.4-.6.3-.9Z"/></svg>
</span>
<div>
<p class="brand-name">Skyward Club</p>
<p class="brand-sub">Loyalty & Status</p>
</div>
</div>
<span class="tier-toggle" role="group" aria-label="Select membership tier">
<button class="tt-btn is-active" data-tier="gold" aria-pressed="true">Gold</button>
<button class="tt-btn" data-tier="platinum" aria-pressed="false">Platinum</button>
</span>
</header>
<section class="stage" aria-label="Membership card">
<div class="flip" id="flip">
<button class="flip-inner" id="flipInner" type="button" aria-label="Flip card to view membership QR" aria-pressed="false">
<!-- FRONT -->
<div class="face front">
<div class="card-top">
<div class="card-brand">
<span class="card-logo" aria-hidden="true">✈</span>
<span>Skyward Club</span>
</div>
<span class="tier-pill" id="frontTier">Gold</span>
</div>
<div class="miles">
<p class="miles-label">Available miles</p>
<p class="miles-value tnum"><span id="milesNum">0</span><span class="miles-unit">mi</span></p>
</div>
<div class="member">
<div>
<p class="m-label">Member</p>
<p class="m-name" id="memberName">A. NAKAMURA</p>
</div>
<div>
<p class="m-label">Member no.</p>
<p class="m-num tnum" id="memberNo">SK 4471 0982</p>
</div>
<div>
<p class="m-label">Member since</p>
<p class="m-num tnum">2018</p>
</div>
</div>
<p class="flip-hint">Tap card to show QR ⟳</p>
</div>
<!-- BACK -->
<div class="face back">
<div class="back-head">
<span class="tier-pill" id="backTier">Gold</span>
<span class="back-id tnum" id="backMemberNo">SK 4471 0982</span>
</div>
<div class="qr" id="qr" aria-hidden="true"></div>
<p class="qr-label">Scan at the gate for boarding & lounge access</p>
<p class="flip-hint">Tap card to flip back ⟳</p>
</div>
</button>
</div>
</section>
<section class="progress-card" aria-label="Progress to next tier">
<div class="progress-head">
<p class="p-title">Progress to <strong id="nextTierName">Platinum</strong></p>
<p class="p-sub tnum"><span id="progNow">0</span> / <span id="progGoal">75,000</span> mi</p>
</div>
<div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="bar">
<div class="bar-fill" id="barFill"></div>
</div>
<p class="p-foot"><span class="tnum" id="progRemain">0</span> miles to unlock next tier benefits</p>
</section>
<section class="cols">
<div class="panel benefits">
<h2 class="panel-title">Your benefits</h2>
<ul class="benefit-list" id="benefitList"></ul>
<button class="redeem-btn" id="redeemBtn" type="button">
<span aria-hidden="true">★</span> Redeem 25,000 mi → Upgrade reward
</button>
</div>
<div class="panel activity">
<h2 class="panel-title">Recent activity</h2>
<ul class="act-list" id="actList"></ul>
</div>
</section>
</main>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Frequent Flyer Card
A self-contained loyalty card for the fictional Skyward Club program. The hero is a credit-card-sized membership card with a tier-themed gradient, the member’s available miles in tabular figures, member name, number and join year. Tap it and it flips in 3D to reveal a generated membership QR for gate and lounge scanning. Below the card, an animated progress bar counts up the member’s miles and fills toward the next tier, while two panels list current benefits and a feed of recent flights and redemptions with credit and debit amounts.
The header toggle switches between Gold and Platinum status. Each tier rewrites the card palette, miles balance, benefit set, and the goal on the progress bar, with the numbers counting up smoothly on every change. The redeem button spends 25,000 miles, decrements the balance, re-runs the progress animation, and fires a toast; it disables itself automatically once the balance can no longer cover a redemption.
Everything is plain HTML, CSS, and vanilla JavaScript — no frameworks, no build step, no external assets beyond the Inter web font. Status pills, hover and active states, reduced-motion handling, and a responsive layout that collapses to a single column near 360px keep it mobile-first and accessible.
Illustrative UI only — fictional airline, not a real booking or flight system.