Salon — Nail Bar / Studio Landing
A trendy, Instagrammable marketing landing for the fictional Polish and Petals nail studio, dressed in a blush, lavender, and chrome palette with a friendly rounded Poppins type system. It pairs a blurred sticky nav and accessible mobile menu with a bubbly hero of floating blob shapes, count-up stats, and an inline quick-booking card whose total updates live. Below sit a tappable services grid, a filterable gradient nail-art gallery with likeable tiles, a copyable offers band, a stylist strip, and playful toast confirmations.
MCP
程式碼
:root {
/* Palette override — Blush + lavender + chrome */
--blush: #f7c8d6;
--blush-d: #e58aa6;
--lav: #cdbdf0;
--lav-d: #a98fe0;
--chrome: #d8dde6;
--ink: #2a2233;
--ink-2: #4a3f57;
--muted: #8a8095;
--bg: #fdf6f9;
--white: #ffffff;
--line: rgba(42, 34, 51, 0.1);
--line-2: rgba(42, 34, 51, 0.16);
--ok: #5f8a6b;
--sans: "Poppins", "Inter", system-ui, -apple-system, sans-serif;
--r-sm: 14px;
--r-md: 22px;
--r-lg: 34px;
--r-pill: 999px;
--sh-sm: 0 4px 16px rgba(42, 34, 51, 0.07);
--sh-md: 0 14px 40px rgba(165, 120, 170, 0.16);
--sh-lg: 0 28px 70px rgba(165, 120, 170, 0.22);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--sans);
font-weight: 400;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
h1, h2, h3 {
margin: 0;
font-weight: 700;
line-height: 1.08;
letter-spacing: -0.02em;
}
p {
margin: 0;
}
a {
color: inherit;
text-decoration: none;
}
button {
font-family: inherit;
cursor: pointer;
}
.wrap {
width: min(1140px, 92vw);
margin-inline: auto;
}
.skip-link {
position: absolute;
left: -999px;
top: 8px;
z-index: 200;
background: var(--ink);
color: #fff;
padding: 10px 16px;
border-radius: var(--r-pill);
}
.skip-link:focus {
left: 16px;
}
.eyebrow {
display: inline-block;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--blush-d);
margin-bottom: 0.7rem;
}
.eyebrow--light {
color: rgba(255, 255, 255, 0.9);
}
/* ============ BUTTONS ============ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border: 0;
border-radius: var(--r-pill);
padding: 0.7rem 1.35rem;
font-weight: 600;
font-size: 0.95rem;
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
white-space: nowrap;
}
.btn:active {
transform: translateY(1px) scale(0.99);
}
.btn--solid {
background: linear-gradient(135deg, var(--blush-d), var(--lav-d));
color: #fff;
box-shadow: 0 8px 22px rgba(229, 138, 166, 0.4);
}
.btn--solid:hover {
transform: translateY(-2px);
box-shadow: 0 14px 30px rgba(169, 143, 224, 0.45);
}
.btn--ghost {
background: var(--white);
color: var(--ink);
border: 1.5px solid var(--line-2);
}
.btn--ghost:hover {
border-color: var(--lav-d);
color: var(--lav-d);
transform: translateY(-2px);
}
.btn--lg {
padding: 0.92rem 1.7rem;
font-size: 1rem;
}
.btn:focus-visible {
outline: 3px solid var(--lav-d);
outline-offset: 2px;
}
/* ============ NAV ============ */
.nav {
position: sticky;
top: 0;
z-index: 100;
background: rgba(253, 246, 249, 0.78);
backdrop-filter: saturate(160%) blur(14px);
transition: box-shadow 0.25s ease, border-color 0.25s ease;
border-bottom: 1px solid transparent;
}
.nav.is-stuck {
box-shadow: var(--sh-sm);
border-bottom-color: var(--line);
}
.nav__inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 72px;
}
.brand {
display: flex;
align-items: center;
gap: 0.6rem;
font-weight: 800;
font-size: 1.18rem;
letter-spacing: -0.02em;
}
.brand__name em {
font-style: normal;
color: var(--blush-d);
}
.brand__dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(135deg, var(--blush), var(--lav));
box-shadow: 0 0 0 4px rgba(205, 189, 240, 0.3);
}
.nav__links {
display: flex;
align-items: center;
gap: 1.7rem;
}
.nav__links a[data-link] {
font-weight: 500;
font-size: 0.96rem;
color: var(--ink-2);
position: relative;
transition: color 0.18s;
}
.nav__links a[data-link]::after {
content: "";
position: absolute;
left: 0;
bottom: -6px;
width: 0;
height: 2px;
border-radius: 2px;
background: linear-gradient(90deg, var(--blush-d), var(--lav-d));
transition: width 0.22s ease;
}
.nav__links a[data-link]:hover,
.nav__links a[data-link].is-active {
color: var(--ink);
}
.nav__links a[data-link]:hover::after,
.nav__links a[data-link].is-active::after {
width: 100%;
}
.nav__toggle {
display: none;
flex-direction: column;
gap: 5px;
background: var(--white);
border: 1.5px solid var(--line-2);
border-radius: 12px;
padding: 11px 10px;
}
.nav__toggle span {
width: 22px;
height: 2.4px;
border-radius: 2px;
background: var(--ink);
transition: transform 0.25s ease, opacity 0.2s ease;
}
.nav__toggle[aria-expanded="true"] span:nth-child(1) {
transform: translateY(7.4px) rotate(45deg);
}
.nav__toggle[aria-expanded="true"] span:nth-child(2) {
opacity: 0;
}
.nav__toggle[aria-expanded="true"] span:nth-child(3) {
transform: translateY(-7.4px) rotate(-45deg);
}
/* ============ HERO ============ */
.hero {
position: relative;
padding: 4.5rem 0 5.5rem;
overflow: hidden;
}
.hero__grid {
position: relative;
z-index: 2;
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 3rem;
align-items: center;
}
.hero__copy {
max-width: 36ch;
}
.hero__title {
font-size: clamp(2.6rem, 6vw, 4.4rem);
margin: 0.2rem 0 1.1rem;
}
.grad {
background: linear-gradient(120deg, var(--blush-d), var(--lav-d) 70%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.hero__lead {
font-size: 1.1rem;
color: var(--ink-2);
margin-bottom: 1.8rem;
}
.hero__cta {
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
}
.hero__stats {
list-style: none;
margin: 2.4rem 0 0;
padding: 0;
display: flex;
gap: 2.2rem;
}
.hero__stats b {
display: block;
font-size: 1.9rem;
font-weight: 800;
color: var(--ink);
letter-spacing: -0.03em;
}
.hero__stats small {
font-size: 0.78rem;
color: var(--muted);
font-weight: 500;
}
/* Blobs */
.blob {
position: absolute;
border-radius: 48% 52% 60% 40% / 55% 45% 55% 45%;
filter: blur(8px);
opacity: 0.85;
z-index: 1;
animation: float 9s ease-in-out infinite;
}
.blob--1 {
width: 340px;
height: 340px;
background: radial-gradient(circle at 30% 30%, var(--blush), transparent 70%);
top: -90px;
right: -60px;
}
.blob--2 {
width: 300px;
height: 300px;
background: radial-gradient(circle at 40% 40%, var(--lav), transparent 70%);
bottom: -120px;
left: -90px;
animation-delay: -3s;
}
.blob--3 {
width: 180px;
height: 180px;
background: radial-gradient(circle at 50% 50%, var(--chrome), transparent 72%);
top: 40%;
left: 44%;
animation-delay: -5s;
opacity: 0.6;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) rotate(0deg); }
50% { transform: translate(14px, -22px) rotate(8deg); }
}
/* Booking card */
.bookcard {
position: relative;
z-index: 2;
background: var(--white);
border-radius: var(--r-lg);
padding: 1.8rem;
box-shadow: var(--sh-md);
border: 1px solid var(--line);
}
.bookcard__title {
font-size: 1.4rem;
}
.bookcard__sub {
color: var(--muted);
font-size: 0.9rem;
margin: 0.3rem 0 1.3rem;
}
.field {
display: block;
margin-bottom: 1rem;
}
.field > span {
display: block;
font-size: 0.78rem;
font-weight: 600;
color: var(--ink-2);
margin-bottom: 0.4rem;
}
.field input,
.field select {
width: 100%;
font-family: inherit;
font-size: 0.95rem;
color: var(--ink);
background: var(--bg);
border: 1.5px solid var(--line);
border-radius: var(--r-sm);
padding: 0.7rem 0.85rem;
transition: border-color 0.18s, box-shadow 0.18s;
}
.field input:focus,
.field select:focus {
outline: none;
border-color: var(--lav-d);
box-shadow: 0 0 0 4px rgba(205, 189, 240, 0.3);
}
.field.is-invalid input,
.field.is-invalid select {
border-color: var(--blush-d);
box-shadow: 0 0 0 4px rgba(229, 138, 166, 0.22);
}
.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8rem;
}
.bookcard__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: 0.5rem;
}
.bookcard__price {
font-size: 0.82rem;
color: var(--muted);
font-weight: 500;
}
.bookcard__price b {
display: block;
font-size: 1.3rem;
color: var(--ink);
font-weight: 800;
}
/* ============ SECTIONS ============ */
.section {
padding: 5rem 0;
}
.section--tint {
background: linear-gradient(180deg, rgba(247, 200, 214, 0.16), rgba(205, 189, 240, 0.14));
}
.sec-head {
text-align: center;
max-width: 44ch;
margin: 0 auto 2.8rem;
}
.sec-title {
font-size: clamp(1.8rem, 4vw, 2.7rem);
}
.sec-sub {
color: var(--muted);
margin-top: 0.7rem;
font-size: 1rem;
}
.grid {
display: grid;
gap: 1.2rem;
}
.grid--svc {
grid-template-columns: repeat(3, 1fr);
}
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--sh-sm);
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: var(--sh-md);
border-color: rgba(169, 143, 224, 0.4);
}
/* Service card */
.svc {
text-align: left;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.svc__emoji {
font-size: 1.8rem;
width: 56px;
height: 56px;
display: grid;
place-items: center;
border-radius: 18px;
background: linear-gradient(135deg, rgba(247, 200, 214, 0.5), rgba(205, 189, 240, 0.5));
margin-bottom: 0.5rem;
}
.svc h3 {
font-size: 1.18rem;
}
.svc p {
color: var(--ink-2);
font-size: 0.92rem;
flex: 1;
}
.svc__price {
font-weight: 700;
font-size: 0.9rem;
color: var(--blush-d);
margin-top: 0.5rem;
}
.svc.is-picked {
border-color: var(--lav-d);
box-shadow: 0 0 0 3px rgba(205, 189, 240, 0.45), var(--sh-md);
}
/* ============ GALLERY ============ */
.filters {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
justify-content: center;
margin-bottom: 2rem;
}
.chip {
border: 1.5px solid var(--line-2);
background: var(--white);
color: var(--ink-2);
border-radius: var(--r-pill);
padding: 0.5rem 1.1rem;
font-weight: 600;
font-size: 0.88rem;
transition: all 0.18s ease;
}
.chip:hover {
border-color: var(--lav-d);
color: var(--lav-d);
}
.chip.is-active {
background: linear-gradient(135deg, var(--blush-d), var(--lav-d));
border-color: transparent;
color: #fff;
}
.gallery {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.tile {
position: relative;
aspect-ratio: 1;
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--sh-sm);
transition: transform 0.25s ease, box-shadow 0.25s ease, opacity 0.3s ease;
cursor: pointer;
border: 0;
}
.tile:hover {
transform: translateY(-4px) rotate(-1deg);
box-shadow: var(--sh-md);
}
.tile.is-hidden {
display: none;
}
.tile__meta {
position: absolute;
inset: auto 0 0 0;
padding: 0.7rem 0.9rem;
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(transparent, rgba(42, 34, 51, 0.55));
color: #fff;
}
.tile__name {
font-weight: 600;
font-size: 0.82rem;
}
.tile__like {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.8rem;
font-weight: 600;
}
.tile.is-liked .tile__like {
color: var(--blush);
}
.tile__like .heart {
font-size: 0.95rem;
transition: transform 0.2s;
}
.tile.is-liked .tile__like .heart {
transform: scale(1.3);
}
/* ============ OFFERS ============ */
.offers {
padding: 3.5rem 0;
}
.offers__inner {
background: linear-gradient(120deg, var(--blush-d), var(--lav-d));
border-radius: var(--r-lg);
padding: 2.6rem 2.8rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
flex-wrap: wrap;
color: #fff;
box-shadow: var(--sh-md);
}
.offers__title {
font-size: clamp(1.6rem, 3.4vw, 2.3rem);
margin: 0.3rem 0 0.7rem;
}
.offers__copy {
max-width: 46ch;
}
.offers__copy p {
color: rgba(255, 255, 255, 0.92);
}
.offers__codebox {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.6rem;
background: rgba(255, 255, 255, 0.16);
border: 1px dashed rgba(255, 255, 255, 0.55);
border-radius: var(--r-md);
padding: 1.4rem 1.6rem;
}
.offers__label {
font-size: 0.74rem;
letter-spacing: 0.14em;
text-transform: uppercase;
font-weight: 700;
opacity: 0.85;
}
.offers__code {
font-size: 1.7rem;
font-weight: 800;
letter-spacing: 0.06em;
font-family: var(--sans);
}
.offers__codebox .btn--solid {
background: #fff;
color: var(--ink);
box-shadow: none;
}
/* ============ TEAM ============ */
.team {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.2rem;
}
.artist {
padding: 1.6rem 1.4rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
}
.artist__avatar {
width: 76px;
height: 76px;
border-radius: 50%;
display: grid;
place-items: center;
background: var(--av, linear-gradient(135deg, var(--blush), var(--lav)));
color: #fff;
font-weight: 800;
font-size: 1.4rem;
letter-spacing: 0.02em;
margin-bottom: 0.6rem;
box-shadow: 0 6px 18px rgba(169, 143, 224, 0.35);
}
.artist h3 {
font-size: 1.1rem;
}
.artist p {
color: var(--muted);
font-size: 0.86rem;
}
.artist.is-picked {
border-color: var(--lav-d);
box-shadow: 0 0 0 3px rgba(205, 189, 240, 0.45), var(--sh-md);
}
/* ============ FOOTER ============ */
.footer {
background: var(--ink);
color: #fff;
padding: 3rem 0 2rem;
}
.footer__inner {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 1.6rem;
align-items: start;
}
.footer__brand {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.footer .brand__name {
font-size: 1.3rem;
display: inline-flex;
align-items: center;
}
.footer .brand__name em {
color: var(--blush);
}
.footer__brand p {
color: rgba(255, 255, 255, 0.6);
font-size: 0.86rem;
}
.footer__links {
display: flex;
gap: 1.4rem;
flex-wrap: wrap;
justify-content: flex-end;
}
.footer__links a {
color: rgba(255, 255, 255, 0.8);
font-size: 0.92rem;
font-weight: 500;
}
.footer__links a:hover {
color: var(--blush);
}
.footer__fine {
grid-column: 1 / -1;
margin-top: 1.6rem;
padding-top: 1.4rem;
border-top: 1px solid rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.5);
font-size: 0.8rem;
}
/* ============ TOASTS ============ */
.toast-wrap {
position: fixed;
bottom: 1.4rem;
left: 50%;
transform: translateX(-50%);
z-index: 300;
display: flex;
flex-direction: column;
gap: 0.6rem;
width: max-content;
max-width: 90vw;
}
.toast {
background: var(--ink);
color: #fff;
padding: 0.85rem 1.3rem;
border-radius: var(--r-pill);
box-shadow: var(--sh-lg);
font-size: 0.92rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.6rem;
animation: toast-in 0.3s cubic-bezier(0.2, 0.9, 0.3, 1.3);
}
.toast.is-out {
animation: toast-out 0.3s ease forwards;
}
.toast .dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--blush);
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(14px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toast-out {
to { opacity: 0; transform: translateY(14px) scale(0.95); }
}
/* ============ REVEAL ============ */
.reveal {
opacity: 0;
transform: translateY(28px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.is-in {
opacity: 1;
transform: none;
}
/* ============ RESPONSIVE ============ */
@media (max-width: 920px) {
.hero__grid {
grid-template-columns: 1fr;
gap: 2.4rem;
}
.grid--svc,
.gallery,
.team {
grid-template-columns: repeat(2, 1fr);
}
.footer__inner {
grid-template-columns: 1fr;
}
.footer__links {
justify-content: flex-start;
}
}
@media (max-width: 520px) {
.nav__links {
position: absolute;
top: 72px;
left: 0;
right: 0;
flex-direction: column;
align-items: stretch;
gap: 0.4rem;
padding: 1rem 4vw 1.4rem;
background: rgba(253, 246, 249, 0.97);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--line);
box-shadow: var(--sh-md);
transform: translateY(-12px);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.nav__links.is-open {
transform: none;
opacity: 1;
pointer-events: auto;
}
.nav__links a[data-link] {
padding: 0.7rem 0.4rem;
border-bottom: 1px solid var(--line);
}
.nav__links a[data-link]::after {
display: none;
}
.nav__cta {
margin-top: 0.4rem;
text-align: center;
}
.nav__toggle {
display: flex;
}
.hero {
padding: 2.6rem 0 3.4rem;
}
.hero__stats {
gap: 1.4rem;
}
.hero__stats b {
font-size: 1.5rem;
}
.grid--svc,
.gallery,
.team {
grid-template-columns: 1fr;
}
.gallery {
grid-template-columns: 1fr 1fr;
}
.offers__inner {
padding: 1.8rem;
}
.field-row {
grid-template-columns: 1fr;
}
.section {
padding: 3.4rem 0;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
scroll-behavior: auto !important;
}
.reveal {
opacity: 1;
transform: none;
}
}(function () {
"use strict";
const $ = (sel, ctx) => (ctx || document).querySelector(sel);
const $$ = (sel, ctx) => Array.from((ctx || document).querySelectorAll(sel));
/* ---------- Toast helper ---------- */
const toastWrap = $("#toasts");
function toast(msg, ms = 2600) {
const el = document.createElement("div");
el.className = "toast";
el.setAttribute("role", "status");
el.innerHTML = '<span class="dot" aria-hidden="true"></span>';
el.append(msg);
toastWrap.appendChild(el);
setTimeout(() => {
el.classList.add("is-out");
el.addEventListener("animationend", () => el.remove(), { once: true });
}, ms);
}
/* ---------- Sticky nav shadow ---------- */
const nav = $("#nav");
const onScroll = () => nav.classList.toggle("is-stuck", window.scrollY > 8);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
/* ---------- Mobile nav toggle ---------- */
const navToggle = $("#navToggle");
const navLinks = $("#navLinks");
function closeNav() {
navLinks.classList.remove("is-open");
navToggle.setAttribute("aria-expanded", "false");
navToggle.setAttribute("aria-label", "Open menu");
}
navToggle.addEventListener("click", () => {
const open = navLinks.classList.toggle("is-open");
navToggle.setAttribute("aria-expanded", String(open));
navToggle.setAttribute("aria-label", open ? "Close menu" : "Open menu");
});
navLinks.addEventListener("click", (e) => {
if (e.target.closest("a")) closeNav();
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeNav();
});
/* ---------- Active link on scroll ---------- */
const sections = ["services", "gallery", "offers", "team"]
.map((id) => document.getElementById(id))
.filter(Boolean);
const linkFor = {};
$$("#navLinks a[data-link]").forEach((a) => {
const id = a.getAttribute("href").slice(1);
linkFor[id] = a;
});
const spy = new IntersectionObserver(
(entries) => {
entries.forEach((en) => {
if (en.isIntersecting) {
Object.values(linkFor).forEach((a) => a.classList.remove("is-active"));
const a = linkFor[en.target.id];
if (a) a.classList.add("is-active");
}
});
},
{ rootMargin: "-45% 0px -50% 0px" }
);
sections.forEach((s) => spy.observe(s));
/* ---------- Reveal on scroll ---------- */
const revealer = new IntersectionObserver(
(entries, obs) => {
entries.forEach((en) => {
if (en.isIntersecting) {
en.target.classList.add("is-in");
obs.unobserve(en.target);
}
});
},
{ threshold: 0.12 }
);
$$(".reveal").forEach((el) => revealer.observe(el));
/* ---------- Animated stat counters ---------- */
let countersRun = false;
const statsEl = $("#stats");
function runCounters() {
if (countersRun) return;
countersRun = true;
$$("[data-count]", statsEl).forEach((el) => {
const target = parseFloat(el.dataset.count);
const decimals = parseInt(el.dataset.decimals || "0", 10);
const dur = 1400;
const start = performance.now();
function tick(now) {
const p = Math.min((now - start) / dur, 1);
const eased = 1 - Math.pow(1 - p, 3);
const val = target * eased;
el.textContent =
decimals > 0
? val.toFixed(decimals)
: Math.round(val).toLocaleString("en-US");
if (p < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
});
}
const statSpy = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
runCounters();
statSpy.disconnect();
}
},
{ threshold: 0.4 }
);
if (statsEl) statSpy.observe(statsEl);
/* ---------- Booking form ---------- */
const form = $("#bookForm");
const fService = $("#bService");
const fDate = $("#bDate");
const fName = $("#bName");
const bTotal = $("#bTotal");
// sensible min date = today
const today = new Date();
fDate.min = today.toISOString().split("T")[0];
function currentPrice() {
const opt = fService.options[fService.selectedIndex];
return opt && opt.dataset.price ? parseInt(opt.dataset.price, 10) : 0;
}
function updateTotal() {
bTotal.textContent = "$" + currentPrice();
}
fService.addEventListener("change", () => {
updateTotal();
field(fService).classList.remove("is-invalid");
});
function field(input) {
return input.closest(".field");
}
[fName, fDate].forEach((inp) =>
inp.addEventListener("input", () => field(inp).classList.remove("is-invalid"))
);
// Pick a service from a card / select option
function selectService(name) {
const opt = $$("#bService option").find((o) => o.value === name);
if (opt) {
fService.value = name;
updateTotal();
}
$$(".svc").forEach((c) =>
c.classList.toggle("is-picked", c.dataset.service === name)
);
}
$("#svcGrid").addEventListener("click", (e) => {
const card = e.target.closest(".svc");
if (!card) return;
selectService(card.dataset.service);
toast(card.dataset.emoji + " " + card.dataset.service + " selected");
$("#book").scrollIntoView({ behavior: "smooth", block: "start" });
fName.focus({ preventScroll: true });
});
// Artist picker
let pickedArtist = null;
$("#team").addEventListener("click", (e) => {
const card = e.target.closest(".artist");
if (!card) return;
const name = card.dataset.artist;
const same = pickedArtist === name;
pickedArtist = same ? null : name;
$$(".artist").forEach((c) =>
c.classList.toggle("is-picked", !same && c.dataset.artist === name)
);
toast(same ? "Artist cleared" : "Requested " + name + " ✦");
if (!same) $("#book").scrollIntoView({ behavior: "smooth", block: "start" });
});
form.addEventListener("submit", (e) => {
e.preventDefault();
let ok = true;
if (!fName.value.trim()) {
field(fName).classList.add("is-invalid");
ok = false;
}
if (!fService.value) {
field(fService).classList.add("is-invalid");
ok = false;
}
if (!fDate.value) {
field(fDate).classList.add("is-invalid");
ok = false;
}
if (!ok) {
toast("Pop in your name, service & date 💕");
return;
}
const when = new Date(fDate.value + "T00:00:00").toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
});
const artist = pickedArtist ? " with " + pickedArtist : "";
toast(
"Booked! " + fService.value + artist + " on " + when + " — $" + currentPrice()
);
form.reset();
pickedArtist = null;
$$(".svc, .artist").forEach((c) => c.classList.remove("is-picked"));
updateTotal();
fDate.min = today.toISOString().split("T")[0];
});
/* ---------- Gallery ---------- */
const looks = [
{ name: "Liquid Chrome", cat: "chrome", g: "linear-gradient(135deg,#d8dde6,#a98fe0,#e58aa6)" },
{ name: "Sunset Ombré", cat: "ombre", g: "linear-gradient(160deg,#f7c8d6,#e58aa6,#cdbdf0)" },
{ name: "Petal Bloom", cat: "floral", g: "radial-gradient(circle at 30% 30%,#f7c8d6,#cdbdf0)" },
{ name: "Milk Bath", cat: "minimal", g: "linear-gradient(135deg,#fdf6f9,#d8dde6)" },
{ name: "Mirror Mauve", cat: "chrome", g: "linear-gradient(120deg,#cdbdf0,#d8dde6,#a98fe0)" },
{ name: "Peach Fade", cat: "ombre", g: "linear-gradient(170deg,#f7c8d6,#fdf6f9)" },
{ name: "Wildflower", cat: "floral", g: "radial-gradient(circle at 70% 20%,#cdbdf0,#f7c8d6,#e58aa6)" },
{ name: "Glass Nude", cat: "minimal", g: "linear-gradient(135deg,#f7c8d6,#fdf6f9,#d8dde6)" },
];
const galleryEl = $("#gallery-grid");
const likeState = {};
function renderGallery() {
galleryEl.innerHTML = "";
looks.forEach((look, i) => {
const likes = (likeState[i] = likeState[i] ?? 40 + ((i * 17) % 60));
const liked = likeState["liked_" + i] || false;
const btn = document.createElement("button");
btn.className = "tile" + (liked ? " is-liked" : "");
btn.dataset.cat = look.cat;
btn.style.background = look.g;
btn.setAttribute("aria-label", "Like " + look.name + " design");
btn.innerHTML =
'<span class="tile__meta">' +
'<span class="tile__name">' + look.name + "</span>" +
'<span class="tile__like"><span class="heart" aria-hidden="true">' +
(liked ? "♥" : "♡") +
'</span><span class="cnt">' + likes + "</span></span>" +
"</span>";
btn.addEventListener("click", () => {
const now = !likeState["liked_" + i];
likeState["liked_" + i] = now;
likeState[i] += now ? 1 : -1;
btn.classList.toggle("is-liked", now);
$(".heart", btn).textContent = now ? "♥" : "♡";
$(".cnt", btn).textContent = likeState[i];
});
galleryEl.appendChild(btn);
});
applyFilter(activeFilter);
}
let activeFilter = "all";
function applyFilter(cat) {
activeFilter = cat;
$$(".tile", galleryEl).forEach((t) => {
t.classList.toggle("is-hidden", cat !== "all" && t.dataset.cat !== cat);
});
}
$("#filters").addEventListener("click", (e) => {
const chip = e.target.closest(".chip");
if (!chip) return;
$$(".chip").forEach((c) => {
c.classList.remove("is-active");
c.setAttribute("aria-selected", "false");
});
chip.classList.add("is-active");
chip.setAttribute("aria-selected", "true");
applyFilter(chip.dataset.filter);
});
renderGallery();
/* ---------- Copy promo code ---------- */
const copyBtn = $("#copyCode");
copyBtn.addEventListener("click", async () => {
const code = $("#promoCode").textContent.trim();
try {
await navigator.clipboard.writeText(code);
} catch (_) {
const r = document.createRange();
r.selectNode($("#promoCode"));
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(r);
document.execCommand("copy");
sel.removeAllRanges();
}
toast("Code " + code + " copied ✨");
});
/* ---------- Book buttons ---------- */
$$("[data-book]").forEach((b) =>
b.addEventListener("click", () => {
closeNav();
setTimeout(() => fName.focus({ preventScroll: true }), 400);
})
);
updateTotal();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Nail Bar · Polish & Petals Studio</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=Poppins:wght@400;500;600;700;800&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>
<a class="skip-link" href="#main">Skip to content</a>
<!-- ============ NAV ============ -->
<header class="nav" id="nav">
<div class="wrap nav__inner">
<a class="brand" href="#top" aria-label="Polish and Petals home">
<span class="brand__dot" aria-hidden="true"></span>
<span class="brand__name">Polish<em>&</em>Petals</span>
</a>
<nav class="nav__links" id="navLinks" aria-label="Primary">
<a href="#services" data-link>Services</a>
<a href="#gallery" data-link>Nail Art</a>
<a href="#offers" data-link>Offers</a>
<a href="#team" data-link>Artists</a>
<a class="btn btn--solid nav__cta" href="#book" data-book>Book now</a>
</nav>
<button
class="nav__toggle"
id="navToggle"
aria-expanded="false"
aria-controls="navLinks"
aria-label="Open menu"
>
<span></span><span></span><span></span>
</button>
</div>
</header>
<main id="main">
<span id="top"></span>
<!-- ============ HERO ============ -->
<section class="hero reveal" id="book">
<span class="blob blob--1" aria-hidden="true"></span>
<span class="blob blob--2" aria-hidden="true"></span>
<span class="blob blob--3" aria-hidden="true"></span>
<div class="wrap hero__grid">
<div class="hero__copy">
<span class="eyebrow">Blush · Lavender · Chrome</span>
<h1 class="hero__title">
Tips & toes,<br /><span class="grad">perfected.</span>
</h1>
<p class="hero__lead">
A trendy little nail bar in the heart of the city. Gel, acrylic,
chrome and hand-painted art — booked in seconds, polished to
perfection.
</p>
<div class="hero__cta">
<a class="btn btn--solid btn--lg" href="#book" data-book>Book your set</a>
<a class="btn btn--ghost btn--lg" href="#gallery">See the art</a>
</div>
<ul class="hero__stats" id="stats" aria-label="Studio highlights">
<li><b data-count="14200">0</b><small>Sets painted</small></li>
<li><b data-count="38">0</b><small>Art designs</small></li>
<li><b data-count="4.9" data-decimals="1">0</b><small>Studio rating</small></li>
</ul>
</div>
<!-- Quick booking card -->
<form class="bookcard" id="bookForm" novalidate aria-label="Quick booking">
<h2 class="bookcard__title">Grab a slot</h2>
<p class="bookcard__sub">Pick a service & we'll do the rest.</p>
<label class="field">
<span>Your name</span>
<input type="text" name="name" id="bName" placeholder="e.g. Mila Reyes" required />
</label>
<div class="field-row">
<label class="field">
<span>Service</span>
<select name="service" id="bService" required>
<option value="" selected disabled>Choose…</option>
<option value="Classic Manicure" data-price="32">Classic Manicure · $32</option>
<option value="Gel Manicure" data-price="48">Gel Manicure · $48</option>
<option value="Acrylic Full Set" data-price="65">Acrylic Full Set · $65</option>
<option value="Nail Art" data-price="58">Nail Art · $58</option>
<option value="Spa Pedicure" data-price="52">Spa Pedicure · $52</option>
</select>
</label>
<label class="field">
<span>Date</span>
<input type="date" name="date" id="bDate" required />
</label>
</div>
<div class="bookcard__foot">
<span class="bookcard__price">
Est. total <b id="bTotal">$0</b>
</span>
<button class="btn btn--solid" type="submit">Reserve</button>
</div>
</form>
</div>
</section>
<!-- ============ SERVICES ============ -->
<section class="section reveal" id="services">
<div class="wrap">
<header class="sec-head">
<span class="eyebrow">The menu</span>
<h2 class="sec-title">Services we obsess over</h2>
<p class="sec-sub">Tap a card to start a booking — totals update live.</p>
</header>
<div class="grid grid--svc" id="svcGrid">
<button class="card svc" data-service="Classic Manicure" data-price="32" data-emoji="💅">
<span class="svc__emoji" aria-hidden="true">💅</span>
<h3>Classic Manicure</h3>
<p>Shape, cuticle care, buff & a flawless coat in any shade.</p>
<span class="svc__price">$32 · 30 min</span>
</button>
<button class="card svc" data-service="Gel Manicure" data-price="48" data-emoji="✨">
<span class="svc__emoji" aria-hidden="true">✨</span>
<h3>Gel Manicure</h3>
<p>Chip-resistant, mirror-shine gel that lasts a glorious 3 weeks.</p>
<span class="svc__price">$48 · 45 min</span>
</button>
<button class="card svc" data-service="Acrylic Full Set" data-price="65" data-emoji="🦋">
<span class="svc__emoji" aria-hidden="true">🦋</span>
<h3>Acrylic Full Set</h3>
<p>Sculpted length & shape, built to your dream silhouette.</p>
<span class="svc__price">$65 · 75 min</span>
</button>
<button class="card svc" data-service="Nail Art" data-price="58" data-emoji="🎨">
<span class="svc__emoji" aria-hidden="true">🎨</span>
<h3>Nail Art</h3>
<p>Chrome, ombré, French twists & freehand minis by request.</p>
<span class="svc__price">$58 · 60 min</span>
</button>
<button class="card svc" data-service="Spa Pedicure" data-price="52" data-emoji="🌸">
<span class="svc__emoji" aria-hidden="true">🌸</span>
<h3>Spa Pedicure</h3>
<p>Soak, scrub, massage & polish — toes that deserve the spotlight.</p>
<span class="svc__price">$52 · 50 min</span>
</button>
<button class="card svc" data-service="Chrome Add-On" data-price="14" data-emoji="🪩">
<span class="svc__emoji" aria-hidden="true">🪩</span>
<h3>Chrome Add-On</h3>
<p>That liquid-metal finish everyone's saving — on any base set.</p>
<span class="svc__price">$14 · 15 min</span>
</button>
</div>
</div>
</section>
<!-- ============ GALLERY ============ -->
<section class="section section--tint reveal" id="gallery">
<div class="wrap">
<header class="sec-head">
<span class="eyebrow">Fresh from the chair</span>
<h2 class="sec-title">Nail art that breaks the feed</h2>
<p class="sec-sub">Filter the looks, then tap to like.</p>
</header>
<div class="filters" id="filters" role="tablist" aria-label="Filter nail art">
<button class="chip is-active" data-filter="all" role="tab" aria-selected="true">All</button>
<button class="chip" data-filter="chrome" role="tab" aria-selected="false">Chrome</button>
<button class="chip" data-filter="ombre" role="tab" aria-selected="false">Ombré</button>
<button class="chip" data-filter="floral" role="tab" aria-selected="false">Floral</button>
<button class="chip" data-filter="minimal" role="tab" aria-selected="false">Minimal</button>
</div>
<div class="gallery" id="gallery-grid"></div>
</div>
</section>
<!-- ============ OFFERS ============ -->
<section class="offers reveal" id="offers">
<div class="wrap offers__inner">
<div class="offers__copy">
<span class="eyebrow eyebrow--light">Limited drop</span>
<h2 class="offers__title">Bring a bestie, both get 20% off</h2>
<p>
Book any two sets on the same slot and the second is a fifth lighter.
Plus every 5th visit on your card is on the house.
</p>
</div>
<div class="offers__codebox">
<span class="offers__label">Use code</span>
<code class="offers__code" id="promoCode">BESTIE20</code>
<button class="btn btn--solid" id="copyCode">Copy code</button>
</div>
</div>
</section>
<!-- ============ TEAM ============ -->
<section class="section reveal" id="team">
<div class="wrap">
<header class="sec-head">
<span class="eyebrow">Your artists</span>
<h2 class="sec-title">The hands behind the hype</h2>
<p class="sec-sub">Tap an artist to request them with your booking.</p>
</header>
<div class="team" id="team">
<button class="card artist" data-artist="Aria Vance" style="--av:linear-gradient(135deg,#f7c8d6,#cdbdf0)">
<span class="artist__avatar" aria-hidden="true">AV</span>
<h3>Aria Vance</h3>
<p>Chrome & mirror finishes</p>
</button>
<button class="card artist" data-artist="Noor Haddad" style="--av:linear-gradient(135deg,#cdbdf0,#d8dde6)">
<span class="artist__avatar" aria-hidden="true">NH</span>
<h3>Noor Haddad</h3>
<p>Freehand floral art</p>
</button>
<button class="card artist" data-artist="Jules Okafor" style="--av:linear-gradient(135deg,#f7c8d6,#e58aa6)">
<span class="artist__avatar" aria-hidden="true">JO</span>
<h3>Jules Okafor</h3>
<p>Acrylic sculpting pro</p>
</button>
<button class="card artist" data-artist="Remy Sato" style="--av:linear-gradient(135deg,#d8dde6,#cdbdf0)">
<span class="artist__avatar" aria-hidden="true">RS</span>
<h3>Remy Sato</h3>
<p>Minimal & ombré specialist</p>
</button>
</div>
</div>
</section>
</main>
<!-- ============ FOOTER ============ -->
<footer class="footer">
<div class="wrap footer__inner">
<div class="footer__brand">
<span class="brand__dot" aria-hidden="true"></span>
<span class="brand__name">Polish<em>&</em>Petals</span>
<p>14 Marigold Lane, Studio 3 · Open Tue–Sun, 10am–8pm</p>
</div>
<nav class="footer__links" aria-label="Footer">
<a href="#services">Services</a>
<a href="#gallery">Nail Art</a>
<a href="#offers">Offers</a>
<a href="#team">Artists</a>
<a href="#book" data-book>Book</a>
</nav>
<p class="footer__fine">© 2026 Polish & Petals Studio · Made with sparkle.</p>
</div>
</footer>
<div class="toast-wrap" id="toasts" aria-live="polite" aria-atomic="false"></div>
<script src="script.js"></script>
</body>
</html>Nail Bar / Studio Landing
A full, playful marketing landing for the fictional Polish and Petals Studio, built around a blush, lavender, and chrome palette with rounded Poppins type, big soft radii, and gently floating blob shapes. A translucent sticky nav blurs the page beneath it, gains a hairline shadow on scroll, highlights the active section as you move, and collapses into an accessible toggle menu on small screens. The bubbly hero leads with a gradient headline, dual calls-to-action, and a row of stats that count up the moment they enter view.
The right of the hero holds a live quick-booking card: choose a service and the estimated total updates instantly, with inline validation on name, service, and date before a tasteful toast confirms the reservation. The services grid is fully interactive — tapping any card selects that service, scrolls back to the form, and pre-fills it. A filterable gallery row of CSS-gradient nail-art tiles lets you flip between chrome, ombré, floral, and minimal looks, and every tile can be liked with a live count.
An offers band ships a copy-to-clipboard promo code, a stylist strip lets you request a specific artist that rides along with your booking, and a tidy footer rounds it out. Every section reveals on scroll via IntersectionObserver, motion respects prefers-reduced-motion, and it is pure vanilla HTML, CSS, and JavaScript — no frameworks, no build step, responsive down to 360px.