Hotel Digital Key Card
A mobile-phone-framed digital room key for Aurelia Hotels — shows hotel branding, room number, guest name, and validity dates. Tap the large 'Hold to unlock' button to trigger a locked → unlocking → unlocked animation sequence with an NFC pulse ring, then auto-relocks after a brief confirmation. Includes share key and room info quick actions.
MCP
Código
/* ── Design tokens ── */
:root {
--navy: #1a2b4a;
--navy-d: #0f1d36;
--navy-2: #2d4570;
--cream: #f7f3eb;
--cream-2: #ece5d4;
--bone: #fbf8f2;
--gold: #c9a649;
--gold-light: #e0c378;
--gold-d: #a88a2e;
--ink: #161e2c;
--ink-2: #2e3a52;
--warm-gray: #6c7280;
--line: rgba(22, 30, 44, 0.1);
--line-strong: rgba(22, 30, 44, 0.18);
--success: #4a7752;
--danger: #b34232;
--warning: #d99020;
--info: #4a6da0;
--font-display: "Cormorant Garamond", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--shadow-1: 0 1px 2px rgba(22, 30, 44, 0.06), 0 2px 8px rgba(22, 30, 44, 0.06);
--shadow-2: 0 12px 36px rgba(15, 29, 54, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: radial-gradient(circle at 30% 20%, rgba(201, 166, 73, 0.18), transparent 55%),
linear-gradient(160deg, #14213b 0%, #0f1d36 100%);
color: var(--ink);
-webkit-font-smoothing: antialiased;
display: grid;
place-items: center;
min-height: 100vh;
padding: 32px 16px;
}
/* ── Phone frame ── */
.scene {
perspective: 1200px;
}
.phone {
width: 360px;
background: #0d1929;
border-radius: 44px;
padding: 14px 0 28px;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.07), 0 40px 100px rgba(0, 0, 0, 0.7), inset 0 0 0 1px
rgba(255, 255, 255, 0.03);
display: flex;
flex-direction: column;
gap: 0;
position: relative;
overflow: hidden;
}
/* ── Status bar ── */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 22px 0;
color: rgba(255, 255, 255, 0.55);
font-size: 0.72rem;
font-family: var(--font-mono);
font-weight: 700;
}
.status-time {
color: rgba(255, 255, 255, 0.85);
font-size: 0.78rem;
}
.status-icons {
display: flex;
gap: 6px;
align-items: center;
font-size: 0.6rem;
}
/* ── Hotel header ── */
.hotel-header {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 22px 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.logo-mark {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--gold), var(--gold-d));
border-radius: 10px;
display: grid;
place-items: center;
font-family: var(--font-display);
font-size: 1.3rem;
font-weight: 700;
color: var(--navy-d);
flex-shrink: 0;
}
.hotel-name {
font-family: var(--font-display);
font-size: 1rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.92);
letter-spacing: 0.01em;
}
.hotel-loc {
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.45);
font-weight: 500;
margin-top: 1px;
}
/* ── Key card ── */
.key-card {
margin: 16px 18px;
background: linear-gradient(135deg, #1e3a6e 0%, #0f1d36 60%, #1a2b4a 100%);
border-radius: 18px;
padding: 20px 22px 18px;
border: 1px solid rgba(201, 166, 73, 0.22);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.06);
position: relative;
overflow: hidden;
}
.key-card::before {
content: "";
position: absolute;
top: -30px;
right: -30px;
width: 120px;
height: 120px;
background: radial-gradient(circle, rgba(201, 166, 73, 0.12), transparent 70%);
border-radius: 50%;
}
.key-card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.kicker {
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: rgba(201, 166, 73, 0.7);
font-weight: 700;
margin-bottom: 2px;
}
.room-number {
font-family: var(--font-display);
font-size: 2.8rem;
font-weight: 700;
color: #fff;
line-height: 1;
letter-spacing: -0.02em;
}
.key-card-right {
text-align: right;
}
.floor-number {
font-family: var(--font-mono);
font-size: 1.4rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.75);
}
.key-divider {
height: 1px;
background: linear-gradient(to right, rgba(201, 166, 73, 0.3), transparent);
margin: 12px 0;
}
.guest-line {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
}
.guest-name {
font-family: var(--font-display);
font-size: 1.1rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
letter-spacing: 0.01em;
}
.member-pill {
background: rgba(201, 166, 73, 0.18);
color: var(--gold-light);
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 3px 9px;
border-radius: 999px;
border: 1px solid rgba(201, 166, 73, 0.3);
}
.validity-row {
display: flex;
align-items: center;
gap: 10px;
}
.validity-item {
display: flex;
flex-direction: column;
gap: 1px;
}
.val-label {
font-size: 0.62rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: rgba(255, 255, 255, 0.4);
font-weight: 600;
}
.val-date {
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.8);
}
.validity-sep {
color: rgba(201, 166, 73, 0.5);
font-size: 1rem;
flex: 1;
text-align: center;
}
/* ── NFC unlock area ── */
.unlock-area {
padding: 10px 22px 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
position: relative;
}
.nfc-rings {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -48%);
width: 140px;
height: 140px;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
}
.nfc-rings.is-active {
opacity: 1;
}
.ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px solid rgba(201, 166, 73, 0.4);
animation: none;
}
.nfc-rings.is-active .ring-1 {
animation: pulse-ring 1.6s ease-out infinite;
}
.nfc-rings.is-active .ring-2 {
animation: pulse-ring 1.6s ease-out 0.4s infinite;
}
.nfc-rings.is-active .ring-3 {
animation: pulse-ring 1.6s ease-out 0.8s infinite;
}
@keyframes pulse-ring {
0% {
transform: scale(0.4);
opacity: 0.8;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
.unlock-btn {
width: 110px;
height: 110px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.12);
background: linear-gradient(145deg, #1e3460, #0f1d36);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.07);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s, border-color 0.3s, background 0.3s;
position: relative;
z-index: 1;
}
.unlock-btn:hover:not(:disabled) {
transform: scale(1.04);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.unlock-btn:active:not(:disabled) {
transform: scale(0.97);
}
.unlock-btn.is-unlocking {
border-color: var(--gold);
background: linear-gradient(145deg, #2d4570, #1a2b4a);
}
.unlock-btn.is-unlocked {
border-color: var(--success);
background: linear-gradient(145deg, #1e3b28, #0f2219);
}
.lock-icon {
font-size: 1.8rem;
line-height: 1;
transition: transform 0.3s;
}
.unlock-btn.is-unlocking .lock-icon {
transform: scale(0.85) rotate(-15deg);
}
.unlock-btn.is-unlocked .lock-icon {
transform: scale(1.1);
}
.unlock-label {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(255, 255, 255, 0.6);
text-align: center;
}
.unlock-btn.is-unlocking .unlock-label {
color: var(--gold-light);
}
.unlock-btn.is-unlocked .unlock-label {
color: #7ec98a;
}
.unlock-hint {
margin-top: 10px;
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.3);
font-weight: 500;
text-align: center;
transition: color 0.3s;
}
.unlock-hint.is-unlocking {
color: var(--gold);
}
.unlock-hint.is-unlocked {
color: #7ec98a;
}
/* ── Status strip ── */
.status-strip {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 0 22px 14px;
}
.status-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 1px;
}
.stat-label {
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(255, 255, 255, 0.28);
font-weight: 700;
}
.stat-val {
font-family: var(--font-mono);
font-size: 0.76rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.6);
}
.status-dot {
width: 3px;
height: 3px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.15);
}
/* ── Quick actions ── */
.quick-actions {
display: flex;
gap: 8px;
padding: 0 18px;
justify-content: center;
}
.qa-btn {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.09);
border-radius: var(--r-md);
color: rgba(255, 255, 255, 0.65);
font-family: var(--font-body);
font-size: 0.72rem;
font-weight: 600;
padding: 10px 6px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.qa-btn:hover {
background: rgba(201, 166, 73, 0.1);
border-color: rgba(201, 166, 73, 0.3);
color: var(--gold-light);
}
.qa-icon {
font-size: 1rem;
line-height: 1;
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--navy-d);
color: var(--bone);
padding: 11px 22px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: 0 10px 30px rgba(22, 30, 44, 0.28);
white-space: nowrap;
z-index: 999;
}
/* ── Responsive ── */
@media (max-width: 400px) {
.phone {
width: 100%;
border-radius: 28px;
}
}// ── Helpers ──
const $ = (id) => document.getElementById(id);
const toast = $("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2200);
}
// ── Live clock in status bar ──
function updateClock() {
const now = new Date();
const hh = String(now.getHours()).padStart(2, "0");
const mm = String(now.getMinutes()).padStart(2, "0");
$("statusTime").textContent = `${hh}:${mm}`;
}
updateClock();
setInterval(updateClock, 30000);
// ── Unlock state machine ──
// States: "locked" | "unlocking" | "unlocked"
let lockState = "locked";
let unlockTimer = null;
let relockTimer = null;
const btn = $("unlockBtn");
const lockIcon = $("lockIcon");
const unlockLbl = $("unlockLabel");
const unlockHint = $("unlockHint");
const nfcRings = $("nfcRings");
function setLockState(state) {
lockState = state;
btn.classList.remove("is-unlocking", "is-unlocked");
nfcRings.classList.remove("is-active");
unlockHint.classList.remove("is-unlocking", "is-unlocked");
if (state === "locked") {
lockIcon.textContent = "🔒";
unlockLbl.textContent = "Hold to unlock";
unlockHint.textContent = "Tap and hold near door reader";
} else if (state === "unlocking") {
btn.classList.add("is-unlocking");
nfcRings.classList.add("is-active");
unlockHint.classList.add("is-unlocking");
lockIcon.textContent = "🔓";
unlockLbl.textContent = "Unlocking…";
unlockHint.textContent = "Reading NFC key…";
} else if (state === "unlocked") {
btn.classList.add("is-unlocked");
unlockHint.classList.add("is-unlocked");
lockIcon.textContent = "✓";
unlockLbl.textContent = "Unlocked";
unlockHint.textContent = "Door open — re-locking in 3 s";
$("lastUsed").textContent = "Just now";
showToast("Room 512 unlocked successfully");
}
}
// ── Press-and-hold unlock (400 ms) ──
let holdStart = null;
function onPressStart(e) {
e.preventDefault();
if (lockState !== "locked") return;
holdStart = Date.now();
setLockState("unlocking");
unlockTimer = setTimeout(() => {
setLockState("unlocked");
relockTimer = setTimeout(() => setLockState("locked"), 3000);
}, 1400);
}
function onPressEnd() {
if (lockState === "unlocking") {
clearTimeout(unlockTimer);
setLockState("locked");
}
}
btn.addEventListener("mousedown", onPressStart);
btn.addEventListener("touchstart", onPressStart, { passive: false });
btn.addEventListener("mouseup", onPressEnd);
btn.addEventListener("mouseleave", onPressEnd);
btn.addEventListener("touchend", onPressEnd);
btn.addEventListener("touchcancel", onPressEnd);
// ── Quick action buttons ──
$("shareBtn").addEventListener("click", () => {
showToast("Share link copied — valid for 2 hours");
});
$("infoBtn").addEventListener("click", () => {
showToast("Room 512 · Deluxe Double · Floor 5 · Sea view");
});
$("helpBtn").addEventListener("click", () => {
showToast("Connecting to concierge…");
});
// ── Battery indicator (cosmetic cycling) ──
let battPct = 82;
setInterval(() => {
// gently drift to simulate real usage
battPct = Math.max(10, Math.min(100, battPct - (Math.random() < 0.3 ? 1 : 0)));
$("battPct").textContent = `${battPct}%`;
// update status-bar icon opacity
const icons = ["▮", "▮▮", "▮▮▮", "▮▮▮▮"];
$("battIcon").textContent = battPct > 60 ? icons[3] : battPct > 30 ? icons[2] : icons[1];
}, 15000);
// ── Init ──
setLockState("locked");<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Digital Key · Aurelia Hotels</title>
</head>
<body>
<!-- ── Phone mockup wrapper ── -->
<div class="scene">
<div class="phone" role="main" aria-label="Digital room key">
<!-- Status bar -->
<div class="status-bar">
<span class="status-time" id="statusTime">09:41</span>
<div class="status-icons">
<span class="sig-icon" title="Signal">▮▮▮</span>
<span class="wifi-icon" title="Wi-Fi">((•))</span>
<span class="batt-icon" id="battIcon" title="Battery">▮▮▮▮</span>
</div>
</div>
<!-- Hotel header -->
<div class="hotel-header">
<div class="logo-mark" aria-hidden="true">A</div>
<div class="hotel-info">
<p class="hotel-name">Aurelia Hotels</p>
<p class="hotel-loc">Gran Vía · Barcelona</p>
</div>
</div>
<!-- Key card -->
<div class="key-card">
<div class="key-card-top">
<div>
<p class="kicker">Room</p>
<p class="room-number" id="roomNumber">512</p>
</div>
<div class="key-card-right">
<p class="kicker">Floor</p>
<p class="floor-number">5</p>
</div>
</div>
<div class="key-divider"></div>
<div class="guest-line">
<p class="guest-name" id="guestName">Isabelle Moreau</p>
<p class="member-pill">Gold Member</p>
</div>
<div class="validity-row">
<div class="validity-item">
<span class="val-label">Check-in</span>
<span class="val-date" id="valIn">09 Jun 2026</span>
</div>
<div class="validity-sep">→</div>
<div class="validity-item">
<span class="val-label">Check-out</span>
<span class="val-date" id="valOut">12 Jun 2026</span>
</div>
</div>
</div>
<!-- NFC unlock area -->
<div class="unlock-area">
<div class="nfc-rings" id="nfcRings" aria-hidden="true">
<div class="ring ring-1"></div>
<div class="ring ring-2"></div>
<div class="ring ring-3"></div>
</div>
<button
class="unlock-btn"
id="unlockBtn"
type="button"
aria-label="Hold to unlock room door"
>
<span class="lock-icon" id="lockIcon" aria-hidden="true">🔒</span>
<span class="unlock-label" id="unlockLabel">Hold to unlock</span>
</button>
<p class="unlock-hint" id="unlockHint">Tap and hold near door reader</p>
</div>
<!-- Status strip -->
<div class="status-strip">
<div class="status-item">
<span class="stat-label">Battery</span>
<span class="stat-val" id="battPct">82%</span>
</div>
<div class="status-dot"></div>
<div class="status-item">
<span class="stat-label">Last used</span>
<span class="stat-val" id="lastUsed">Today, 07:23</span>
</div>
<div class="status-dot"></div>
<div class="status-item">
<span class="stat-label">Keys active</span>
<span class="stat-val">1 / 2</span>
</div>
</div>
<!-- Quick actions -->
<div class="quick-actions">
<button class="qa-btn" id="shareBtn" type="button">
<span class="qa-icon" aria-hidden="true">↗</span>
<span>Share key</span>
</button>
<button class="qa-btn" id="infoBtn" type="button">
<span class="qa-icon" aria-hidden="true">ℹ</span>
<span>Room info</span>
</button>
<button class="qa-btn" id="helpBtn" type="button">
<span class="qa-icon" aria-hidden="true">☎</span>
<span>Concierge</span>
</button>
</div>
</div>
</div>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Hotel Digital Key Card
A centered phone-width (~360 px) widget that simulates a mobile digital room key for Aurelia Hotels. The card displays the hotel logo, room number, guest name, and a check-in / check-out validity window. A large interactive button cycles through three states — Locked → Unlocking… → Unlocked ✓ — with a pulsing NFC ring animation during the unlock sequence and a brief success state before it auto-relocks. A status bar shows battery level and last-used timestamp, and two quick-action buttons let users share the key or view room information.