Hotel Gallery & Virtual Tour
A filterable photo gallery and virtual tour page for Aurelia Hotels, using CSS-gradient tiles by category (Rooms, Dining, Spa, Views, Events), with a full lightbox offering prev/next navigation, captions, and keyboard support.
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: 60px 32px 48px;
text-align: center;
position: relative;
overflow: hidden;
}
.page-hero::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(ellipse 70% 50% at 50% 0%, rgba(201, 166, 73, 0.16), 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: 14px;
}
.page-hero h1 {
position: relative;
font-family: var(--font-display);
font-weight: 700;
font-size: clamp(2.2rem, 5vw, 3.6rem);
line-height: 1.05;
}
.hero-sub {
position: relative;
margin-top: 12px;
font-size: 0.95rem;
color: rgba(251, 248, 242, 0.68);
}
/* ── Filter bar ── */
.filter-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: 7px 18px;
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);
}
.gallery-count {
font-size: 0.82rem;
color: var(--warm-gray);
font-weight: 600;
white-space: nowrap;
}
.gallery-count span {
font-family: var(--font-mono);
color: var(--navy-d);
font-variant-numeric: tabular-nums;
}
/* ── Gallery wrap ── */
.gallery-wrap {
max-width: 1200px;
margin: 0 auto;
padding: 0 32px 72px;
}
/* ── Gallery grid (CSS columns masonry) ── */
.gallery-grid {
column-count: 3;
column-gap: 16px;
}
.gallery-tile {
break-inside: avoid;
margin-bottom: 16px;
border-radius: var(--r-md);
overflow: hidden;
position: relative;
cursor: pointer;
box-shadow: var(--shadow-1);
transition: box-shadow 0.18s, transform 0.18s;
}
.gallery-tile:hover {
box-shadow: var(--shadow-2);
transform: scale(1.02);
}
.tile-img {
width: 100%;
display: block;
position: relative;
}
/* Tile gradient fill — heights vary for masonry effect */
.tile-img.h-sm {
height: 180px;
}
.tile-img.h-md {
height: 240px;
}
.tile-img.h-lg {
height: 300px;
}
.tile-img.h-xl {
height: 360px;
}
/* Per-category gradients */
.tile-img.g-rooms {
background: linear-gradient(135deg, #3a5180, #1a2b4a);
}
.tile-img.g-rooms-2 {
background: linear-gradient(155deg, #5a6e88, #2d4570);
}
.tile-img.g-dining {
background: linear-gradient(135deg, #5a3a28, #2e1a10);
}
.tile-img.g-dining-2 {
background: linear-gradient(155deg, #7a5040, #3e2018);
}
.tile-img.g-spa {
background: linear-gradient(135deg, #3a5a4a, #1a3028);
}
.tile-img.g-spa-2 {
background: linear-gradient(155deg, #4a6858, #2a3e34);
}
.tile-img.g-views {
background: linear-gradient(135deg, #2a4a6a, #0e2038);
}
.tile-img.g-views-2 {
background: linear-gradient(155deg, #4a6880, #1e3856);
}
.tile-img.g-events {
background: linear-gradient(135deg, var(--gold-d), #4a2e08);
}
.tile-img.g-events-2 {
background: linear-gradient(155deg, #d0a050, #6a4018);
}
.tile-img::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(70% 60% at 65% 20%, rgba(255, 255, 255, 0.14), transparent 70%);
}
/* ── Tile hover overlay ── */
.tile-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(10, 18, 34, 0.82) 0%, transparent 55%);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 16px;
opacity: 0;
transition: opacity 0.2s;
}
.gallery-tile:hover .tile-overlay {
opacity: 1;
}
.tile-overlay-title {
font-family: var(--font-display);
font-size: 1.1rem;
font-weight: 700;
color: var(--bone);
line-height: 1.2;
}
.tile-overlay-cat {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--gold-light);
margin-top: 4px;
}
/* ── 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);
}
/* ── Lightbox ── */
.lightbox {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
}
.lightbox[hidden] {
display: none;
}
.lb-backdrop {
position: absolute;
inset: 0;
background: rgba(7, 14, 28, 0.88);
cursor: pointer;
}
.lb-panel {
position: relative;
z-index: 1;
width: min(860px, 94vw);
background: var(--navy-d);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: 0 32px 80px rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
}
.lb-close {
position: absolute;
top: 14px;
right: 14px;
z-index: 10;
background: rgba(255, 255, 255, 0.12);
border: none;
color: var(--bone);
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 1rem;
cursor: pointer;
display: grid;
place-items: center;
transition: background 0.15s;
}
.lb-close:hover {
background: rgba(255, 255, 255, 0.22);
}
.lb-image {
width: 100%;
height: 420px;
position: relative;
flex-shrink: 0;
}
.lb-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: rgba(255, 255, 255, 0.14);
border: none;
color: var(--bone);
width: 44px;
height: 44px;
border-radius: 50%;
font-size: 1.6rem;
cursor: pointer;
display: grid;
place-items: center;
transition: background 0.15s;
line-height: 1;
}
.lb-nav:hover {
background: rgba(255, 255, 255, 0.26);
}
.lb-prev {
left: 16px;
}
.lb-next {
right: 16px;
}
.lb-caption {
padding: 20px 24px 22px;
background: var(--navy-d);
color: var(--bone);
}
.lb-caption-top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 6px;
}
.lb-caption h2 {
font-family: var(--font-display);
font-size: 1.6rem;
font-weight: 700;
line-height: 1.1;
}
.lb-cat-badge {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--navy-d);
background: var(--gold);
padding: 4px 10px;
border-radius: 999px;
white-space: nowrap;
}
.lb-caption p {
font-size: 0.86rem;
color: rgba(251, 248, 242, 0.65);
line-height: 1.55;
}
.lb-counter {
font-family: var(--font-mono);
font-size: 0.76rem;
font-variant-numeric: tabular-nums;
color: var(--gold-light);
margin-top: 8px;
}
/* ── 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;
}
.gallery-grid {
column-count: 2;
}
.lb-image {
height: 320px;
}
}
@media (max-width: 560px) {
.topbar {
padding: 0 18px;
gap: 16px;
}
.page-hero {
padding: 44px 20px 36px;
}
.filter-bar {
padding: 14px 18px;
flex-wrap: wrap;
}
.gallery-wrap {
padding: 0 18px 48px;
}
.gallery-grid {
column-count: 2;
column-gap: 10px;
}
.gallery-tile {
margin-bottom: 10px;
}
.lb-image {
height: 240px;
}
.lb-caption h2 {
font-size: 1.2rem;
}
}
@media (max-width: 380px) {
.gallery-grid {
column-count: 1;
}
}// ── Gallery data ──────────────────────────────────────────────────────────────
const TILES = [
// Rooms
{
id: 1,
cat: "rooms",
title: "Signature Suite — Living Room",
grad: "g-rooms",
height: "h-xl",
desc: "The 66 m² Signature Suite living area, 14th floor. Hand-stitched velvet furnishings, panoramic terrace doors, and curated local art. June 2026.",
},
{
id: 2,
cat: "rooms",
title: "Classic Double — City View",
grad: "g-rooms-2",
height: "h-md",
desc: "A 24 m² Classic Double overlooking the Gran Vía. Queen bed, bespoke Italian linens, and blackout drapes.",
},
{
id: 3,
cat: "rooms",
title: "Junior Suite — Bedroom",
grad: "g-rooms",
height: "h-lg",
desc: "The Junior Suite bedroom with a king bed, herringbone parquet, and a private balcony facing the Royal Palace.",
},
{
id: 4,
cat: "rooms",
title: "Deluxe Double — Bathroom",
grad: "g-rooms-2",
height: "h-sm",
desc: "Carrara marble bathroom in the Deluxe Double. Freestanding bath, rain shower, and Byredo toiletries.",
},
// Dining
{
id: 5,
cat: "dining",
title: "Restaurante Laurel — Table 9",
grad: "g-dining",
height: "h-lg",
desc: "Intimate table setting for the 8-course tasting menu. Baccarat crystal, Limoges china, and a sommelier-curated pairing of Spanish natural wines.",
},
{
id: 6,
cat: "dining",
title: "Cenit Rooftop Bar — Terrace",
grad: "g-dining-2",
height: "h-xl",
desc: "The 14th-floor Cenit terrace at dusk. Craft cocktails and a panoramic skyline during the June residency, 9–12 Jun 2026.",
},
{
id: 7,
cat: "dining",
title: "Breakfast Buffet — Garden Room",
grad: "g-dining",
height: "h-sm",
desc: "Morning light in the Garden Room breakfast service. Local charcuterie, pastries from Mallorca, and house-pressed orange juice.",
},
{
id: 8,
cat: "dining",
title: "Chef's Table Experience",
grad: "g-dining-2",
height: "h-md",
desc: "An intimate 6-seat chef's table in the kitchen, led by Executive Chef Marcos Soto. Available Friday and Saturday evenings.",
},
// Spa
{
id: 9,
cat: "spa",
title: "Hammam & Steam Room",
grad: "g-spa",
height: "h-md",
desc: "Our 1,200 m² wellness sanctuary. Hand-laid mosaic tiles, aromatic steam, and a 38°C hammam plunge pool.",
},
{
id: 10,
cat: "spa",
title: "Treatment Suite — Serenity",
grad: "g-spa-2",
height: "h-lg",
desc: "A private double-treatment suite for couples. Aurelia Signature massage with orange blossom oil, 90 minutes.",
},
{
id: 11,
cat: "spa",
title: "Indoor Pool — Morning Lap",
grad: "g-spa",
height: "h-xl",
desc: "The heated 25-metre indoor pool before the morning rush. Available from 07:00 daily for exclusive lane swimming.",
},
{
id: 12,
cat: "spa",
title: "Fitness Studio — Strength Zone",
grad: "g-spa-2",
height: "h-sm",
desc: "Technogym Artis strength equipment and free weights. Personal training available by advance booking.",
},
// Views
{
id: 13,
cat: "views",
title: "Madrid Skyline — Sunrise",
grad: "g-views",
height: "h-xl",
desc: "Looking east from the Signature Suite terrace, 14th floor. Sunrise over the Castellana, 09 Jun 2026, 06:42.",
},
{
id: 14,
cat: "views",
title: "Rooftop — Golden Hour",
grad: "g-views-2",
height: "h-lg",
desc: "Cenit Bar terrace facing the Sierra de Guadarrama at golden hour, 10 Jun 2026, 21:15.",
},
{
id: 15,
cat: "views",
title: "Courtyard Garden — Midday",
grad: "g-views",
height: "h-md",
desc: "The private courtyard garden, planted with olive, orange, and jasmine. Open to all guests for al-fresco reading.",
},
{
id: 16,
cat: "views",
title: "Gran Vía — Night Panorama",
grad: "g-views-2",
height: "h-sm",
desc: "A panoramic long-exposure of the Gran Vía from Room 1104, 11 Jun 2026 at 23:00.",
},
// Events
{
id: 17,
cat: "events",
title: "Salón Real — Wedding Reception",
grad: "g-events",
height: "h-lg",
desc: "The Salón Real ballroom dressed for a 120-seat wedding reception on 10 Jun 2026. Floral design by Botanica Madrid.",
},
{
id: 18,
cat: "events",
title: "Boardroom — Executive Summit",
grad: "g-events-2",
height: "h-sm",
desc: "24-seat boardroom configuration for the Iberia Leadership Forum, 9 Jun 2026. Full AV and live-translation booths.",
},
{
id: 19,
cat: "events",
title: "Terrace Cocktail Hour",
grad: "g-events",
height: "h-md",
desc: "Pre-dinner cocktails on the 8th-floor terrace for 60 guests, 12 Jun 2026. Bespoke Aurelia spritz and live jazz.",
},
{
id: 20,
cat: "events",
title: "Gala Dinner — Table Styling",
grad: "g-events-2",
height: "h-xl",
desc: "Round tables dressed in ivory and gold for a 90-person gala dinner in the Salón Dorado. Custom centrepieces by Flores Aurelia.",
},
];
// ── State ─────────────────────────────────────────────────────────────────────
let activeCat = "all";
let lightboxIndex = 0;
let visibleTiles = [];
// ── DOM refs ──────────────────────────────────────────────────────────────────
const galleryGrid = document.getElementById("galleryGrid");
const emptyEl = document.getElementById("emptyMsg");
const tileCountEl = document.getElementById("tileCount");
const lightbox = document.getElementById("lightbox");
const lbImage = document.getElementById("lbImage");
const lbTitle = document.getElementById("lbTitle");
const lbDesc = document.getElementById("lbDesc");
const lbCatBadge = document.getElementById("lbCatBadge");
const lbCurrent = document.getElementById("lbCurrent");
const lbTotal = document.getElementById("lbTotal");
const toast = document.getElementById("toast");
// ── Helpers ───────────────────────────────────────────────────────────────────
const CAT_LABEL = {
rooms: "Rooms",
dining: "Dining",
spa: "Spa",
views: "Views",
events: "Events",
};
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2200);
}
// ── Render grid ───────────────────────────────────────────────────────────────
function renderGrid() {
visibleTiles = TILES.filter((t) => activeCat === "all" || t.cat === activeCat);
emptyEl.hidden = visibleTiles.length > 0;
tileCountEl.textContent = visibleTiles.length;
galleryGrid.innerHTML = visibleTiles
.map(
(t, i) => `
<div class="gallery-tile" data-index="${i}" role="button" tabindex="0" aria-label="Open photo: ${t.title}">
<div class="tile-img ${t.grad} ${t.height}"></div>
<div class="tile-overlay">
<p class="tile-overlay-title">${t.title}</p>
<p class="tile-overlay-cat">${CAT_LABEL[t.cat] || t.cat}</p>
</div>
</div>`
)
.join("");
}
// ── Open lightbox ─────────────────────────────────────────────────────────────
function openLightbox(index) {
lightboxIndex = index;
updateLightbox();
lightbox.hidden = false;
document.body.style.overflow = "hidden";
document.getElementById("lbClose").focus();
}
function updateLightbox() {
const t = visibleTiles[lightboxIndex];
if (!t) return;
// Rebuild gradient in lightbox image area
lbImage.className = `lb-image tile-img ${t.grad}`;
lbImage.style.height = "420px";
lbTitle.textContent = t.title;
lbDesc.textContent = t.desc;
lbCatBadge.textContent = CAT_LABEL[t.cat] || t.cat;
lbCurrent.textContent = lightboxIndex + 1;
lbTotal.textContent = visibleTiles.length;
// Show/hide nav when only one tile
document.getElementById("lbPrev").style.visibility =
visibleTiles.length > 1 ? "visible" : "hidden";
document.getElementById("lbNext").style.visibility =
visibleTiles.length > 1 ? "visible" : "hidden";
}
function closeLightbox() {
lightbox.hidden = true;
document.body.style.overflow = "";
}
function prevPhoto() {
lightboxIndex = (lightboxIndex - 1 + visibleTiles.length) % visibleTiles.length;
updateLightbox();
}
function nextPhoto() {
lightboxIndex = (lightboxIndex + 1) % visibleTiles.length;
updateLightbox();
}
// ── 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");
});
renderGrid();
});
// ── Grid click / keyboard ─────────────────────────────────────────────────────
galleryGrid.addEventListener("click", (e) => {
const tile = e.target.closest(".gallery-tile");
if (!tile) return;
openLightbox(Number(tile.dataset.index));
});
galleryGrid.addEventListener("keydown", (e) => {
if (e.key !== "Enter" && e.key !== " ") return;
const tile = e.target.closest(".gallery-tile");
if (!tile) return;
e.preventDefault();
openLightbox(Number(tile.dataset.index));
});
// ── Lightbox controls ─────────────────────────────────────────────────────────
document.getElementById("lbClose").addEventListener("click", closeLightbox);
document.getElementById("lbBackdrop").addEventListener("click", closeLightbox);
document.getElementById("lbPrev").addEventListener("click", prevPhoto);
document.getElementById("lbNext").addEventListener("click", nextPhoto);
document.addEventListener("keydown", (e) => {
if (lightbox.hidden) return;
if (e.key === "Escape") closeLightbox();
if (e.key === "ArrowLeft") prevPhoto();
if (e.key === "ArrowRight") nextPhoto();
});
// ── Init ──────────────────────────────────────────────────────────────────────
renderGrid();<!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>Gallery & Virtual Tour · 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="#">Rooms</a>
<a href="#">Amenities</a>
<a href="#">Dining</a>
<a href="#" class="active">Gallery</a>
</nav>
<a class="nav-cta" href="#">Book now</a>
</header>
<!-- ── Page hero ── -->
<section class="page-hero">
<p class="breadcrumb">Aurelia · Madrid › Gallery & Virtual Tour</p>
<h1>Gallery & Virtual Tour</h1>
<p class="hero-sub">Explore Aurelia Hotels Madrid through curated photography</p>
</section>
<!-- ── Filter bar ── -->
<div class="filter-bar">
<div class="filter-pills" id="filterPills" role="tablist" aria-label="Gallery category">
<button class="pill active" data-cat="all" role="tab" aria-selected="true">All</button>
<button class="pill" data-cat="rooms" role="tab" aria-selected="false">Rooms</button>
<button class="pill" data-cat="dining" role="tab" aria-selected="false">Dining</button>
<button class="pill" data-cat="spa" role="tab" aria-selected="false">Spa</button>
<button class="pill" data-cat="views" role="tab" aria-selected="false">Views</button>
<button class="pill" data-cat="events" role="tab" aria-selected="false">Events</button>
</div>
<p class="gallery-count"><span id="tileCount">0</span> photos</p>
</div>
<!-- ── Gallery grid ── -->
<main class="gallery-wrap">
<div class="gallery-grid" id="galleryGrid"></div>
<p class="empty" id="emptyMsg" hidden>No photos in this category.</p>
</main>
<!-- ── Lightbox ── -->
<div class="lightbox" id="lightbox" hidden role="dialog" aria-modal="true" aria-label="Photo lightbox">
<div class="lb-backdrop" id="lbBackdrop"></div>
<div class="lb-panel">
<button class="lb-close" id="lbClose" aria-label="Close lightbox">✕</button>
<button class="lb-nav lb-prev" id="lbPrev" aria-label="Previous photo">‹</button>
<div class="lb-image" id="lbImage"></div>
<button class="lb-nav lb-next" id="lbNext" aria-label="Next photo">›</button>
<div class="lb-caption">
<div class="lb-caption-top">
<h2 id="lbTitle"></h2>
<span class="lb-cat-badge" id="lbCatBadge"></span>
</div>
<p id="lbDesc"></p>
<p class="lb-counter"><span id="lbCurrent">1</span> / <span id="lbTotal">1</span></p>
</div>
</div>
</div>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Gallery & Virtual Tour
A polished media gallery page for Aurelia Hotels Madrid. A sticky nav and navy-gradient hero open the page, followed by a category filter bar (All · Rooms · Dining · Spa · Views · Events) that instantly reflows the masonry-style CSS grid. Each tile is a CSS-gradient “photo” with a hover overlay showing the title and category label. Clicking any tile opens a lightbox — a full-screen modal with the enlarged tile, a title, caption, and category badge. Prev and next buttons (with keyboard ← → support) cycle through the visible filtered set, and pressing Escape or clicking the backdrop closes the lightbox.