Salon — Portfolio / Gallery
An image-forward portfolio page for Maison Lumière Salon that presents fifteen recent looks as a masonry-style grid of gradient work tiles, each revealing the stylist and service on hover. Category chips with live counts filter cuts, color, updos and nails in place, while a running result line stays in sync. Clicking any tile opens an elegant lightbox with previous and next navigation, the full caption, and a Book this look action that fires a refined toast confirmation.
MCP
程式碼
: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 4px 14px rgba(28, 24, 20, 0.06);
--shadow-md: 0 10px 30px rgba(28, 24, 20, 0.12), 0 2px 8px rgba(28, 24, 20, 0.06);
--shadow-lg: 0 30px 80px rgba(28, 24, 20, 0.32);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
h1,
h2,
h3 {
font-family: var(--serif);
font-weight: 600;
margin: 0;
color: var(--ink);
}
button {
font-family: inherit;
}
.page {
max-width: 1120px;
margin: 0 auto;
padding: 28px clamp(18px, 5vw, 48px) 64px;
}
/* ---------- Masthead ---------- */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding-bottom: 22px;
border-bottom: 1px solid var(--line);
flex-wrap: wrap;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-mark {
width: 46px;
height: 46px;
display: grid;
place-items: center;
border-radius: 50%;
border: 1px solid var(--gold);
color: var(--gold-d);
font-family: var(--serif);
font-weight: 700;
font-size: 18px;
letter-spacing: 0.04em;
background: radial-gradient(circle at 30% 25%, var(--white), var(--gold-soft));
}
.brand-text {
display: flex;
flex-direction: column;
line-height: 1.1;
}
.brand-eyebrow {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.22em;
color: var(--muted);
}
.brand-name {
font-family: var(--serif);
font-size: 22px;
font-weight: 600;
color: var(--ink);
}
.masthead-nav {
display: flex;
align-items: center;
gap: 22px;
}
.masthead-link {
font-size: 13px;
letter-spacing: 0.02em;
color: var(--ink-2);
text-decoration: none;
padding-bottom: 3px;
border-bottom: 1px solid transparent;
transition: color 0.2s, border-color 0.2s;
}
.masthead-link:hover {
color: var(--ink);
}
.masthead-link.is-active {
color: var(--gold-d);
border-color: var(--gold);
}
/* ---------- Buttons ---------- */
.btn {
border: none;
cursor: pointer;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 10px 20px;
transition: transform 0.16s, box-shadow 0.2s, background 0.2s, color 0.2s;
}
.btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.btn:active {
transform: translateY(1px);
}
.btn-ghost {
background: transparent;
border: 1px solid var(--line-2);
color: var(--ink);
}
.btn-ghost:hover {
border-color: var(--gold);
color: var(--gold-d);
}
.btn-gold {
background: linear-gradient(135deg, var(--gold), var(--gold-d));
color: var(--white);
box-shadow: var(--shadow-sm);
}
.btn-gold:hover {
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
/* ---------- Hero ---------- */
.hero {
text-align: center;
padding: 54px 0 36px;
max-width: 640px;
margin: 0 auto;
}
.eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.3em;
color: var(--gold-d);
margin: 0 0 12px;
}
.hero-title {
font-size: clamp(40px, 7vw, 66px);
line-height: 1.02;
font-weight: 600;
letter-spacing: -0.01em;
}
.hero-sub {
margin: 18px auto 0;
max-width: 480px;
color: var(--ink-2);
font-size: 15.5px;
}
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
margin: 8px 0 30px;
}
.chips {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
.chip {
border: 1px solid var(--line-2);
background: var(--white);
color: var(--ink-2);
border-radius: 999px;
padding: 9px 18px;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.02em;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: background 0.2s, color 0.2s, border-color 0.2s, transform 0.16s;
}
.chip:hover {
border-color: var(--gold);
color: var(--ink);
}
.chip:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.chip.is-active {
background: var(--ink);
border-color: var(--ink);
color: var(--white);
}
.chip-count {
font-size: 11px;
font-weight: 600;
min-width: 18px;
text-align: center;
padding: 1px 6px;
border-radius: 999px;
background: var(--gold-soft);
color: var(--gold-d);
}
.chip.is-active .chip-count {
background: rgba(255, 255, 255, 0.18);
color: var(--white);
}
.result-line {
margin: 0;
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
/* ---------- Gallery (masonry-ish) ---------- */
.gallery {
columns: 3 280px;
column-gap: 18px;
}
.tile {
position: relative;
break-inside: avoid;
margin-bottom: 18px;
border-radius: var(--r-md);
overflow: hidden;
cursor: pointer;
border: none;
padding: 0;
width: 100%;
display: block;
box-shadow: var(--shadow-sm);
background: var(--cream);
transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 0.28s;
animation: tile-in 0.4s ease both;
}
@keyframes tile-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.tile:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-md);
}
.tile:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 3px;
}
.tile-art {
width: 100%;
display: block;
position: relative;
}
.tile-art::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 40%, rgba(28, 24, 20, 0.72));
opacity: 0;
transition: opacity 0.28s;
}
.tile:hover .tile-art::after,
.tile:focus-visible .tile-art::after {
opacity: 1;
}
.tile-badge {
position: absolute;
top: 12px;
left: 12px;
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--white);
background: rgba(28, 24, 20, 0.4);
backdrop-filter: blur(6px);
padding: 5px 11px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.25);
z-index: 2;
}
.tile-cap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 16px;
z-index: 2;
text-align: left;
transform: translateY(8px);
opacity: 0;
transition: transform 0.28s, opacity 0.28s;
}
.tile:hover .tile-cap,
.tile:focus-visible .tile-cap {
transform: translateY(0);
opacity: 1;
}
.tile-service {
font-family: var(--serif);
font-size: 21px;
font-weight: 600;
color: var(--white);
line-height: 1.1;
}
.tile-stylist {
margin-top: 3px;
font-size: 12px;
letter-spacing: 0.04em;
color: var(--gold-soft);
}
/* ---------- Empty ---------- */
.empty {
text-align: center;
color: var(--muted);
font-size: 14px;
padding: 48px 0;
}
.link-btn {
background: none;
border: none;
color: var(--gold-d);
cursor: pointer;
font: inherit;
text-decoration: underline;
text-underline-offset: 3px;
padding: 0;
}
.link-btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
/* ---------- Footer ---------- */
.foot {
margin-top: 56px;
padding-top: 22px;
border-top: 1px solid var(--line);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
font-size: 12px;
letter-spacing: 0.06em;
color: var(--muted);
}
.foot-dot {
color: var(--gold);
}
/* ---------- Lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: clamp(16px, 4vw, 48px);
}
.lightbox[hidden] {
display: none;
}
.lightbox-backdrop {
position: absolute;
inset: 0;
background: rgba(20, 16, 12, 0.74);
backdrop-filter: blur(4px);
animation: fade-in 0.25s ease;
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.lightbox-panel {
position: relative;
z-index: 2;
width: min(760px, 100%);
background: var(--white);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-lg);
animation: pop-in 0.32s cubic-bezier(0.2, 0.7, 0.2, 1);
}
@keyframes pop-in {
from {
opacity: 0;
transform: translateY(14px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.lb-figure {
margin: 0;
}
.lb-stage {
width: 100%;
aspect-ratio: 16 / 11;
}
.lb-caption {
padding: 24px 28px 26px;
border-top: 1px solid var(--line);
}
.lb-cat {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.24em;
color: var(--gold-d);
}
.lb-service {
font-size: 32px;
font-weight: 600;
line-height: 1.05;
margin: 6px 0 2px;
}
.lb-stylist {
margin: 0;
color: var(--ink-2);
font-size: 14px;
}
.lb-stylist strong {
font-weight: 600;
}
.lb-actions {
margin-top: 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.lb-index {
font-size: 12px;
letter-spacing: 0.14em;
color: var(--muted);
}
.lb-close,
.lb-nav {
position: absolute;
z-index: 3;
border: none;
cursor: pointer;
background: rgba(255, 255, 255, 0.92);
color: var(--ink);
border-radius: 50%;
display: grid;
place-items: center;
box-shadow: var(--shadow-sm);
transition: background 0.2s, transform 0.16s, color 0.2s;
}
.lb-close:focus-visible,
.lb-nav:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.lb-close {
top: 14px;
right: 14px;
width: 38px;
height: 38px;
font-size: 24px;
line-height: 1;
}
.lb-close:hover {
background: var(--ink);
color: var(--white);
}
.lb-nav {
top: calc(50% - 60px);
width: 46px;
height: 46px;
font-size: 28px;
line-height: 1;
}
.lb-nav:hover {
background: var(--gold);
color: var(--white);
transform: scale(1.06);
}
.lb-prev {
left: 16px;
}
.lb-next {
right: 16px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 30px;
transform: translateX(-50%) translateY(20px);
z-index: 80;
background: var(--ink);
color: var(--white);
padding: 13px 22px;
border-radius: 999px;
font-size: 13.5px;
letter-spacing: 0.01em;
box-shadow: var(--shadow-md);
border: 1px solid rgba(176, 141, 87, 0.5);
display: flex;
align-items: center;
gap: 10px;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
}
.toast[hidden] {
display: none;
}
.toast.is-visible {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.toast::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--gold);
box-shadow: 0 0 0 3px rgba(176, 141, 87, 0.3);
}
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.gallery {
columns: 2 220px;
}
}
@media (max-width: 520px) {
.masthead {
justify-content: center;
text-align: center;
}
.masthead-nav {
gap: 16px;
}
.masthead-link:not(.is-active) {
display: none;
}
.hero {
padding: 36px 0 26px;
}
.gallery {
columns: 1;
}
.lb-service {
font-size: 26px;
}
.lb-nav {
top: auto;
bottom: 16px;
width: 42px;
height: 42px;
}
.lb-actions {
justify-content: flex-start;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
// ---- Data: realistic but fictional portfolio looks ----
var CAT_LABEL = {
cuts: "Cuts",
color: "Color",
updos: "Updos",
nails: "Nails",
};
var LOOKS = [
{ cat: "color", service: "Seamless Balayage", stylist: "Aria Vance", c1: "#caa27a", c2: "#7d5a3c" },
{ cat: "cuts", service: "Soft Curtain Bangs", stylist: "Noé Lambert", c1: "#e6cdb8", c2: "#b08d57" },
{ cat: "updos", service: "Low Chignon, Pearled", stylist: "Mireille Roux", c1: "#d9c3ae", c2: "#8a6d52" },
{ cat: "color", service: "Smoky Rosé Gloss", stylist: "Aria Vance", c1: "#e2b6a6", c2: "#a05f54" },
{ cat: "nails", service: "Almond French, Cream", stylist: "Juno Park", c1: "#f3e6dc", c2: "#c9a78f" },
{ cat: "cuts", service: "Textured Italian Bob", stylist: "Noé Lambert", c1: "#cbb59a", c2: "#6f5436" },
{ cat: "color", service: "Honey Money-Piece", stylist: "Selene Cho", c1: "#e9cf9a", c2: "#b0863f" },
{ cat: "updos", service: "Braided Crown Twist", stylist: "Mireille Roux", c1: "#d2bda7", c2: "#7a5f47" },
{ cat: "nails", service: "Chrome Mocha Ombré", stylist: "Juno Park", c1: "#cdb6a6", c2: "#8a6b56" },
{ cat: "cuts", service: "Sliced Shag Layers", stylist: "Selene Cho", c1: "#dcc6ac", c2: "#86663f" },
{ cat: "color", service: "Cool Ash Melt", stylist: "Aria Vance", c1: "#c9c0b4", c2: "#6c655c" },
{ cat: "updos", service: "Sculpted Side Sweep", stylist: "Mireille Roux", c1: "#e3d0bb", c2: "#9c7a55" },
{ cat: "nails", service: "Micro-Pearl Tips", stylist: "Juno Park", c1: "#f1e3d6", c2: "#caa27a" },
{ cat: "cuts", service: "Blunt Collarbone Cut", stylist: "Noé Lambert", c1: "#d7c1a8", c2: "#7e6040" },
{ cat: "color", service: "Copper Glaze Babylights", stylist: "Selene Cho", c1: "#e0a878", c2: "#9a532e" },
];
// Varied tile heights for the masonry feel.
var HEIGHTS = [320, 240, 280, 360, 260, 300, 340, 250, 290, 330, 270, 310, 245, 300, 350];
var gallery = document.getElementById("gallery");
var emptyEl = document.getElementById("empty");
var resultLine = document.getElementById("resultLine");
var chipsWrap = document.getElementById("chips");
var toastEl = document.getElementById("toast");
var lightbox = document.getElementById("lightbox");
var lbStage = document.getElementById("lbStage");
var lbCat = document.getElementById("lbCat");
var lbService = document.getElementById("lbService");
var lbStylist = document.getElementById("lbStylist");
var lbIndex = document.getElementById("lbIndex");
var lbBook = document.getElementById("lbBook");
var activeCat = "all";
var visible = []; // currently visible looks (filtered subset)
var current = 0; // index into `visible` shown in lightbox
var lastFocus = null;
var toastTimer = null;
// ---- Toast helper ----
function toast(msg) {
toastEl.textContent = msg;
toastEl.hidden = false;
// force reflow so the transition runs each time
void toastEl.offsetWidth;
toastEl.classList.add("is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
setTimeout(function () {
toastEl.hidden = true;
}, 320);
}, 2600);
}
function gradient(look) {
return "linear-gradient(150deg, " + look.c1 + ", " + look.c2 + ")";
}
// ---- Counts per category ----
function computeCounts() {
var counts = { all: LOOKS.length, cuts: 0, color: 0, updos: 0, nails: 0 };
LOOKS.forEach(function (l) {
counts[l.cat] += 1;
});
Object.keys(counts).forEach(function (key) {
var el = chipsWrap.querySelector('[data-count="' + key + '"]');
if (el) el.textContent = counts[key];
});
}
// ---- Render the grid for the active category ----
function render() {
visible = LOOKS.filter(function (l) {
return activeCat === "all" || l.cat === activeCat;
});
gallery.innerHTML = "";
visible.forEach(function (look, i) {
var globalIndex = LOOKS.indexOf(look);
var btn = document.createElement("button");
btn.type = "button";
btn.className = "tile";
btn.style.animationDelay = Math.min(i * 0.04, 0.4) + "s";
btn.setAttribute(
"aria-label",
look.service + " by " + look.stylist + ", " + CAT_LABEL[look.cat]
);
btn.dataset.index = String(i);
var art = document.createElement("span");
art.className = "tile-art";
art.style.height = HEIGHTS[globalIndex % HEIGHTS.length] + "px";
art.style.background = gradient(look);
var badge = document.createElement("span");
badge.className = "tile-badge";
badge.textContent = CAT_LABEL[look.cat];
var cap = document.createElement("span");
cap.className = "tile-cap";
cap.innerHTML =
'<span class="tile-service">' +
look.service +
'</span><span class="tile-stylist">' +
look.stylist +
"</span>";
btn.appendChild(art);
btn.appendChild(badge);
btn.appendChild(cap);
btn.addEventListener("click", function () {
openLightbox(i);
});
gallery.appendChild(btn);
});
// empty state + result line
var n = visible.length;
emptyEl.hidden = n !== 0;
gallery.hidden = n === 0;
if (activeCat === "all") {
resultLine.textContent = "Showing all " + n + " looks";
} else {
resultLine.textContent =
"Showing " + n + " " + CAT_LABEL[activeCat].toLowerCase() + " look" + (n === 1 ? "" : "s");
}
}
// ---- Chip filtering ----
chipsWrap.addEventListener("click", function (e) {
var chip = e.target.closest(".chip");
if (!chip) return;
setCategory(chip.dataset.cat);
});
function setCategory(cat) {
activeCat = cat;
Array.prototype.forEach.call(chipsWrap.querySelectorAll(".chip"), function (c) {
var on = c.dataset.cat === cat;
c.classList.toggle("is-active", on);
c.setAttribute("aria-selected", on ? "true" : "false");
});
render();
}
document.getElementById("resetBtn").addEventListener("click", function () {
setCategory("all");
});
// ---- Lightbox ----
function paintLightbox() {
var look = visible[current];
if (!look) return;
lbStage.style.background = gradient(look);
lbCat.textContent = CAT_LABEL[look.cat];
lbService.textContent = look.service;
lbStylist.textContent = look.stylist;
lbIndex.textContent = current + 1 + " / " + visible.length;
}
function openLightbox(i) {
current = i;
lastFocus = document.activeElement;
lightbox.hidden = false;
document.body.style.overflow = "hidden";
paintLightbox();
document.getElementById("lbClose").focus();
}
function closeLightbox() {
lightbox.hidden = true;
document.body.style.overflow = "";
if (lastFocus && typeof lastFocus.focus === "function") lastFocus.focus();
}
function step(dir) {
if (!visible.length) return;
current = (current + dir + visible.length) % visible.length;
paintLightbox();
}
document.getElementById("lbPrev").addEventListener("click", function () {
step(-1);
});
document.getElementById("lbNext").addEventListener("click", function () {
step(1);
});
document.getElementById("lbClose").addEventListener("click", closeLightbox);
Array.prototype.forEach.call(lightbox.querySelectorAll("[data-close]"), function (el) {
el.addEventListener("click", closeLightbox);
});
lbBook.addEventListener("click", function () {
var look = visible[current];
if (look) {
toast("Requested " + look.service + " with " + look.stylist + " — we'll confirm shortly.");
}
});
// ---- Keyboard ----
document.addEventListener("keydown", function (e) {
if (lightbox.hidden) return;
if (e.key === "Escape") closeLightbox();
else if (e.key === "ArrowLeft") step(-1);
else if (e.key === "ArrowRight") step(1);
});
// ---- Brand "Book a visit" ----
document.getElementById("brandBook").addEventListener("click", function () {
toast("Booking desk open — tell us the look you love.");
});
// ---- Init ----
computeCounts();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Portfolio · 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&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<div class="brand">
<span class="brand-mark" aria-hidden="true">ML</span>
<div class="brand-text">
<span class="brand-eyebrow">Maison Lumière Salon</span>
<span class="brand-name">Atelier</span>
</div>
</div>
<nav class="masthead-nav" aria-label="Sections">
<a href="#" class="masthead-link is-active">Portfolio</a>
<a href="#" class="masthead-link">Services</a>
<a href="#" class="masthead-link">Team</a>
<button class="btn btn-ghost" id="brandBook" type="button">Book a visit</button>
</nav>
</header>
<section class="hero">
<p class="eyebrow">The Lookbook</p>
<h1 class="hero-title">Portfolio & Gallery</h1>
<p class="hero-sub">
A curated record of recent work from our stylists — cuts, color, updos and
nails, captured in the studio light of Maison Lumière.
</p>
</section>
<section class="toolbar" aria-label="Filter portfolio">
<div class="chips" role="tablist" aria-label="Categories" id="chips">
<button class="chip is-active" role="tab" aria-selected="true" data-cat="all" type="button">
All <span class="chip-count" data-count="all">0</span>
</button>
<button class="chip" role="tab" aria-selected="false" data-cat="cuts" type="button">
Cuts <span class="chip-count" data-count="cuts">0</span>
</button>
<button class="chip" role="tab" aria-selected="false" data-cat="color" type="button">
Color <span class="chip-count" data-count="color">0</span>
</button>
<button class="chip" role="tab" aria-selected="false" data-cat="updos" type="button">
Updos <span class="chip-count" data-count="updos">0</span>
</button>
<button class="chip" role="tab" aria-selected="false" data-cat="nails" type="button">
Nails <span class="chip-count" data-count="nails">0</span>
</button>
</div>
<p class="result-line" id="resultLine" aria-live="polite">Showing all looks</p>
</section>
<main class="gallery" id="gallery" aria-label="Portfolio gallery">
<!-- tiles injected by script.js -->
</main>
<p class="empty" id="empty" hidden>
No looks in this category yet — <button class="link-btn" id="resetBtn" type="button">view all</button>.
</p>
<footer class="foot">
<span>Maison Lumière Salon · 14 Rue des Lys</span>
<span class="foot-dot" aria-hidden="true">·</span>
<span>Photography for portfolio use only</span>
</footer>
</div>
<!-- Lightbox -->
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Look detail" hidden>
<div class="lightbox-backdrop" data-close></div>
<div class="lightbox-panel" role="document">
<button class="lb-close" id="lbClose" type="button" aria-label="Close">×</button>
<button class="lb-nav lb-prev" id="lbPrev" type="button" aria-label="Previous look">‹</button>
<button class="lb-nav lb-next" id="lbNext" type="button" aria-label="Next look">›</button>
<figure class="lb-figure">
<div class="lb-stage" id="lbStage" aria-hidden="true"></div>
<figcaption class="lb-caption">
<span class="lb-cat" id="lbCat">Color</span>
<h2 class="lb-service" id="lbService">Seamless Balayage</h2>
<p class="lb-stylist">with <strong id="lbStylist">Aria Vance</strong></p>
<div class="lb-actions">
<button class="btn btn-gold" id="lbBook" type="button">Book this look</button>
<span class="lb-index" id="lbIndex">1 / 12</span>
</div>
</figcaption>
</figure>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>Portfolio / Gallery
An editorial lookbook for the fictional Maison Lumière Salon, built to feel like flipping through a printed atelier portfolio. A serif display masthead and centered hero set the tone, and the work itself fills a masonry-style CSS grid of softly graded tiles in the house palette of rose-gold, cream and matte black. Each tile carries a small-caps category badge and, on hover, lifts gently to reveal a gold-flecked caption naming the service and the stylist behind it.
Pill-style category chips — All, Cuts, Color, Updos and Nails — carry live counts and filter the grid in place, with the active chip flipping to a matte-black pill while a running result line stays in sync. Tap a look and a polished lightbox glides open: a full-bleed gradient stage, the service and stylist caption, an index counter, and previous / next navigation that loops through the current selection. Arrow keys move between looks, Escape closes, and focus returns cleanly to where it left.
A Book this look action in the lightbox and a Book a visit button in the masthead each surface an elegant toast confirmation. The whole screen is vanilla HTML, CSS and JavaScript — no frameworks — keyboard-usable with visible focus rings, respectful of reduced-motion preferences, and reflowing from three columns to a single tasteful stack down to roughly 360px.