Hotel Rooms & Suites Page
A full marketing page listing Aurelia Hotels' room categories with filterable cards, view toggle, amenity badges, and from-price CTAs. Visitors can filter by category (All / Rooms / Suites / Family), switch between grid and list layouts, and click Book to receive toast confirmation.
MCP
程式碼
: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: var(--cream);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
/* ── Topbar ── */
.topbar {
position: sticky;
top: 0;
z-index: 30;
background: var(--navy-d);
color: var(--bone);
padding: 0 32px;
height: 64px;
display: flex;
align-items: center;
gap: 32px;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--bone);
flex-shrink: 0;
}
.brand-mark {
width: 34px;
height: 34px;
display: grid;
place-items: center;
border-radius: var(--r-sm);
background: var(--gold);
color: var(--navy-d);
font-family: var(--font-display);
font-weight: 700;
font-size: 1.2rem;
}
.brand-name {
font-family: var(--font-display);
font-size: 1.15rem;
font-weight: 700;
}
.brand-name em {
font-style: normal;
color: var(--gold-light);
}
.top-nav {
display: flex;
gap: 6px;
margin-left: 24px;
}
.top-nav a {
text-decoration: none;
color: rgba(251, 248, 242, 0.65);
font-size: 0.86rem;
font-weight: 500;
padding: 6px 13px;
border-radius: var(--r-sm);
transition: color 0.15s, background 0.15s;
}
.top-nav a:hover,
.top-nav a.active {
color: var(--bone);
background: rgba(255, 255, 255, 0.08);
}
.top-nav a.active {
color: var(--gold-light);
}
.nav-cta {
margin-left: auto;
background: var(--gold);
color: var(--navy-d);
font-weight: 700;
font-size: 0.84rem;
padding: 9px 20px;
border-radius: 999px;
text-decoration: none;
transition: background 0.15s;
}
.nav-cta:hover {
background: var(--gold-light);
}
/* ── Page hero ── */
.page-hero {
background: linear-gradient(160deg, var(--navy-2) 0%, var(--navy-d) 55%, #070f1e 100%);
color: var(--bone);
padding: 64px 32px 52px;
text-align: center;
position: relative;
overflow: hidden;
}
.page-hero::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(201, 166, 73, 0.18), transparent 70%);
}
.breadcrumb {
position: relative;
font-size: 0.75rem;
color: var(--gold-light);
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 16px;
}
.page-hero h1 {
position: relative;
font-family: var(--font-display);
font-weight: 700;
font-size: clamp(2.4rem, 5vw, 3.8rem);
line-height: 1.05;
color: var(--bone);
}
.hero-sub {
position: relative;
margin-top: 14px;
font-size: 0.95rem;
color: rgba(251, 248, 242, 0.7);
}
/* ── Controls bar ── */
.controls-bar {
max-width: 1200px;
margin: 0 auto;
padding: 20px 32px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.filter-pills {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.pill {
font-family: var(--font-body);
font-size: 0.86rem;
font-weight: 600;
padding: 8px 20px;
border: 1.5px solid var(--line-strong);
border-radius: 999px;
background: var(--bone);
color: var(--ink-2);
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.pill:hover {
border-color: var(--navy);
color: var(--navy);
}
.pill.active {
background: var(--navy);
border-color: var(--navy);
color: var(--bone);
}
.view-toggle {
display: flex;
gap: 4px;
}
.toggle-btn {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border: 1.5px solid var(--line-strong);
border-radius: var(--r-sm);
background: var(--bone);
color: var(--warm-gray);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.toggle-btn:hover {
border-color: var(--navy);
color: var(--navy);
}
.toggle-btn.active {
background: var(--navy);
border-color: var(--navy);
color: var(--bone);
}
/* ── Rooms wrap ── */
.rooms-wrap {
max-width: 1200px;
margin: 0 auto;
padding: 0 32px 72px;
}
/* ── Grid view ── */
.room-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
.room-grid.list-view {
grid-template-columns: 1fr;
}
/* ── Room card ── */
.room-card {
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-1);
display: flex;
flex-direction: column;
transition: box-shadow 0.18s, transform 0.18s;
}
.room-card:hover {
box-shadow: var(--shadow-2);
transform: translateY(-3px);
}
.list-view .room-card {
flex-direction: row;
}
/* ── Card image ── */
.card-img {
position: relative;
height: 200px;
flex-shrink: 0;
}
.list-view .card-img {
width: 260px;
height: auto;
min-height: 180px;
}
.card-img.img-classic {
background: linear-gradient(135deg, #4a5568, #2e3a52);
}
.card-img.img-deluxe {
background: linear-gradient(135deg, #3a5180, #1a2b4a);
}
.card-img.img-junior {
background: linear-gradient(135deg, #5a6e4a, #2e3a2e);
}
.card-img.img-signature {
background: linear-gradient(135deg, var(--gold-d), #4a3010);
}
.card-img.img-family {
background: linear-gradient(135deg, #5a4a80, #2a1e48);
}
.card-img::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(60% 70% at 70% 25%, rgba(255, 255, 255, 0.13), transparent 65%);
}
.card-badge {
position: absolute;
top: 14px;
left: 14px;
z-index: 1;
background: rgba(15, 29, 54, 0.82);
color: var(--gold-light);
font-size: 0.64rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 5px 11px;
border-radius: 999px;
}
.card-price-overlay {
position: absolute;
bottom: 14px;
right: 14px;
z-index: 1;
background: var(--gold);
color: var(--navy-d);
border-radius: var(--r-sm);
padding: 6px 12px;
text-align: right;
line-height: 1.2;
}
.card-price-overlay .from {
font-size: 0.58rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.card-price-overlay .amount {
font-family: var(--font-mono);
font-size: 1.1rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
/* ── Card body ── */
.card-body {
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
gap: 8px;
}
.card-body h3 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.5rem;
color: var(--navy-d);
line-height: 1.1;
}
.card-meta {
font-size: 0.82rem;
color: var(--warm-gray);
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.card-meta span::before {
margin-right: 4px;
}
.amenity-badges {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 2px;
}
.ab {
font-size: 0.68rem;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--cream);
color: var(--ink-2);
}
.ab.green {
background: rgba(74, 119, 82, 0.1);
border-color: rgba(74, 119, 82, 0.28);
color: var(--success);
}
.card-footer {
margin-top: auto;
padding-top: 14px;
border-top: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.card-footer .price-detail {
font-size: 0.78rem;
color: var(--warm-gray);
line-height: 1.4;
}
.card-footer .price-detail strong {
font-family: var(--font-mono);
color: var(--navy-d);
font-size: 1.0rem;
font-variant-numeric: tabular-nums;
}
.book-btn {
flex-shrink: 0;
font-family: var(--font-body);
font-size: 0.84rem;
font-weight: 700;
padding: 10px 20px;
border: none;
border-radius: var(--r-sm);
background: var(--navy);
color: var(--bone);
cursor: pointer;
transition: background 0.15s;
}
.book-btn:hover {
background: var(--navy-d);
}
.book-btn.secondary {
background: transparent;
border: 1.5px solid var(--navy);
color: var(--navy);
}
.book-btn.secondary:hover {
background: var(--navy);
color: var(--bone);
}
/* ── Empty state ── */
.empty {
text-align: center;
padding: 72px 24px;
color: var(--warm-gray);
font-style: italic;
background: var(--bone);
border: 1px dashed var(--line-strong);
border-radius: var(--r-md);
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--navy-d);
color: var(--bone);
padding: 12px 24px;
border-radius: 999px;
font-size: 0.88rem;
font-weight: 600;
box-shadow: var(--shadow-2);
z-index: 50;
white-space: nowrap;
}
/* ── Responsive ── */
@media (max-width: 960px) {
.top-nav {
display: none;
}
.controls-bar {
padding: 16px 20px;
}
.rooms-wrap {
padding: 0 20px 56px;
}
.list-view .card-img {
width: 200px;
}
}
@media (max-width: 560px) {
.topbar {
padding: 0 18px;
gap: 16px;
}
.page-hero {
padding: 44px 20px 36px;
}
.controls-bar {
padding: 14px 18px;
flex-wrap: wrap;
}
.rooms-wrap {
padding: 0 18px 48px;
}
.room-grid {
grid-template-columns: 1fr;
}
.list-view .room-card {
flex-direction: column;
}
.list-view .card-img {
width: 100%;
min-height: 160px;
}
}// ── Room catalogue data ───────────────────────────────────────────────────────
const ROOMS = [
{
id: "classic-double",
name: "Classic Double",
cat: "room",
img: "img-classic",
tag: "Smart value",
size: 24,
sleeps: 2,
fromPrice: 148,
amenities: ["Wi-Fi", "City view", "Free Cancellation"],
desc: "Courtyard view · 24 m² · Queen bed",
},
{
id: "deluxe-double",
name: "Deluxe Double",
cat: "room",
img: "img-deluxe",
tag: "Most popular",
size: 30,
sleeps: 2,
fromPrice: 192,
amenities: ["Wi-Fi", "Balcony", "Breakfast", "Free Cancellation"],
desc: "City view · 30 m² · King bed",
},
{
id: "junior-suite",
name: "Junior Suite",
cat: "suite",
img: "img-junior",
tag: "Suite",
size: 44,
sleeps: 2,
fromPrice: 274,
amenities: ["Wi-Fi", "Balcony", "Breakfast", "Spa Access", "Free Cancellation"],
desc: "Balcony · 44 m² · King bed + lounge area",
},
{
id: "signature-suite",
name: "Signature Suite",
cat: "suite",
img: "img-signature",
tag: "Top suite",
size: 66,
sleeps: 2,
fromPrice: 418,
amenities: ["Wi-Fi", "Terrace", "Breakfast", "Spa Access", "Butler", "Free Cancellation"],
desc: "Terrace · 66 m² · Separate living room",
},
{
id: "family-room",
name: "Family Room",
cat: "family",
img: "img-family",
tag: "Family",
size: 42,
sleeps: 4,
fromPrice: 236,
amenities: ["Wi-Fi", "City view", "Breakfast", "Free Cancellation"],
desc: "City view · 42 m² · King bed + 2 singles",
},
{
id: "family-suite",
name: "Family Suite",
cat: "family",
img: "img-signature",
tag: "Family Suite",
size: 58,
sleeps: 5,
fromPrice: 354,
amenities: ["Wi-Fi", "Balcony", "Breakfast", "Spa Access", "Free Cancellation"],
desc: "Balcony · 58 m² · Two bedrooms",
},
];
// ── State ─────────────────────────────────────────────────────────────────────
let activeCat = "all";
let isGrid = true;
// ── DOM refs ──────────────────────────────────────────────────────────────────
const grid = document.getElementById("roomGrid");
const emptyEl = document.getElementById("emptyMsg");
const toast = document.getElementById("toast");
// ── Helpers ───────────────────────────────────────────────────────────────────
const eur = (n) => "€" + n.toLocaleString("en-GB");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2200);
}
// ── Render ────────────────────────────────────────────────────────────────────
function render() {
const visible = ROOMS.filter((r) => activeCat === "all" || r.cat === activeCat);
emptyEl.hidden = visible.length > 0;
if (!isGrid) {
grid.classList.add("list-view");
} else {
grid.classList.remove("list-view");
}
grid.innerHTML = visible
.map((r) => {
const greenBadges = new Set(["Free Cancellation", "Breakfast"]);
const badges = r.amenities
.map((a) => `<span class="ab ${greenBadges.has(a) ? "green" : ""}">${a}</span>`)
.join("");
return `
<article class="room-card" data-id="${r.id}">
<div class="card-img ${r.img}">
<span class="card-badge">${r.tag}</span>
<div class="card-price-overlay">
<div class="from">From</div>
<div class="amount">${eur(r.fromPrice)}</div>
</div>
</div>
<div class="card-body">
<h3>${r.name}</h3>
<p class="card-meta">
<span>📐 ${r.size} m²</span>
<span>👤 Sleeps ${r.sleeps}</span>
</p>
<p style="font-size:0.82rem;color:var(--warm-gray)">${r.desc}</p>
<div class="amenity-badges">${badges}</div>
<div class="card-footer">
<div class="price-detail">
<strong>${eur(r.fromPrice)}</strong><br/>
per night · from
</div>
<div style="display:flex;gap:8px">
<button class="book-btn secondary" data-action="view" data-name="${r.name}">View room</button>
<button class="book-btn" data-action="book" data-name="${r.name}">Book</button>
</div>
</div>
</div>
</article>`;
})
.join("");
}
// ── Category filter ───────────────────────────────────────────────────────────
document.getElementById("filterPills").addEventListener("click", (e) => {
const pill = e.target.closest(".pill");
if (!pill) return;
activeCat = pill.dataset.cat;
document.querySelectorAll(".pill").forEach((p) => {
p.classList.toggle("active", p === pill);
p.setAttribute("aria-selected", p === pill ? "true" : "false");
});
render();
});
// ── View toggle ───────────────────────────────────────────────────────────────
document.getElementById("gridBtn").addEventListener("click", () => {
isGrid = true;
document.getElementById("gridBtn").classList.add("active");
document.getElementById("listBtn").classList.remove("active");
document.getElementById("gridBtn").setAttribute("aria-pressed", "true");
document.getElementById("listBtn").setAttribute("aria-pressed", "false");
render();
});
document.getElementById("listBtn").addEventListener("click", () => {
isGrid = false;
document.getElementById("listBtn").classList.add("active");
document.getElementById("gridBtn").classList.remove("active");
document.getElementById("listBtn").setAttribute("aria-pressed", "true");
document.getElementById("gridBtn").setAttribute("aria-pressed", "false");
render();
});
// ── CTA delegation ────────────────────────────────────────────────────────────
grid.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const { action, name } = btn.dataset;
if (action === "book") {
showToast(`${name} — reservation started for 9–12 Jun 2026 ✓`);
} else if (action === "view") {
showToast(`Opening details for ${name}…`);
}
});
// ── Init ──────────────────────────────────────────────────────────────────────
render();<!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>Rooms & Suites · Aurelia Hotels</title>
</head>
<body>
<!-- ── Nav ── -->
<header class="topbar">
<a class="brand" href="#">
<span class="brand-mark">A</span>
<span class="brand-name">Aurelia <em>Hotels</em></span>
</a>
<nav class="top-nav">
<a href="#">Overview</a>
<a href="#" class="active">Rooms</a>
<a href="#">Amenities</a>
<a href="#">Dining</a>
<a href="#">Gallery</a>
</nav>
<a class="nav-cta" href="#">Book now</a>
</header>
<!-- ── Page header ── -->
<section class="page-hero">
<p class="breadcrumb">Aurelia · Madrid › Rooms & Suites</p>
<h1>Rooms & Suites</h1>
<p class="hero-sub">Choose your sanctuary — 9–12 Jun 2026 · 3 nights · Madrid</p>
</section>
<!-- ── Controls bar ── -->
<div class="controls-bar">
<div class="filter-pills" id="filterPills" role="tablist" aria-label="Room category">
<button class="pill active" data-cat="all" role="tab" aria-selected="true">All</button>
<button class="pill" data-cat="room" role="tab" aria-selected="false">Rooms</button>
<button class="pill" data-cat="suite" role="tab" aria-selected="false">Suites</button>
<button class="pill" data-cat="family" role="tab" aria-selected="false">Family</button>
</div>
<div class="view-toggle" aria-label="Layout toggle">
<button class="toggle-btn active" id="gridBtn" title="Grid view" aria-pressed="true">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="1" y="1" width="6" height="6" rx="1.5" fill="currentColor"/>
<rect x="9" y="1" width="6" height="6" rx="1.5" fill="currentColor"/>
<rect x="1" y="9" width="6" height="6" rx="1.5" fill="currentColor"/>
<rect x="9" y="9" width="6" height="6" rx="1.5" fill="currentColor"/>
</svg>
</button>
<button class="toggle-btn" id="listBtn" title="List view" aria-pressed="false">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<rect x="1" y="2" width="14" height="3" rx="1.5" fill="currentColor"/>
<rect x="1" y="7" width="14" height="3" rx="1.5" fill="currentColor"/>
<rect x="1" y="12" width="14" height="3" rx="1.5" fill="currentColor"/>
</svg>
</button>
</div>
</div>
<!-- ── Room grid ── -->
<main class="rooms-wrap" id="roomsWrap">
<div class="room-grid" id="roomGrid"></div>
<p class="empty" id="emptyMsg" hidden>No rooms match this category.</p>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Rooms & Suites Page
A polished marketing index page for Aurelia Hotels’ accommodation catalogue. The sticky nav carries the gold “A” brand mark; a cinematic page header uses a navy-gradient wash with a subtitle and breadcrumb. A pill-style category bar lets guests filter between All, Rooms, Suites, and Family room types, while a grid/list view toggle rearranges the card layout. Each room card features a CSS-gradient image area, room name, size and sleeps metadata, a row of amenity badges (Wi-Fi, Balcony, Breakfast, Spa Access, Free Cancellation), a starting nightly rate, and a “View Room” button that fires a toast. The four room tiers — Classic, Deluxe, Junior Suite, Signature Suite — carry internally consistent figures anchored to a 9–12 Jun 2026 availability window.