4 Commits

Author SHA1 Message Date
865440dffb enhance: update text content for clarity and warmth across sections
- Revise headings and descriptions in Contact, FinalCta, and Services sections for improved engagement.
- Introduce pricing information for services to enhance transparency.
- Update site content to reflect a more inviting tone and better align with customer experience.
2026-05-25 18:22:51 +02:00
Matthias
143820f859 bolder: Increase visual impact across all sections
- Richer color palette with more chroma and warmer tones
- Subtle paper texture background
- Stronger shadow scale (sm/md/lg/xl)
- Hero: larger typography, decorative ring, enhanced choreography
- Trust: star rating row, decorative background rings, bigger scale
- Services: card backgrounds with lift-on-hover shadows
- Reviews: stronger hover lift, thicker borders
- Team: image border, stronger avatar hover effects
- SalonPromise: decorative ring, hover states, fixed grid alignment
- SiteHeader: frosted glass backdrop-filter, stronger brand mark
- FinalCta: decorative ring, stronger radial gradients
2026-05-25 16:05:57 +02:00
Matthias
5cfcea9caf refactor: update sections for improved layout and styling
- Revamp Contact, FinalCta, Hero, Hours, SalonPromise, Services, SiteHeader, Trust components to use a new v2 design system.
- Enhance responsiveness and accessibility across sections.
- Introduce new styles for better visual hierarchy and user experience.
- Integrate review data into Trust section and update site content structure.
2026-05-25 11:59:10 +02:00
3eb46029e9 feat: add footer with impressum and datenschutz links 2026-05-18 08:22:27 +02:00
22 changed files with 1997 additions and 937 deletions

17
.impeccable.md Normal file
View File

@@ -0,0 +1,17 @@
## Design Context
### Users
Lokale Kundinnen und Kunden in Crimmitschau und Umgebung, die einen Friseurtermin suchen. Die Zielgruppe ist breit gefächert — von jungen Erwachsenen bis Senioren. Die Seite wird typischerweise kurz vor dem Anruf oder der Terminvereinbarung besucht, oft auf dem Smartphone unterwegs oder am Desktop zu Hause.
### Brand Personality
Edel, bodenständig, persönlich. Warm und einladend ohne kitschig zu wirken. Präzise Handwerkskunst trifft auf entspannte Atmosphäre. Die Kommunikation ist direkt und ehrlich — keine überzogene Marketing-Sprache.
### Aesthetic Direction
Editorial-warm mit asymmetrischen Layouts, großzügiger Typografie (Playfair Display + Source Sans 3) und einer erdigen, warmen Farbpalette in OKLCH. Lichtmodus, da die Seite tagsüber und in entspannten Kontexten konsumiert wird. Anti-Reference: Überladene Beauty-Salon-Websites mit Glitzer, Neon und Stockfoto-Ästhetik.
### Design Principles
1. **Ruhe vor Lautstärke** — Weißraum und klare Hierarchie schaffen Vertrauen
2. **Präzision spürbar machen** — Typografie und Layout sollten die Sorgfalt des Salons widerspiegeln
3. **Warm, nicht süß** — Erdige Töne statt pastelliger Süßlichkeit
4. **Funktional, aber nie langweilig** — Asymmetrie und unerwartete Proportionen verleihen Charakter
5. **Barrierefreiheit ist selbstverständlich** — Reduzierte Bewegung respektieren, klare Kontraste, lesbare Typografie

View File

@@ -0,0 +1,26 @@
---
const currentYear = new Date().getFullYear()
---
<footer class="border-t border-stone-200 bg-stone-50 py-8">
<div class="mx-auto flex max-w-7xl flex-col items-center justify-between gap-4 px-6 sm:flex-row">
<p class="text-sm text-stone-500">
&copy; {currentYear} Haarscharf Crimmitschau
</p>
<nav class="flex items-center gap-6">
<a
href="/impressum"
class="text-sm text-stone-600 transition-colors hover:text-stone-900"
>
Impressum
</a>
<a
href="/datenschutz"
class="text-sm text-stone-600 transition-colors hover:text-stone-900"
>
Datenschutz
</a>
</nav>
</div>
</footer>

View File

@@ -2,32 +2,147 @@
import { siteContent } from "@/content/site-content"
---
<section id="kontakt" class="section-shell contact-section" aria-labelledby="contact-title">
<div class="section-heading">
<p class="eyebrow">Kontakt & Anfahrt</p>
<h2 id="contact-title">Mitten in Crimmitschau.</h2>
<p class="contact-note">Der schnellste Weg zum Termin bleibt ein kurzer Anruf im Salon.</p>
<section id="kontakt" class="section-v2 contact-v2" aria-labelledby="contact-title">
<div class="section-heading-v2">
<p class="eyebrow-v2">Kontakt & Anfahrt</p>
<h2 id="contact-title">Mitten in Crimmitschau wir freuen uns auf Sie.</h2>
<p class="contact-note">Der schnellste Weg zum Termin: ein kurzer Anruf.</p>
</div>
<div class="contact-layout">
<address class="contact-address">
<div class="contact-v2__layout">
<div class="contact-v2__left">
<address class="contact-v2__address">
<span>{siteContent.business.name}</span>
<span>{siteContent.business.address.street}</span>
<span>{siteContent.business.address.postalCode} {siteContent.business.address.city}</span>
</address>
<div class="contact-actions contact-card">
<a class="contact-line" href={siteContent.business.phone.href}>
<span>Telefon</span>
<figure class="contact-image-v2">
<img
src="https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?auto=format&fit=crop&w=800&q=80"
alt="Friseur-Werkzeuge: Schere und Kamm auf einer Marmor-Arbeitsfläche"
width="800"
height="600"
loading="lazy"
decoding="async"
/>
<figcaption>Detail &amp; Sorgfalt</figcaption>
</figure>
</div>
<div class="contact-v2__actions contact-v2__card">
<a class="contact-v2__line" href={siteContent.business.phone.href}>
<span class="contact-label-v2">Telefon</span>
<strong>{siteContent.business.phone.display}</strong>
</a>
<a class="contact-line" href={`mailto:${siteContent.business.email}`}>
<span>E-Mail</span>
<a class="contact-v2__line" href={`mailto:${siteContent.business.email}`}>
<span class="contact-label-v2">E-Mail</span>
<strong>{siteContent.business.email}</strong>
</a>
<a class="button-link" href={siteContent.cta.secondary.href} target="_blank" rel="noreferrer">
<a class="button-link-v2" href={siteContent.cta.secondary.href} target="_blank" rel="noreferrer">
Route über Google Maps
</a>
</div>
</div>
</section>
<style>
.contact-v2__layout {
display: grid;
gap: clamp(3rem, 6vw, 6rem);
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
margin-top: clamp(2.5rem, 5vw, 4.5rem);
}
.contact-v2__left {
display: grid;
gap: clamp(2rem, 4vw, 3rem);
align-content: start;
}
.contact-v2__address {
display: grid;
gap: 0.35rem;
font-family: var(--font-heading);
font-size: clamp(2.2rem, 4.5vw, 4.2rem);
font-style: normal;
line-height: 1.05;
color: var(--v2-fg);
}
.contact-image-v2 {
margin: 0;
overflow: hidden;
}
.contact-image-v2 img {
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
filter: saturate(0.85) contrast(1.05);
transition: transform 0.9s var(--ease-out-quart);
}
.contact-image-v2:hover img {
transform: scale(1.03);
}
.contact-image-v2 figcaption {
color: var(--v2-muted);
font-size: 0.82rem;
margin-top: 0.5rem;
}
.contact-v2__actions {
display: grid;
gap: 0;
align-content: start;
}
.contact-v2__card {
border-top: 2px solid var(--v2-fg);
border-bottom: 1.5px solid var(--v2-hairline);
}
.contact-v2__line {
display: grid;
gap: 0.5rem;
border: 0;
border-bottom: 1.5px solid var(--v2-hairline);
color: inherit;
padding: 1.25rem 0;
text-decoration: none;
transition: border-color 200ms ease, background-color 200ms ease;
}
.contact-v2__line:hover,
.contact-v2__line:focus-visible {
background: color-mix(in oklch, var(--v2-surface) 45%, transparent);
border-color: var(--v2-primary);
}
.contact-label-v2 {
display: block;
color: var(--v2-muted);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.contact-v2__line strong {
font-size: clamp(1.25rem, 2.2vw, 1.75rem);
}
.contact-v2__actions > .button-link-v2 {
margin-top: 1.5rem;
}
@media (max-width: 820px) {
.contact-v2__layout {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,50 @@
---
---
<figure class="editorial-v2">
<img
src="https://images.unsplash.com/photo-1562322140-8baeececf3df?auto=format&fit=crop&w=1600&q=80"
alt="Warm beleuchteter Salon-Innenraum mit Friseurstühlen und Spiegeln"
width="1600"
height="900"
loading="lazy"
decoding="async"
/>
<figcaption>Atmosphäre im Salon</figcaption>
</figure>
<style>
.editorial-v2 {
margin: 0;
position: relative;
width: 100%;
height: clamp(22rem, 55vw, 38rem);
overflow: hidden;
}
.editorial-v2 img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
filter: saturate(0.85) contrast(1.05);
transition: transform 0.9s var(--ease-out-quart);
}
.editorial-v2:hover img {
transform: scale(1.03);
}
.editorial-v2 figcaption {
position: absolute;
right: clamp(1rem, 4vw, 3rem);
bottom: 1.25rem;
z-index: 2;
padding: 0.35rem 0.75rem;
background: oklch(0.1 0.02 28 / 55%);
color: oklch(0.95 0.01 84);
font-size: 0.78rem;
font-weight: 500;
}
</style>

View File

@@ -2,12 +2,69 @@
import { siteContent } from "@/content/site-content"
---
<section class="final-cta" aria-labelledby="final-cta-title">
<p class="eyebrow">Bereit für den nächsten Schnitt?</p>
<h2 id="final-cta-title">Kurz anrufen, Termin abstimmen, vorbeikommen.</h2>
<div class="final-cta-actions">
<a class="button-link button-link--light" href={siteContent.cta.primary.href}>
<section class="final-cta-v2" aria-labelledby="final-cta-title">
<div class="final-cta-v2__deco" aria-hidden="true"></div>
<p class="eyebrow-v2">Bereit für den nächsten Schnitt?</p>
<h2 id="final-cta-title">Kurz anrufen, Termin sichern, sich wohlfühlen.</h2>
<div class="final-cta-v2__actions">
<a class="button-link-v2 button-link-v2--light" href={siteContent.cta.primary.href}>
{siteContent.business.phone.display}
</a>
</div>
</section>
<style>
.final-cta-v2 {
display: grid;
gap: 2rem;
justify-items: start;
background: var(--v2-surface-dark);
color: var(--v2-hero-ink);
padding: clamp(6rem, 12vw, 11rem) clamp(1rem, 5vw, 5rem);
position: relative;
overflow: hidden;
}
.final-cta-v2__deco {
position: absolute;
top: -40%;
right: -20%;
width: 60vw;
height: 60vw;
border: 2px solid color-mix(in oklch, var(--v2-accent) 15%, transparent);
border-radius: 50%;
pointer-events: none;
}
.final-cta-v2::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 60% 50% at 80% 120%, color-mix(in oklch, var(--v2-primary) 40%, transparent) 0%, transparent 70%),
radial-gradient(ellipse 40% 30% at 20% -10%, color-mix(in oklch, var(--v2-accent) 12%, transparent) 0%, transparent 60%);
pointer-events: none;
}
.final-cta-v2 .eyebrow-v2 {
color: var(--v2-accent);
position: relative;
z-index: 2;
}
.final-cta-v2 h2 {
max-width: 14ch;
position: relative;
z-index: 2;
color: var(--v2-hero-ink);
font-size: clamp(3rem, 7vw, 7rem);
line-height: 0.92;
}
.final-cta-v2__actions {
border-top: 2px solid color-mix(in oklch, var(--v2-hero-ink) 22%, transparent);
padding-top: 2rem;
position: relative;
z-index: 2;
}
</style>

View File

@@ -2,22 +2,25 @@
import { siteContent } from "@/content/site-content"
---
<section id="top" class="hero-section" aria-labelledby="hero-title">
<div class="hero-copy">
<p class="eyebrow">{siteContent.hero.eyebrow}</p>
<section id="top" class="hero-v2" aria-labelledby="hero-title">
<div class="hero-deco" aria-hidden="true">H</div>
<div class="hero-deco-ring" aria-hidden="true"></div>
<div class="hero-v2__copy">
<p class="eyebrow-v2">{siteContent.hero.eyebrow}</p>
<h1 id="hero-title">{siteContent.hero.title}</h1>
<p class="hero-intro">{siteContent.hero.intro}</p>
<p class="hero-v2__intro">{siteContent.hero.intro}</p>
<div class="hero-actions">
<a class="button-link" href={siteContent.cta.primary.href}>
<div class="hero-v2__actions">
<a class="button-link-v2" href={siteContent.cta.primary.href}>
{siteContent.cta.primary.label}
</a>
<a class="button-link button-link--secondary" href={siteContent.cta.secondary.href} target="_blank" rel="noreferrer">
<a class="button-link-v2 button-link-v2--ghost" href={siteContent.cta.secondary.href} target="_blank" rel="noreferrer">
{siteContent.cta.secondary.label}
</a>
</div>
</div>
<dl class="salon-meta" aria-label="Saloninformationen">
<dl class="hero-v2__meta" aria-label="Saloninformationen">
<div>
<dt>Adresse</dt>
<dd>{siteContent.business.address.display}</dd>
@@ -31,12 +34,186 @@ import { siteContent } from "@/content/site-content"
<dd>{siteContent.hours[0].time}</dd>
</div>
</dl>
</div>
<figure class="hero-image">
<img src={siteContent.hero.image.src} alt={siteContent.hero.image.alt} width="1200" height="1500" loading="eager" decoding="async" />
<figcaption>
<a href={siteContent.hero.image.creditUrl} target="_blank" rel="noreferrer">{siteContent.hero.image.credit}</a>
</figcaption>
</figure>
</section>
<style>
/* ─── Hero: asymmetric, oversized, dramatic ─── */
.hero-v2 {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
gap: clamp(2rem, 5vw, 5rem);
align-items: end;
min-height: 100svh;
overflow: hidden;
padding: clamp(8rem, 16vw, 12rem) clamp(1rem, 5vw, 5rem) clamp(4rem, 8vw, 7rem);
background:
linear-gradient(170deg, color-mix(in oklch, var(--v2-surface-warm) 65%, transparent) 0%, transparent 55%),
linear-gradient(220deg, color-mix(in oklch, var(--v2-accent) 8%, transparent) 0%, transparent 50%);
}
.hero-deco {
position: absolute;
top: 52%;
left: 50%;
transform: translate(-50%, -50%);
font-family: var(--font-heading);
font-size: 58vw;
font-weight: 600;
line-height: 0.78;
color: color-mix(in oklch, var(--v2-primary) 7%, transparent);
pointer-events: none;
user-select: none;
z-index: 0;
}
.hero-deco-ring {
position: absolute;
top: 52%;
left: 50%;
transform: translate(-50%, -50%);
width: 48vw;
height: 48vw;
border: 1.5px solid color-mix(in oklch, var(--v2-primary) 10%, transparent);
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
.hero-v2__copy {
position: relative;
z-index: 2;
max-width: 54rem;
}
.hero-v2__copy .eyebrow-v2 {
margin-bottom: 1.4rem;
}
.hero-v2__copy h1 {
font-family: var(--font-heading);
font-size: clamp(4rem, 12vw, 11rem);
font-weight: 600;
line-height: 0.86;
margin: 0 0 1.8rem;
max-width: 14ch;
letter-spacing: -0.02em;
}
.hero-v2__intro {
max-width: 38ch;
color: var(--v2-muted);
font-size: clamp(1.15rem, 2.1vw, 1.55rem);
line-height: 1.55;
margin: 0;
}
.hero-v2__actions {
display: flex;
flex-wrap: wrap;
gap: 0.85rem;
padding-top: 2.2rem;
}
.hero-v2__meta {
position: relative;
z-index: 2;
display: grid;
gap: 0;
align-self: end;
margin: 0;
border-top: 2px solid var(--v2-hairline);
}
.hero-v2__meta div {
display: grid;
gap: 0.4rem;
padding: 1.2rem 0;
border-bottom: 2px solid var(--v2-hairline);
}
.hero-v2__meta dt {
color: var(--v2-muted);
font-size: 0.7rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.hero-v2__meta dd {
margin: 0;
font-weight: 700;
font-size: 1.15rem;
line-height: 1.2;
}
/* ─── Hero Load Choreography ─── */
.hero-deco {
opacity: 0;
animation: scale-in 1.4s var(--ease-out-expo) 0.1s forwards;
}
.hero-deco-ring {
opacity: 0;
animation: scale-in 1.6s var(--ease-out-expo) 0.25s forwards;
}
.hero-v2__copy .eyebrow-v2 {
opacity: 0;
animation: fade-up 0.75s var(--ease-out-expo) 0.35s forwards;
}
.hero-v2__copy h1 {
opacity: 0;
animation: fade-up 0.85s var(--ease-out-expo) 0.48s forwards;
}
.hero-v2__intro {
opacity: 0;
animation: fade-up 0.75s var(--ease-out-expo) 0.6s forwards;
}
.hero-v2__actions {
opacity: 0;
animation: fade-up 0.65s var(--ease-out-expo) 0.72s forwards;
}
.hero-v2__meta {
opacity: 0;
animation: fade-up 0.75s var(--ease-out-expo) 0.9s forwards;
}
@media (prefers-reduced-motion: reduce) {
.hero-deco,
.hero-deco-ring,
.hero-v2__copy .eyebrow-v2,
.hero-v2__copy h1,
.hero-v2__intro,
.hero-v2__actions,
.hero-v2__meta {
opacity: 1;
transform: none;
animation: none;
}
}
@media (max-width: 820px) {
.hero-v2 {
grid-template-columns: 1fr;
min-height: 100svh;
}
.hero-v2__copy h1 {
font-size: clamp(3.2rem, 14vw, 5.5rem);
}
.hero-v2__meta {
margin-top: 2.5rem;
}
.hero-deco-ring {
width: 80vw;
height: 80vw;
}
}
</style>

View File

@@ -2,17 +2,17 @@
import { siteContent } from "@/content/site-content"
---
<section id="zeiten" class="section-shell hours-section cut-line" aria-labelledby="hours-title">
<div class="section-heading">
<p class="eyebrow">Öffnungszeiten</p>
<section id="zeiten" class="section-v2 hours-v2" aria-labelledby="hours-title">
<div class="section-heading-v2">
<p class="eyebrow-v2">Öffnungszeiten</p>
<h2 id="hours-title">Planbar von Montag bis Samstag.</h2>
<p>Für Termine bitte kurz anrufen. So lässt sich direkt klären, wann genug Zeit für Beratung, Schnitt oder Farbe frei ist.</p>
</div>
<div class="hours-panel">
<div class="hours-v2__panel">
{
siteContent.hours.map((entry) => (
<div class="hours-row">
<div class="hours-v2__row">
<span>{entry.day}</span>
<time>{entry.time}</time>
</div>
@@ -20,3 +20,41 @@ import { siteContent } from "@/content/site-content"
}
</div>
</section>
<style>
.hours-v2 {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.75fr);
gap: clamp(3rem, 6vw, 6rem);
background: var(--v2-surface);
}
.hours-v2__panel {
border-top: 2px solid var(--v2-fg);
background: color-mix(in oklch, var(--v2-bg) 50%, transparent);
padding: 0 clamp(0.2rem, 1vw, 0.8rem);
}
.hours-v2__row {
display: flex;
justify-content: space-between;
gap: 1.5rem;
border-bottom: 1.5px solid color-mix(in oklch, var(--v2-fg) 16%, transparent);
padding: 1.15rem 0;
}
.hours-v2__row span {
font-weight: 800;
}
.hours-v2__row time {
color: var(--v2-muted);
font-variant-numeric: tabular-nums;
}
@media (max-width: 820px) {
.hours-v2 {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,178 @@
---
import { siteContent } from "@/content/site-content"
function renderStars(rating: number) {
return Array.from({ length: 5 }, (_, i) =>
i < rating
? '<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0.5l2.47 5.01L16 6.22l-4 3.9.94 5.5L8 12.88l-4.94 2.6.94-5.5-4-3.9 5.53-.71L8 .5z"/></svg>'
: '<svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true"><path d="M8 0.5l2.47 5.01L16 6.22l-4 3.9.94 5.5L8 12.88l-4.94 2.6.94-5.5-4-3.9 5.53-.71L8 .5z"/></svg>'
).join("")
}
---
<section id="bewertungen" class="section-v2 reviews-v2" aria-labelledby="reviews-title">
<div class="section-heading-v2 section-heading-v2--centered">
<p class="eyebrow-v2">Kundenstimmen</p>
<h2 id="reviews-title">Das sagen unsere Gäste.</h2>
<p>Echte Bewertungen von Google ungefiltert und ehrlich.</p>
</div>
<div class="reviews-v2__grid">
{
siteContent.reviews.map((review) => (
<article class="reviews-v2__card">
<div class="reviews-v2__header">
<div class="reviews-v2__avatar">
<span>{review.author.split(" ").map(n => n[0]).join("").substring(0, 2)}</span>
</div>
<div class="reviews-v2__meta">
<p class="reviews-v2__author">{review.author}</p>
<p class="reviews-v2__date">{review.date}</p>
</div>
<div class="reviews-v2__stars" set:html={renderStars(review.rating)} />
</div>
<blockquote class="reviews-v2__text">
<span class="reviews-v2__quote" aria-hidden="true">"</span>
{review.text}
</blockquote>
</article>
))
}
</div>
<div class="reviews-v2__footer">
<a
class="reviews-v2__link"
href={siteContent.reviewSummary.url}
target="_blank"
rel="noreferrer"
>
<span>Alle {siteContent.reviewSummary.count} auf Google ansehen</span>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M6 3l5 5-5 5"/>
</svg>
</a>
</div>
</section>
<style>
.reviews-v2 {
background: var(--v2-surface-warm);
}
.reviews-v2__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: clamp(1.5rem, 2.8vw, 2.2rem);
margin-top: clamp(3rem, 6vw, 5rem);
}
.reviews-v2__card {
display: grid;
gap: 1.4rem;
padding: clamp(1.8rem, 3.5vw, 2.6rem);
background: var(--v2-bg);
border: 2px solid var(--v2-hairline);
border-radius: var(--v2-radius);
transition: transform 0.4s var(--ease-out-quart), box-shadow 0.4s var(--ease-out-quart);
}
.reviews-v2__card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-lg);
}
.reviews-v2__header {
display: flex;
align-items: center;
gap: 0.9rem;
}
.reviews-v2__avatar {
display: grid;
place-items: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 999px;
background: var(--v2-primary);
color: var(--v2-hero-ink);
font-family: var(--font-heading);
font-size: 0.85rem;
font-weight: 600;
flex-shrink: 0;
}
.reviews-v2__meta {
display: grid;
gap: 0.15rem;
flex: 1;
}
.reviews-v2__author {
font-weight: 700;
font-size: 0.95rem;
line-height: 1.2;
margin: 0;
}
.reviews-v2__date {
font-size: 0.76rem;
color: var(--v2-muted);
margin: 0;
}
.reviews-v2__stars {
display: flex;
gap: 0.2rem;
color: var(--v2-accent);
flex-shrink: 0;
}
.reviews-v2__text {
position: relative;
margin: 0;
padding-left: 1.2rem;
color: var(--v2-muted);
font-size: 0.95rem;
line-height: 1.65;
}
.reviews-v2__quote {
position: absolute;
left: -0.2rem;
top: -0.5rem;
font-family: var(--font-heading);
font-size: 3rem;
line-height: 1;
color: var(--v2-primary);
opacity: 0.3;
}
.reviews-v2__footer {
display: flex;
justify-content: center;
margin-top: clamp(2.5rem, 5vw, 3.5rem);
}
.reviews-v2__link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.85rem 1.8rem;
border: 2px solid var(--v2-hairline);
border-radius: var(--v2-radius);
color: var(--v2-fg);
font-weight: 700;
font-size: 0.95rem;
text-decoration: none;
transition: background-color 180ms ease, border-color 180ms ease, transform 220ms var(--ease-out-quart);
}
.reviews-v2__link:hover,
.reviews-v2__link:focus-visible {
background: var(--v2-fg);
border-color: var(--v2-fg);
color: var(--v2-bg);
transform: translateY(-2px);
}
</style>

View File

@@ -2,17 +2,17 @@
import { siteContent } from "@/content/site-content"
---
<section class="section-shell promise-section cut-line" aria-labelledby="promise-title">
<div class="promise-intro">
<p class="eyebrow">Salonversprechen</p>
<section class="section-v2 promise-v2" aria-labelledby="promise-title">
<div class="promise-v2__intro">
<p class="eyebrow-v2">Salonversprechen</p>
<h2 id="promise-title">Haare dürfen leicht aussehen, auch wenn dahinter Präzision steckt.</h2>
</div>
<div class="promise-grid">
<div class="promise-v2__grid">
{
siteContent.promise.map((item, index) => (
<article class="promise-card">
<span class="index-label">{String(index + 1).padStart(2, "0")}</span>
<article class="promise-v2__card">
<span class="index-label-v2">{String(index + 1).padStart(2, "0")}</span>
<h3>{item.title}</h3>
<p>{item.text}</p>
</article>
@@ -20,3 +20,102 @@ import { siteContent } from "@/content/site-content"
}
</div>
</section>
<style>
.promise-v2 {
display: grid;
grid-template-columns: minmax(0, 0.75fr) minmax(0, 1.25fr);
gap: clamp(3.5rem, 7vw, 7rem);
background: var(--v2-surface-dark);
color: var(--v2-hero-ink);
position: relative;
overflow: hidden;
}
.promise-v2::before {
content: "";
position: absolute;
top: -20%;
right: -10%;
width: 50vw;
height: 50vw;
border: 2px solid color-mix(in oklch, var(--v2-accent) 12%, transparent);
border-radius: 50%;
pointer-events: none;
}
.promise-v2__intro {
position: sticky;
top: 6rem;
align-self: start;
position: relative;
z-index: 2;
}
.promise-v2__intro .eyebrow-v2 {
color: var(--v2-accent);
}
.promise-v2__intro h2 {
color: var(--v2-hero-ink);
margin-top: 1.1rem;
font-size: clamp(2.8rem, 6.5vw, 6.5rem);
line-height: 0.95;
}
.promise-v2__grid {
display: grid;
gap: 0;
border-top: 2px solid color-mix(in oklch, var(--v2-hero-ink) 16%, transparent);
position: relative;
z-index: 2;
}
.promise-v2__card {
display: grid;
grid-template-columns: minmax(4rem, 0.2fr) minmax(0, 0.8fr);
column-gap: clamp(1.4rem, 3.5vw, 2.8rem);
border-bottom: 2px solid color-mix(in oklch, var(--v2-hero-ink) 16%, transparent);
padding: clamp(1.8rem, 3vw, 2.8rem) 0;
transition: background-color 250ms ease;
}
.promise-v2__card:hover {
background: color-mix(in oklch, var(--v2-hero-ink) 3%, transparent);
}
.index-label-v2 {
color: var(--v2-accent);
display: block;
font-family: var(--font-heading);
font-size: clamp(2.8rem, 5.5vw, 5.5rem);
line-height: 0.82;
opacity: 0.9;
}
.promise-v2__card h3 {
grid-column: 2;
font-family: var(--font-heading);
font-size: clamp(1.5rem, 2.4vw, 2.4rem);
line-height: 1.05;
margin: 0 0 0.85rem;
color: var(--v2-hero-ink);
}
.promise-v2__card p {
grid-column: 2;
color: color-mix(in oklch, var(--v2-hero-ink) 70%, transparent);
line-height: 1.65;
margin: 0;
}
@media (max-width: 820px) {
.promise-v2 {
grid-template-columns: 1fr;
}
.promise-v2__intro {
position: static;
}
}
</style>

View File

@@ -2,22 +2,96 @@
import { siteContent } from "@/content/site-content"
---
<section id="leistungen" class="section-shell services-section" aria-labelledby="services-title">
<div class="section-heading">
<p class="eyebrow">Leistungen</p>
<section id="leistungen" class="section-v2 services-v2" aria-labelledby="services-title">
<div class="section-heading-v2">
<p class="eyebrow-v2">Leistungen</p>
<h2 id="services-title">Alles, was ein guter Salonbesuch braucht.</h2>
<p>Die Auswahl bleibt bewusst klar. Details, Wünsche und Termine werden am besten direkt telefonisch besprochen.</p>
<p>Fokus auf das Wesentliche: Ihr Look, Ihr Typ, Ihr Wohlfühlen. Details und Preise besprechen wir am besten kurz am Telefon.</p>
</div>
<div class="service-list">
<div class="services-v2__grid">
{
siteContent.services.map((service, index) => (
<article class="service-row">
<span class="service-index">{String(index + 1).padStart(2, "0")}</span>
<article class="services-v2__card" style={index % 2 === 1 ? "margin-top: 3.5rem;" : undefined}>
<span class="service-index-v2">{String(index + 1).padStart(2, "0")}</span>
<h3>{service.title}</h3>
<p>{service.text}</p>
{service.price && <span class="service-price-v2">{service.price}</span>}
</article>
))
}
</div>
</section>
<style>
.services-v2 {
background: var(--v2-bg);
}
.services-v2__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(2.5rem, 5vw, 5rem);
margin-top: clamp(3.5rem, 6vw, 6rem);
}
.services-v2__card {
display: grid;
gap: 0.9rem;
padding: clamp(2rem, 3.5vw, 3.5rem) clamp(1.5rem, 2.5vw, 2.5rem);
background: var(--v2-surface);
border: 1.5px solid var(--v2-hairline);
border-radius: var(--v2-radius);
transition: transform 0.4s var(--ease-out-quart), box-shadow 0.4s var(--ease-out-quart);
}
.services-v2__card:hover {
transform: translateY(-6px);
box-shadow: var(--shadow-lg);
}
.services-v2__card:nth-child(even) {
margin-top: 5rem;
}
.service-index-v2 {
color: var(--v2-primary);
font-family: var(--font-heading);
font-size: clamp(3.5rem, 7vw, 7rem);
line-height: 0.82;
display: block;
margin-bottom: 0.75rem;
opacity: 0.85;
}
.services-v2__card h3 {
font-family: var(--font-heading);
font-size: clamp(1.6rem, 2.8vw, 2.4rem);
line-height: 1.05;
margin: 0;
}
.services-v2__card p {
color: var(--v2-muted);
line-height: 1.65;
margin: 0;
max-width: 36ch;
}
.service-price-v2 {
color: var(--v2-muted);
font-size: 0.85rem;
margin-top: 0.5rem;
opacity: 0.75;
}
@media (max-width: 820px) {
.services-v2__grid {
grid-template-columns: 1fr;
}
.services-v2__card:nth-child(even) {
margin-top: 0;
}
}
</style>

View File

@@ -3,12 +3,14 @@ import { siteContent } from "@/content/site-content"
const navItems = [
{ label: "Leistungen", href: "#leistungen" },
{ label: "Team", href: "#team" },
{ label: "Bewertungen", href: "#bewertungen" },
{ label: "Zeiten", href: "#zeiten" },
{ label: "Kontakt", href: "#kontakt" },
]
---
<header class="site-header">
<header class="site-header-v2">
<a class="brand-lockup" href="#top" aria-label="Haarscharf Startseite">
<span class="brand-mark">H</span>
<span>
@@ -17,17 +19,292 @@ const navItems = [
</span>
</a>
<nav aria-label="Hauptnavigation" class="hidden items-center gap-1 md:flex">
{
navItems.map((item) => (
<a class="nav-link" href={item.href}>
{item.label}
</a>
))
}
<nav aria-label="Hauptnavigation" class="desktop-nav">
{navItems.map((item) => <a class="nav-link-v2" href={item.href}>{item.label}</a>)}
</nav>
<a class="button-link button-link--compact" href={siteContent.cta.primary.href}>
<div class="flex items-center gap-2">
<button
class="mobile-nav-toggle"
aria-expanded="false"
aria-controls="mobile-nav"
aria-label="Menü öffnen"
type="button"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
<line x1="3" y1="5" x2="17" y2="5" />
<line x1="3" y1="10" x2="17" y2="10" />
<line x1="3" y1="15" x2="17" y2="15" />
</svg>
</button>
<a class="button-link-v2 button-link-v2--compact" href={siteContent.cta.primary.href}>
Anrufen
</a>
</div>
</header>
<div id="mobile-nav" class="mobile-nav" aria-label="Mobile Navigation" role="dialog" aria-modal="true">
<div class="mobile-nav-backdrop"></div>
<div class="mobile-nav-panel">
<button class="mobile-nav-close" aria-label="Menü schließen" type="button">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
<line x1="4" y1="4" x2="16" y2="16" />
<line x1="16" y1="4" x2="4" y2="16" />
</svg>
</button>
<ul class="mobile-nav-list">
{navItems.map((item) => (
<li><a class="mobile-nav-link" href={item.href}>{item.label}</a></li>
))}
</ul>
<a class="mobile-nav-phone" href={siteContent.business.phone.href}>
<span class="contact-label">Telefon</span>
<strong>{siteContent.business.phone.display}</strong>
</a>
</div>
</div>
<script>
import "@/scripts/mobile-nav";
</script>
<style>
/* ─── Header ─── */
.site-header-v2 {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 50;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 1rem;
padding: clamp(0.85rem, 2.2vw, 1.35rem) clamp(1rem, 4vw, 3rem);
color: var(--v2-fg);
background: color-mix(in oklch, var(--v2-bg) 92%, transparent);
backdrop-filter: blur(14px) saturate(130%);
-webkit-backdrop-filter: blur(14px) saturate(130%);
border-bottom: 1.5px solid color-mix(in oklch, var(--v2-fg) 6%, transparent);
}
.site-header-v2 .brand-lockup {
display: inline-flex;
align-items: center;
gap: 0.75rem;
width: fit-content;
color: inherit;
text-decoration: none;
}
.site-header-v2 .brand-mark {
display: grid;
place-items: center;
width: 2.5rem;
height: 2.5rem;
border: 2px solid currentColor;
border-radius: 999px;
font-family: var(--font-heading);
font-size: 1.1rem;
line-height: 1;
font-weight: 600;
}
.site-header-v2 .brand-descriptor {
display: block;
color: color-mix(in oklch, currentColor 60%, transparent);
font-size: 0.76rem;
line-height: 1.1;
font-weight: 500;
}
.nav-link-v2 {
border-radius: 999px;
color: inherit;
font-size: 0.92rem;
font-weight: 600;
padding: 0.55rem 0.95rem;
text-decoration: none;
transition: background-color 180ms ease, color 180ms ease;
}
.nav-link-v2:hover,
.nav-link-v2:focus-visible {
background: color-mix(in oklch, currentColor 12%, transparent);
}
.desktop-nav {
display: none;
align-items: center;
gap: 0.25rem;
}
/* ─── Mobile Navigation ─── */
.mobile-nav-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border: 2px solid color-mix(in oklch, currentColor 28%, transparent);
border-radius: var(--v2-radius);
background: color-mix(in oklch, currentColor 8%, transparent);
color: inherit;
cursor: pointer;
transition: background-color 180ms ease, border-color 180ms ease;
}
.mobile-nav-toggle:hover,
.mobile-nav-toggle:focus-visible {
background: color-mix(in oklch, currentColor 16%, transparent);
border-color: color-mix(in oklch, currentColor 45%, transparent);
}
.mobile-nav {
position: fixed;
inset: 0;
z-index: 60;
pointer-events: none;
visibility: hidden;
}
.mobile-nav.is-open {
pointer-events: auto;
visibility: visible;
}
.mobile-nav-backdrop {
position: absolute;
inset: 0;
background: oklch(0.1 0.02 28 / 55%);
opacity: 0;
transition: opacity 200ms ease;
}
.mobile-nav.is-open .mobile-nav-backdrop {
opacity: 1;
}
.mobile-nav-panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: min(22rem, 85vw);
background: var(--v2-bg);
color: var(--v2-fg);
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 1.5rem;
transform: translateX(100%);
transition: transform 280ms cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: -12px 0 40px oklch(0.1 0.02 28 / 18%);
}
.mobile-nav.is-open .mobile-nav-panel {
transform: translateX(0);
}
.mobile-nav-close {
align-self: flex-end;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border: 2px solid var(--v2-hairline);
border-radius: var(--v2-radius);
background: transparent;
color: var(--v2-fg);
cursor: pointer;
transition: background-color 180ms ease;
}
.mobile-nav-close:hover,
.mobile-nav-close:focus-visible {
background: var(--v2-surface);
}
.mobile-nav-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.mobile-nav-link {
display: block;
padding: 0.9rem 0.5rem;
border-radius: var(--v2-radius);
color: var(--v2-fg);
font-size: 1.35rem;
font-weight: 600;
text-decoration: none;
transition: background-color 180ms ease;
min-height: 44px;
line-height: 1.2;
}
.mobile-nav-link:hover,
.mobile-nav-link:focus-visible {
background: var(--v2-surface);
}
.mobile-nav-phone {
margin-top: auto;
display: grid;
gap: 0.3rem;
padding: 1rem;
border-radius: var(--v2-radius);
border: 2px solid var(--v2-hairline);
background: color-mix(in oklch, var(--v2-surface) 50%, transparent);
color: var(--v2-fg);
text-decoration: none;
transition: background-color 180ms ease, border-color 180ms ease;
}
.mobile-nav-phone:hover,
.mobile-nav-phone:focus-visible {
background: var(--v2-surface);
border-color: var(--v2-primary);
}
.mobile-nav-phone strong {
font-size: 1.4rem;
font-weight: 700;
}
/* Mobile nav link stagger */
.mobile-nav.is-open .mobile-nav-list li {
opacity: 0;
animation: fade-up 0.5s var(--ease-out-expo) forwards;
}
.mobile-nav.is-open .mobile-nav-list li:nth-child(1) { animation-delay: 0.08s; }
.mobile-nav.is-open .mobile-nav-list li:nth-child(2) { animation-delay: 0.14s; }
.mobile-nav.is-open .mobile-nav-list li:nth-child(3) { animation-delay: 0.20s; }
.mobile-nav.is-open .mobile-nav-list li:nth-child(4) { animation-delay: 0.26s; }
.mobile-nav.is-open .mobile-nav-phone {
opacity: 0;
animation: fade-up 0.5s var(--ease-out-expo) 0.32s forwards;
}
@media (min-width: 769px) {
.desktop-nav {
display: flex;
}
.mobile-nav-toggle {
display: none;
}
}
@media (max-width: 768px) {
.site-header-v2 {
grid-template-columns: 1fr auto;
}
}
</style>

View File

@@ -0,0 +1,145 @@
---
import { siteContent } from "@/content/site-content"
---
<section id="team" class="section-v2 team-v2" aria-labelledby="team-title">
<div class="section-heading-v2 section-heading-v2--centered">
<p class="eyebrow-v2">Unser Team</p>
<h2 id="team-title">Die Menschen hinter den Scheren.</h2>
</div>
<figure class="team-image-v2">
<img
src="https://images.unsplash.com/photo-1711274093746-b588a17d2716?auto=format&fit=crop&w=1200&q=82"
alt="Eine Friseurin bei der Arbeit mit Schere"
width="1200"
height="600"
loading="lazy"
decoding="async"
/>
<figcaption>Handwerk mit Präzision</figcaption>
</figure>
<div class="team-v2__grid">
{
siteContent.team.map((member) => (
<article class="team-v2__card">
<div class="team-v2__avatar">
<span>{member.initials}</span>
</div>
<h3>{member.name}</h3>
<p class="team-v2__role">{member.role}</p>
<p>{member.bio}</p>
</article>
))
}
</div>
</section>
<style>
.team-v2 {
background: var(--v2-surface-warm);
}
.team-image-v2 {
margin: clamp(2.5rem, 5vw, 4rem) auto 0;
max-width: 72rem;
position: relative;
overflow: hidden;
border: 2px solid var(--v2-hairline);
border-radius: var(--v2-radius);
}
.team-image-v2 img {
width: 100%;
height: auto;
aspect-ratio: 21 / 9;
object-fit: cover;
display: block;
filter: saturate(0.82) contrast(1.08);
transition: transform 1s var(--ease-out-quart);
}
.team-image-v2:hover img {
transform: scale(1.04);
}
.team-image-v2 figcaption {
position: absolute;
right: 1.25rem;
bottom: 1rem;
z-index: 2;
padding: 0.4rem 0.85rem;
background: oklch(0.1 0.02 28 / 60%);
color: oklch(0.95 0.01 84);
font-size: 0.76rem;
font-weight: 500;
}
.team-v2__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 0;
margin-top: clamp(2.5rem, 5vw, 4.5rem);
border-top: 2px solid var(--v2-hairline);
border-left: 2px solid var(--v2-hairline);
}
.team-v2__card {
display: grid;
justify-items: start;
gap: 0.7rem;
border-bottom: 2px solid var(--v2-hairline);
border-right: 2px solid var(--v2-hairline);
padding: clamp(2.5rem, 4.5vw, 4rem) clamp(1.5rem, 3vw, 2.5rem);
transition: background-color 250ms ease, transform 0.4s var(--ease-out-quart);
}
.team-v2__card:hover {
background: color-mix(in oklch, var(--v2-bg) 50%, transparent);
transform: translateY(-4px);
}
.team-v2__avatar {
display: grid;
place-items: center;
width: 6rem;
height: 6rem;
margin-bottom: 0.9rem;
border-radius: 999px;
background: var(--v2-surface-dark);
font-family: var(--font-heading);
font-size: 1.4rem;
font-weight: 600;
color: var(--v2-hero-ink);
transition: transform 0.4s var(--ease-out-quart), box-shadow 0.4s var(--ease-out-quart);
}
.team-v2__card:hover .team-v2__avatar {
transform: scale(1.08);
box-shadow: var(--shadow-md);
}
.team-v2__card h3 {
font-family: var(--font-heading);
font-size: clamp(1.3rem, 2.2vw, 1.7rem);
line-height: 1.15;
margin: 0;
}
.team-v2__role {
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--v2-primary);
margin: 0;
}
.team-v2__card > p:last-child {
color: var(--v2-muted);
font-size: 0.95rem;
line-height: 1.65;
margin: 0;
}
</style>

View File

@@ -2,13 +2,117 @@
import { siteContent } from "@/content/site-content"
---
<section class="trust-band" aria-labelledby="trust-title">
<div>
<p class="eyebrow">Lokales Vertrauen</p>
<section class="trust-v2" aria-labelledby="trust-title">
<div class="trust-v2__rating">
<p class="eyebrow-v2">Lokales Vertrauen</p>
<h2 id="trust-title">{siteContent.reviewSummary.rating}</h2>
<div class="trust-v2__stars" aria-hidden="true">
<svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0.5l2.47 5.01L16 6.22l-4 3.9.94 5.5L8 12.88l-4.94 2.6.94-5.5-4-3.9 5.53-.71L8 .5z"/></svg>
<svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0.5l2.47 5.01L16 6.22l-4 3.9.94 5.5L8 12.88l-4.94 2.6.94-5.5-4-3.9 5.53-.71L8 .5z"/></svg>
<svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0.5l2.47 5.01L16 6.22l-4 3.9.94 5.5L8 12.88l-4.94 2.6.94-5.5-4-3.9 5.53-.71L8 .5z"/></svg>
<svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0.5l2.47 5.01L16 6.22l-4 3.9.94 5.5L8 12.88l-4.94 2.6.94-5.5-4-3.9 5.53-.71L8 .5z"/></svg>
<svg width="28" height="28" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0.5l2.47 5.01L16 6.22l-4 3.9.94 5.5L8 12.88l-4.94 2.6.94-5.5-4-3.9 5.53-.71L8 .5z"/></svg>
</div>
<p class="trust-copy">
</div>
<p class="trust-v2__copy">
<strong>{siteContent.reviewSummary.count}</strong>
<span>{siteContent.reviewSummary.text}</span>
</p>
</section>
<style>
.trust-v2 {
display: grid;
align-items: center;
gap: 3rem;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
background: var(--v2-primary);
color: var(--v2-hero-ink);
padding: clamp(4.5rem, 9vw, 8rem) clamp(1rem, 5vw, 5rem);
position: relative;
overflow: hidden;
}
.trust-v2::before {
content: "";
position: absolute;
top: -20%;
right: -10%;
width: 50vw;
height: 50vw;
border: 2px solid color-mix(in oklch, var(--v2-accent) 25%, transparent);
border-radius: 50%;
pointer-events: none;
}
.trust-v2::after {
content: "";
position: absolute;
bottom: -30%;
left: -15%;
width: 40vw;
height: 40vw;
border: 2px solid color-mix(in oklch, var(--v2-accent) 18%, transparent);
border-radius: 50%;
pointer-events: none;
}
.trust-v2 .eyebrow-v2 {
color: var(--v2-accent);
position: relative;
z-index: 2;
}
.trust-v2 h2 {
font-family: var(--font-heading);
font-size: clamp(7rem, 16vw, 15rem);
line-height: 0.82;
margin: 0.8rem 0 0;
position: relative;
z-index: 2;
letter-spacing: -0.02em;
}
.trust-v2__stars {
display: flex;
gap: 0.4rem;
margin-top: 1.2rem;
color: var(--v2-accent);
position: relative;
z-index: 2;
}
.trust-v2__copy {
display: grid;
gap: 0.9rem;
font-size: clamp(1.15rem, 2.1vw, 1.6rem);
line-height: 1.55;
max-width: 36ch;
margin: 0;
color: color-mix(in oklch, var(--v2-hero-ink) 78%, transparent);
position: relative;
z-index: 2;
}
.trust-v2__copy strong {
color: var(--v2-hero-ink);
font-size: 1.35em;
font-weight: 700;
}
@media (max-width: 820px) {
.trust-v2 {
grid-template-columns: 1fr;
gap: 2rem;
}
.trust-v2 h2 {
font-size: clamp(5.5rem, 20vw, 9rem);
}
.trust-v2__stars svg {
width: 22px;
height: 22px;
}
}
</style>

View File

@@ -42,7 +42,7 @@ export type SiteContent = {
services: Array<{
title: string
text: string
price?: undefined
price?: string
}>
hours: Array<{
day: string
@@ -52,7 +52,14 @@ export type SiteContent = {
rating: string
count: string
text: string
url: string
}
reviews: Array<{
author: string
rating: number
text: string
date: string
}>
team: Array<{
name: string
role: string
@@ -85,7 +92,7 @@ export const siteContent = {
eyebrow: "Studio Haarscharf · Crimmitschau",
title: "Schnitt, Farbe und Styling mit ruhiger Hand.",
intro:
"Ein lokaler Salon für Menschen, die eine Frisur wollen, die im Alltag sitzt und zur Person passt.",
"Frisuren, die im Alltag funktionieren und zu Ihrem Typ passen. Wir beraten ehrlich, schneiden präzise und sorgen dafür, dass Sie sich wohlfühlen mitten in Crimmitschau.",
image: {
src: "https://images.unsplash.com/photo-1711274093746-b588a17d2716?auto=format&fit=crop&w=1600&q=82",
alt: "Eine Person schneidet Haare mit einer Schere in einem Friseurumfeld",
@@ -95,7 +102,7 @@ export const siteContent = {
},
cta: {
primary: {
label: "Termin telefonisch anfragen",
label: "Anrufen & Termin vereinbaren",
href: "tel:+4937626781710",
},
secondary: {
@@ -106,33 +113,37 @@ export const siteContent = {
promise: [
{
title: "Typgerecht statt beliebig",
text: "Beratung, Schnitt und Styling werden auf Haarstruktur, Alltag und Wunschbild abgestimmt.",
text: "Wir schauen auf Ihre Haarstruktur, Ihren Alltag und Ihren Stil. Das Ergebnis: eine Frisur, die wirklich zu Ihnen passt und morgens ohne Drama funktioniert.",
},
{
title: "Klassisch bis modern",
text: "Von gepflegten Kurzhaarschnitten bis zu frischen Farb- und Stylingideen bleibt der Look tragbar.",
text: "Ob klassischer Kurzhaarschnitt oder moderner Farb-Look wir achten darauf, dass Sie sich damit wohlfühlen und es im Alltag funktioniert.",
},
{
title: "Vor Ort verwurzelt",
text: "Mitten in Crimmitschau, gut erreichbar in der Annenstraße und unkompliziert per Telefon.",
text: "Seit über 20 Jahren in Crimmitschau. Persönlich, verlässlich und unkompliziert erreichbar in der Annenstraße.",
},
],
services: [
{
title: "Damen- und Herrenhaarschnitte",
text: "Saubere Konturen, Form und Finish für kurze, mittlere und lange Haare.",
text: "Vom pflegenden Kurzhaarschnitt bis zur langen Form Sie verlassen den Salon mit einer Frisur, die morgens ohne Drama funktioniert und Ihren Typ unterstreicht.",
price: "ab ca. 30 €",
},
{
title: "Coloration und Haarstyling",
text: "Farbauffrischung, neue Nuancen und Styling für den nächsten Auftritt.",
text: "Ob Ansatz auffrischen oder kompletter Farbwechsel wir finden den Ton, der zu Ihrem Teint passt und sich natürlich anfühlt.",
price: "nach Absprache",
},
{
title: "Hochsteck- und Brautfrisuren",
text: "Festliche Frisuren und besondere Anlässe nach telefonischer Absprache.",
text: "Festliche Frisuren für Hochzeiten, Abibälle und besondere Anlässe nach telefonischer Absprache, damit wir genug Zeit für Sie haben.",
price: "ab ca. 45 €",
},
{
title: "Kinderhaarschnitte",
text: "Ruhig, freundlich und alltagstauglich für kleine Kundinnen und Kunden.",
text: "Geduldig, freundlich und schnell damit der Kinderschnitt für alle entspannt wird und das Ergebnis im Alltag hält.",
price: "ab ca. 15 €",
},
],
hours: [
@@ -145,26 +156,53 @@ export const siteContent = {
],
reviewSummary: {
rating: "4,8 / 5",
count: "rund 89 Google-Bewertungen",
text: "Mehrere Brancheneinträge führen Studio Haarscharf mit sehr guter lokaler Bewertung.",
count: "89 Google-Bewertungen",
text: "Kunden bewerten uns bei Google mit 4,8 von 5 Sternen. Lesen Sie selbst, was sie schreiben.",
url: "https://www.google.com/search?q=haarscharf+crimmitschau#reviews",
},
reviews: [
{
author: "Alexander Nönnig",
rating: 5,
text: "Ohne Termin trotzdem innerhalb von 5 Minuten dran gekommen. Es geht viel, wenn man nur will. Mit dem Ergebnis auch sehr zufrieden gewesen. Super, weiter so!",
date: "vor 3 Jahren",
},
{
author: "Sven Müller",
rating: 5,
text: "Kam auch sofort ohne Termin dran und hat alles gepasst.",
date: "vor einem Jahr",
},
{
author: "Steffen K.",
rating: 5,
text: "Die Besuche bei Haarscharf sind stets fantastisch! Das Team zeigte nicht nur ein herausragendes handwerkliches Können, sondern schafft auch immer eine warme und angenehme Atmosphäre.",
date: "vor 2 Jahren",
},
{
author: "Leoni Preuss",
rating: 5,
text: "Ich habe für meinen Abiball eine wunderschöne, einzigartige, schnelle und preisgünstige Hochsteckfrisur bekommen und bin super zufrieden. Neben guter Laune sind auch die Ergebnisse top!",
date: "vor 3 Jahren",
},
],
team: [
{
name: "Claudia Schäfer",
role: "Salonleitung & Stylistin",
bio: "Über 20 Jahre Erfahrung in Schnitt, Farbe und Styling. Spezialisiert auf typgerechte Beratung und festliche Frisuren.",
bio: "Claudia führt den Salon seit über 20 Jahren. Ihre Stärke: Sie hört zu, berät ehrlich und sorgt dafür, dass Sie den Salon mit einem Lächeln verlassen.",
initials: "CS",
},
{
name: "Maria Klein",
role: "Coloristin & Stylistin",
bio: "Expertin für Farbverläufe, Coloration und kreative Nuancen. Liebt es, mit natürlichen Tönen zu arbeiten.",
bio: "Maria verwandelt Wünsche in Farbe. Ihr Spezialgebiet: natürliche Nuancen und sanfte Verläufe, die sich nicht nur gut ansehen, sondern auch beim Nachwachsen schön aussehen.",
initials: "MK",
},
{
name: "Sophie Weber",
role: "Auszubildende",
bio: "Im 2. Ausbildungsjahr zur Friseurin. Bringt frische Ideen und moderne Styling-Trends mit ins Team.",
bio: "Sophie ist im 2. Lehrjahr und bringt frische Ideen aus der Ausbildung mit. Sie sorgt dafür, dass Sie sich vom ersten Moment an wohlfühlen.",
initials: "SW",
},
],

View File

@@ -1,5 +1,6 @@
---
import "@/styles/global.css"
import Footer from "@/components/Footer.astro"
const {
title = "Haarscharf Crimmitschau | Friseur in der Annenstraße",
@@ -19,7 +20,10 @@ const {
<meta property="og:type" content="website" />
<title>{title}</title>
</head>
<body>
<body class="flex min-h-screen flex-col">
<slot />
<div class="mt-auto">
<Footer />
</div>
</body>
</html>

View File

@@ -0,0 +1,10 @@
---
import Layout from "@/layouts/main.astro"
---
<Layout title="Datenschutz | Haarscharf Crimmitschau">
<main class="mx-auto max-w-3xl px-6 py-16">
<h1 class="mb-8 text-3xl font-semibold text-stone-900">Datenschutzerklärung</h1>
<p class="text-stone-600">Hier folgt die Datenschutzerklärung.</p>
</main>
</Layout>

10
src/pages/impressum.astro Normal file
View File

@@ -0,0 +1,10 @@
---
import Layout from "@/layouts/main.astro"
---
<Layout title="Impressum | Haarscharf Crimmitschau">
<main class="mx-auto max-w-3xl px-6 py-16">
<h1 class="mb-8 text-3xl font-semibold text-stone-900">Impressum</h1>
<p class="text-stone-600">Angaben gemäß § 5 TMG</p>
</main>
</Layout>

View File

@@ -1,850 +1,38 @@
---
import Layout from "@/layouts/main.astro"
import { siteContent } from "@/content/site-content"
const navItems = [
{ label: "Leistungen", href: "#leistungen" },
{ label: "Team", href: "#team" },
{ label: "Zeiten", href: "#zeiten" },
{ label: "Kontakt", href: "#kontakt" },
]
import SiteHeader from "@/components/sections/SiteHeader.astro"
import Hero from "@/components/sections/Hero.astro"
import SalonPromise from "@/components/sections/SalonPromise.astro"
import EditorialImage from "@/components/sections/EditorialImage.astro"
import Services from "@/components/sections/Services.astro"
import Team from "@/components/sections/Team.astro"
import Reviews from "@/components/sections/Reviews.astro"
import Hours from "@/components/sections/Hours.astro"
import Trust from "@/components/sections/Trust.astro"
import Contact from "@/components/sections/Contact.astro"
import FinalCta from "@/components/sections/FinalCta.astro"
import "@/styles/home.css"
---
<Layout title="Haarscharf Crimmitschau | Friseur in der Annenstraße">
<a class="skip-link" href="#top">Zum Inhalt springen</a>
<header class="site-header">
<a class="brand-lockup" href="#top" aria-label="Haarscharf Startseite">
<span class="brand-mark">H</span>
<span>
<span class="block font-heading text-lg leading-none">{siteContent.business.name}</span>
<span class="brand-descriptor">{siteContent.business.descriptor}</span>
</span>
</a>
<nav aria-label="Hauptnavigation" class="hidden items-center gap-1 md:flex">
{navItems.map((item) => <a class="nav-link" href={item.href}>{item.label}</a>)}
</nav>
<div class="flex items-center gap-2">
<button
class="mobile-nav-toggle md:hidden"
aria-expanded="false"
aria-controls="mobile-nav"
aria-label="Menü öffnen"
type="button"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
<line x1="3" y1="5" x2="17" y2="5" />
<line x1="3" y1="10" x2="17" y2="10" />
<line x1="3" y1="15" x2="17" y2="15" />
</svg>
</button>
<a class="button-link button-link--compact" href={siteContent.cta.primary.href}>
Anrufen
</a>
</div>
</header>
<div id="mobile-nav" class="mobile-nav" aria-label="Mobile Navigation" role="dialog" aria-modal="true">
<div class="mobile-nav-backdrop"></div>
<div class="mobile-nav-panel">
<button class="mobile-nav-close" aria-label="Menü schließen" type="button">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
<line x1="4" y1="4" x2="16" y2="16" />
<line x1="16" y1="4" x2="4" y2="16" />
</svg>
</button>
<ul class="mobile-nav-list">
{navItems.map((item) => (
<li><a class="mobile-nav-link" href={item.href}>{item.label}</a></li>
))}
</ul>
<a class="mobile-nav-phone" href={siteContent.business.phone.href}>
<span class="contact-label">Telefon</span>
<strong>{siteContent.business.phone.display}</strong>
</a>
</div>
</div>
<SiteHeader />
<main>
<!-- Hero: text-only, centered, warm typography -->
<section id="top" class="hero-section hero-section--text-only" aria-labelledby="hero-title">
<div class="hero-copy hero-copy--centered">
<p class="eyebrow">{siteContent.hero.eyebrow}</p>
<h1 id="hero-title">{siteContent.hero.title}</h1>
<p class="hero-intro">{siteContent.hero.intro}</p>
<div class="hero-actions hero-actions--centered">
<a class="button-link" href={siteContent.cta.primary.href}>
{siteContent.cta.primary.label}
</a>
<a class="button-link button-link--secondary" href={siteContent.cta.secondary.href} target="_blank" rel="noreferrer">
{siteContent.cta.secondary.label}
</a>
</div>
<dl class="salon-meta salon-meta--centered" aria-label="Saloninformationen">
<div>
<dt>Adresse</dt>
<dd>{siteContent.business.address.display}</dd>
</div>
<div>
<dt>Telefon</dt>
<dd>{siteContent.business.phone.display}</dd>
</div>
<div>
<dt>{siteContent.hours[0].day}</dt>
<dd>{siteContent.hours[0].time}</dd>
</div>
</dl>
</div>
</section>
<!-- Salon Promise -->
<section class="section-shell promise-section cut-line" aria-labelledby="promise-title">
<div class="promise-intro">
<p class="eyebrow">Salonversprechen</p>
<h2 id="promise-title">Haare dürfen leicht aussehen, auch wenn dahinter Präzision steckt.</h2>
</div>
<div class="promise-grid">
{
siteContent.promise.map((item, index) => (
<article class="promise-card">
<span class="index-label">{String(index + 1).padStart(2, "0")}</span>
<h3>{item.title}</h3>
<p>{item.text}</p>
</article>
))
}
</div>
</section>
<!-- Editorial image break: salon atmosphere -->
<figure class="editorial-image">
<img
src="https://images.unsplash.com/photo-1562322140-8baeececf3df?auto=format&fit=crop&w=1600&q=80"
alt="Warm beleuchteter Salon-Innenraum mit Friseurstühlen und Spiegeln"
width="1600"
height="900"
loading="lazy"
decoding="async"
/>
<figcaption>Atmosphäre im Salon</figcaption>
</figure>
<!-- Services: Grid instead of list -->
<section id="leistungen" class="section-shell services-section" aria-labelledby="services-title">
<div class="section-heading section-heading--centered">
<p class="eyebrow">Leistungen</p>
<h2 id="services-title">Alles, was ein guter Salonbesuch braucht.</h2>
<p>Die Auswahl bleibt bewusst klar. Details, Wünsche und Termine werden am besten direkt telefonisch besprochen.</p>
</div>
<div class="service-grid">
{
siteContent.services.map((service, index) => (
<article class="service-card">
<span class="service-index">{String(index + 1).padStart(2, "0")}</span>
<h3>{service.title}</h3>
<p>{service.text}</p>
</article>
))
}
</div>
</section>
<!-- Team: adapted from /2.astro -->
<section id="team" class="section-shell team-section cut-line" aria-labelledby="team-title">
<div class="section-heading section-heading--centered">
<p class="eyebrow">Unser Team</p>
<h2 id="team-title">Die Menschen hinter den Scheren.</h2>
</div>
<figure class="team-image">
<img
src="https://images.unsplash.com/photo-1711274093746-b588a17d2716?auto=format&fit=crop&w=1200&q=82"
alt="Eine Friseurin bei der Arbeit mit Schere"
width="1200"
height="600"
loading="lazy"
decoding="async"
/>
<figcaption>Handwerk mit Präzision</figcaption>
</figure>
<div class="team-grid">
{
siteContent.team.map((member) => (
<article class="team-card">
<div class="team-avatar">
<span>{member.initials}</span>
</div>
<h3>{member.name}</h3>
<p class="team-role">{member.role}</p>
<p>{member.bio}</p>
</article>
))
}
</div>
</section>
<!-- Hours -->
<section id="zeiten" class="section-shell hours-section cut-line" aria-labelledby="hours-title">
<div class="section-heading">
<p class="eyebrow">Öffnungszeiten</p>
<h2 id="hours-title">Planbar von Montag bis Samstag.</h2>
<p>Für Termine bitte kurz anrufen. So lässt sich direkt klären, wann genug Zeit für Beratung, Schnitt oder Farbe frei ist.</p>
</div>
<div class="hours-panel">
{
siteContent.hours.map((entry) => (
<div class="hours-row">
<span>{entry.day}</span>
<time>{entry.time}</time>
</div>
))
}
</div>
</section>
<!-- Trust -->
<section class="trust-band" aria-labelledby="trust-title">
<div>
<p class="eyebrow">Lokales Vertrauen</p>
<h2 id="trust-title">{siteContent.reviewSummary.rating}</h2>
</div>
<p class="trust-copy">
<strong>{siteContent.reviewSummary.count}</strong>
<span>{siteContent.reviewSummary.text}</span>
</p>
</section>
<!-- Contact: with labels adapted from /2.astro -->
<section id="kontakt" class="section-shell contact-section" aria-labelledby="contact-title">
<div class="section-heading">
<p class="eyebrow">Kontakt & Anfahrt</p>
<h2 id="contact-title">Mitten in Crimmitschau.</h2>
<p class="contact-note">Der schnellste Weg zum Termin bleibt ein kurzer Anruf im Salon.</p>
</div>
<div class="contact-layout">
<div class="contact-left">
<address class="contact-address">
<span>{siteContent.business.name}</span>
<span>{siteContent.business.address.street}</span>
<span>{siteContent.business.address.postalCode} {siteContent.business.address.city}</span>
</address>
<figure class="contact-image">
<img
src="https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?auto=format&fit=crop&w=800&q=80"
alt="Friseur-Werkzeuge: Schere und Kamm auf einer Marmor-Arbeitsfläche"
width="800"
height="600"
loading="lazy"
decoding="async"
/>
<figcaption>Detail &amp; Sorgfalt</figcaption>
</figure>
</div>
<div class="contact-actions contact-card">
<a class="contact-line" href={siteContent.business.phone.href}>
<span class="contact-label">Telefon</span>
<strong>{siteContent.business.phone.display}</strong>
</a>
<a class="contact-line" href={`mailto:${siteContent.business.email}`}>
<span class="contact-label">E-Mail</span>
<strong>{siteContent.business.email}</strong>
</a>
<a class="button-link" href={siteContent.cta.secondary.href} target="_blank" rel="noreferrer">
Route über Google Maps
</a>
</div>
</div>
</section>
<!-- Final CTA -->
<section class="final-cta" aria-labelledby="final-cta-title">
<p class="eyebrow">Bereit für den nächsten Schnitt?</p>
<h2 id="final-cta-title">Kurz anrufen, Termin abstimmen, vorbeikommen.</h2>
<div class="final-cta-actions">
<a class="button-link button-link--light" href={siteContent.cta.primary.href}>
{siteContent.business.phone.display}
</a>
</div>
</section>
<Hero />
<SalonPromise />
<EditorialImage />
<Services />
<Team />
<Reviews />
<Hours />
<Trust />
<Contact />
<FinalCta />
</main>
<script is:inline>
(function() {
const toggle = document.querySelector('.mobile-nav-toggle');
const nav = document.querySelector('#mobile-nav');
if (!toggle || !nav) return;
const closeBtn = nav.querySelector('.mobile-nav-close');
const backdrop = nav.querySelector('.mobile-nav-backdrop');
const links = nav.querySelectorAll('a');
function open() {
nav.classList.add('is-open');
toggle.setAttribute('aria-expanded', 'true');
document.body.style.overflow = 'hidden';
if (links.length) {
setTimeout(() => links[0].focus(), 0);
}
document.addEventListener('keydown', onKeyDown);
}
function close() {
nav.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
document.body.style.overflow = '';
document.removeEventListener('keydown', onKeyDown);
toggle.focus();
}
function onKeyDown(e) {
if (e.key === 'Escape') close();
}
toggle.addEventListener('click', open);
if (closeBtn) closeBtn.addEventListener('click', close);
if (backdrop) backdrop.addEventListener('click', close);
links.forEach(link => link.addEventListener('click', close));
})();
<script>
import "@/scripts/scroll-reveal";
</script>
</Layout>
<style>
/* ─── Hero: text-only, centered ─── */
.hero-section--text-only {
position: relative;
display: grid;
place-items: center;
min-height: 92svh;
overflow: hidden;
padding: clamp(7rem, 14vw, 10rem) clamp(1rem, 5vw, 5rem) clamp(3.75rem, 7vw, 6rem);
background:
linear-gradient(180deg, color-mix(in oklch, var(--surface-warm) 55%, transparent) 0%, var(--background) 40%);
color: var(--foreground);
text-align: center;
}
.hero-section--text-only::after {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 80% 50% at 50% 120%, color-mix(in oklch, var(--secondary) 25%, transparent) 0%, transparent 70%);
pointer-events: none;
}
.hero-copy--centered {
position: relative;
z-index: 2;
align-self: end;
max-width: 58rem;
margin: 0 auto;
}
.hero-copy--centered .eyebrow {
color: var(--accent);
}
.hero-copy--centered h1 {
max-width: 18ch;
margin: 1rem auto 1.4rem;
font-family: var(--font-heading);
font-size: clamp(3.4rem, 9vw, 7.5rem);
font-weight: 600;
line-height: 1.0;
color: var(--foreground);
}
.hero-copy--centered .hero-intro {
max-width: 42ch;
margin: 0 auto;
color: var(--muted-foreground);
font-size: clamp(1.15rem, 2vw, 1.45rem);
line-height: 1.6;
}
.hero-actions--centered {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
padding-top: 1.35rem;
}
.hero-actions--centered .button-link--secondary {
border-color: var(--foreground);
color: var(--foreground);
}
.hero-actions--centered .button-link--secondary:hover,
.hero-actions--centered .button-link--secondary:focus-visible {
background: var(--foreground);
border-color: var(--foreground);
color: var(--background);
}
.salon-meta--centered {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0;
max-width: 50rem;
margin: clamp(2.5rem, 5vw, 4rem) auto 0;
border-top: 1px solid var(--hairline);
border-bottom: 1px solid var(--hairline);
color: var(--foreground);
}
.salon-meta--centered div {
display: grid;
gap: 0.25rem;
padding: 1rem clamp(0.8rem, 2vw, 1.2rem);
}
.salon-meta--centered div + div {
border-left: 1px solid var(--hairline);
}
.salon-meta--centered dt {
color: var(--muted-foreground);
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0;
text-transform: uppercase;
}
.salon-meta--centered dd {
margin: 0;
font-weight: 700;
line-height: 1.25;
}
/* ─── Services: grid layout ─── */
.service-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 0;
margin-top: clamp(2rem, 4vw, 4rem);
border-top: 1px solid oklch(0.62 0.042 49 / 45%);
border-left: 1px solid oklch(0.62 0.042 49 / 45%);
}
.service-card {
display: grid;
gap: 0.75rem;
border-bottom: 1px solid oklch(0.62 0.042 49 / 45%);
border-right: 1px solid oklch(0.62 0.042 49 / 45%);
padding: clamp(1.5rem, 3vw, 2.5rem);
transition: background-color 180ms ease;
}
.service-card:hover {
background: color-mix(in oklch, oklch(0.62 0.042 49 / 45%) 12%, transparent);
}
.service-card .service-index {
color: oklch(0.82 0.104 65);
font-family: var(--font-heading);
font-size: clamp(2rem, 4vw, 4rem);
line-height: 0.85;
}
.service-card h3 {
font-family: var(--font-heading);
font-size: clamp(1.3rem, 2vw, 1.65rem);
line-height: 1.15;
margin: 0;
}
.service-card p {
color: oklch(0.84 0.019 83);
line-height: 1.65;
margin: 0;
}
/* ─── Team section ─── */
.team-section {
background: var(--surface-warm);
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 0;
margin-top: clamp(2rem, 4vw, 4rem);
border-top: 1px solid var(--hairline);
border-left: 1px solid var(--hairline);
}
.team-card {
display: grid;
justify-items: center;
gap: 0.625rem;
border-bottom: 1px solid var(--hairline);
border-right: 1px solid var(--hairline);
padding: clamp(2rem, 4vw, 3rem) clamp(1.5rem, 3vw, 2.5rem);
text-align: center;
transition: background-color 180ms ease;
}
.team-card:hover {
background: color-mix(in oklch, var(--background) 55%, transparent);
}
.team-avatar {
display: grid;
place-items: center;
width: 5rem;
height: 5rem;
margin-bottom: 0.75rem;
border-radius: 999px;
background: var(--muted);
font-family: var(--font-heading);
font-size: 1.25rem;
font-weight: 600;
color: var(--muted-foreground);
}
.team-card h3 {
font-family: var(--font-heading);
font-size: clamp(1.2rem, 1.8vw, 1.5rem);
line-height: 1.2;
margin: 0;
}
.team-role {
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
color: var(--accent);
margin: 0;
}
.team-card > p:last-child {
color: var(--muted-foreground);
font-size: 0.95rem;
line-height: 1.65;
margin: 0;
}
/* ─── Contact labels ─── */
.contact-label {
display: block;
color: var(--muted-foreground);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
/* ─── Editorial image break ─── */
.editorial-image {
margin: 0;
position: relative;
width: 100%;
height: clamp(18rem, 45vw, 32rem);
overflow: hidden;
border-radius: var(--radius);
}
.editorial-image img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
.editorial-image figcaption {
position: absolute;
right: clamp(1rem, 4vw, 3rem);
bottom: 1rem;
z-index: 2;
padding: 0.25rem 0.6rem;
border-radius: var(--radius-sm);
background: oklch(0.1 0.02 28 / 55%);
color: oklch(0.95 0.01 84);
font-size: 0.78rem;
text-shadow: none;
}
/* ─── Team image ─── */
.team-image {
margin: clamp(1.5rem, 3vw, 2.5rem) auto 0;
max-width: var(--measure);
position: relative;
border-radius: var(--radius);
overflow: hidden;
}
.team-image img {
width: 100%;
height: auto;
aspect-ratio: 2 / 1;
object-fit: cover;
display: block;
}
.team-image figcaption {
position: absolute;
right: 1rem;
bottom: 0.75rem;
color: oklch(0.95 0.01 84);
font-size: 0.78rem;
text-shadow: 0 1px 3px oklch(0.1 0.02 28 / 50%);
}
/* ─── Contact image ─── */
.contact-left {
display: grid;
gap: clamp(1.5rem, 3vw, 2.5rem);
align-content: start;
}
.contact-image {
margin: 0;
border-radius: var(--radius);
overflow: hidden;
}
.contact-image img {
width: 100%;
height: auto;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
}
.contact-image figcaption {
color: var(--muted-foreground);
font-size: 0.82rem;
margin-top: 0.375rem;
text-align: center;
}
/* ─── Centered section headings ─── */
.section-heading--centered {
text-align: center;
margin: 0 auto;
}
.section-heading--centered h2 {
margin: 0 auto;
}
.section-heading--centered p:not(.eyebrow) {
margin: 0 auto;
}
/* ─── Mobile overrides ─── */
@media (max-width: 820px) {
.hero-section--text-only {
min-height: 94svh;
}
.hero-copy--centered h1 {
font-size: clamp(2.6rem, 10vw, 4rem);
}
.salon-meta--centered {
grid-template-columns: 1fr;
}
.salon-meta--centered div + div {
border-top: 1px solid var(--hairline);
border-left: 0;
}
.service-grid {
grid-template-columns: 1fr;
}
.team-grid {
grid-template-columns: 1fr;
}
}
/* ─── Header override for light hero ─── */
.site-header {
color: var(--foreground);
}
.site-header .nav-link {
color: inherit;
}
.site-header .nav-link:hover,
.site-header .nav-link:focus-visible {
background: color-mix(in oklch, var(--foreground) 10%, transparent);
color: var(--foreground);
}
.site-header .button-link--compact {
background: color-mix(in oklch, var(--foreground) 10%, transparent);
border-color: color-mix(in oklch, var(--foreground) 35%, transparent);
color: var(--foreground);
}
.site-header .button-link--compact:hover,
.site-header .button-link--compact:focus-visible {
background: var(--foreground);
border-color: var(--foreground);
color: var(--background);
}
/* ─── Mobile navigation ─── */
.mobile-nav-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border: 1px solid color-mix(in oklch, var(--foreground) 25%, transparent);
border-radius: var(--radius);
background: color-mix(in oklch, var(--foreground) 8%, transparent);
color: var(--foreground);
cursor: pointer;
transition: background-color 180ms ease, border-color 180ms ease;
}
.mobile-nav-toggle:hover,
.mobile-nav-toggle:focus-visible {
background: color-mix(in oklch, var(--foreground) 16%, transparent);
border-color: color-mix(in oklch, var(--foreground) 45%, transparent);
}
@media (min-width: 821px) {
.mobile-nav-toggle {
display: none;
}
}
.mobile-nav {
position: fixed;
inset: 0;
z-index: 60;
pointer-events: none;
visibility: hidden;
}
.mobile-nav.is-open {
pointer-events: auto;
visibility: visible;
}
.mobile-nav-backdrop {
position: absolute;
inset: 0;
background: oklch(0.1 0.02 28 / 55%);
opacity: 0;
transition: opacity 200ms ease;
}
.mobile-nav.is-open .mobile-nav-backdrop {
opacity: 1;
}
.mobile-nav-panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: min(20rem, 85vw);
background: var(--background);
color: var(--foreground);
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 1.5rem;
transform: translateX(100%);
transition: transform 220ms ease;
box-shadow: -4px 0 24px oklch(0.1 0.02 28 / 12%);
}
.mobile-nav.is-open .mobile-nav-panel {
transform: translateX(0);
}
.mobile-nav-close {
align-self: flex-end;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border: 1px solid var(--hairline);
border-radius: var(--radius);
background: transparent;
color: var(--foreground);
cursor: pointer;
transition: background-color 180ms ease;
}
.mobile-nav-close:hover,
.mobile-nav-close:focus-visible {
background: var(--muted);
}
.mobile-nav-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.mobile-nav-link {
display: block;
padding: 0.85rem 0.5rem;
border-radius: var(--radius);
color: var(--foreground);
font-size: 1.25rem;
font-weight: 600;
text-decoration: none;
transition: background-color 180ms ease;
min-height: 44px;
line-height: 1.2;
}
.mobile-nav-link:hover,
.mobile-nav-link:focus-visible {
background: var(--muted);
}
.mobile-nav-phone {
margin-top: auto;
display: grid;
gap: 0.3rem;
padding: 1rem;
border-radius: var(--radius);
border: 1px solid var(--hairline);
background: var(--card);
color: var(--foreground);
text-decoration: none;
transition: background-color 180ms ease, border-color 180ms ease;
}
.mobile-nav-phone:hover,
.mobile-nav-phone:focus-visible {
background: var(--muted);
border-color: var(--primary);
}
.mobile-nav-phone strong {
font-size: 1.35rem;
font-weight: 700;
}
</style>

36
src/scripts/mobile-nav.ts Normal file
View File

@@ -0,0 +1,36 @@
(function () {
const toggle = document.querySelector(".mobile-nav-toggle") as HTMLElement | null;
const nav = document.querySelector("#mobile-nav") as HTMLElement | null;
if (!toggle || !nav) return;
const closeBtn = nav.querySelector(".mobile-nav-close");
const backdrop = nav.querySelector(".mobile-nav-backdrop");
const links = nav.querySelectorAll("a");
function open() {
nav!.classList.add("is-open");
toggle!.setAttribute("aria-expanded", "true");
document.body.style.overflow = "hidden";
if (links.length) {
setTimeout(() => (links[0] as HTMLElement).focus(), 0);
}
document.addEventListener("keydown", onKeyDown);
}
function close() {
nav!.classList.remove("is-open");
toggle!.setAttribute("aria-expanded", "false");
document.body.style.overflow = "";
document.removeEventListener("keydown", onKeyDown);
toggle!.focus();
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") close();
}
toggle.addEventListener("click", open);
if (closeBtn) closeBtn.addEventListener("click", close);
if (backdrop) backdrop.addEventListener("click", close);
links.forEach((link) => link.addEventListener("click", close));
})();

View File

@@ -0,0 +1,41 @@
(function () {
var revealSelectors = [
".promise-v2__intro",
".promise-v2__card",
".editorial-v2",
".section-heading-v2",
".services-v2__card",
".team-image-v2",
".team-v2__card",
".reviews-v2__card",
".reviews-v2__footer",
".hours-v2__row",
".trust-v2__rating",
".trust-v2__copy",
".contact-v2__left",
".contact-v2__actions",
".final-cta-v2",
];
var revealElements = document.querySelectorAll(revealSelectors.join(","));
if (!revealElements.length) return;
var observer = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.12,
rootMargin: "0px 0px -40px 0px",
}
);
revealElements.forEach(function (el) {
observer.observe(el);
});
})();

View File

@@ -179,6 +179,38 @@
gap: 1rem;
padding: clamp(0.8rem, 2vw, 1.25rem) clamp(1rem, 4vw, 3rem);
color: var(--hero-ink);
transition: background-color 200ms ease, box-shadow 200ms ease, color 200ms ease;
}
.site-header.is-scrolled {
background: color-mix(in oklch, var(--background) 92%, transparent);
backdrop-filter: blur(12px) saturate(140%);
-webkit-backdrop-filter: blur(12px) saturate(140%);
box-shadow: 0 1px 0 var(--hairline);
color: var(--foreground);
}
.site-header.is-scrolled .brand-descriptor {
color: var(--muted-foreground);
}
.site-header.is-scrolled .nav-link:hover,
.site-header.is-scrolled .nav-link:focus-visible {
background: var(--muted);
color: var(--foreground);
}
.site-header.is-scrolled .button-link--compact {
background: var(--primary);
border-color: var(--primary);
color: var(--primary-foreground);
}
.site-header.is-scrolled .button-link--compact:hover,
.site-header.is-scrolled .button-link--compact:focus-visible {
background: oklch(0.28 0.094 18);
border-color: oklch(0.28 0.094 18);
color: oklch(0.98 0.012 84);
}
.brand-lockup {

344
src/styles/home.css Normal file
View File

@@ -0,0 +1,344 @@
/* ─── V2 Design Tokens — BOLDER ─── */
:root {
/* Richer, warmer palette with more chroma */
--v2-bg: oklch(0.96 0.018 78);
--v2-fg: oklch(0.11 0.04 38);
--v2-primary: oklch(0.33 0.16 42);
--v2-primary-dim: oklch(0.26 0.13 42);
--v2-accent: oklch(0.72 0.18 58);
--v2-muted: oklch(0.48 0.04 42);
--v2-surface: oklch(0.93 0.025 75);
--v2-surface-dark: oklch(0.16 0.05 45);
--v2-surface-warm: oklch(0.92 0.03 72);
--v2-hairline: color-mix(in oklch, var(--v2-fg) 14%, transparent);
--v2-hero-ink: oklch(0.98 0.012 82);
--v2-radius: 0.25rem;
/* Stronger shadow scale */
--shadow-sm: 0 1px 2px oklch(0.2 0.04 45 / 8%);
--shadow-md: 0 4px 12px oklch(0.2 0.04 45 / 10%);
--shadow-lg: 0 12px 40px oklch(0.2 0.04 45 / 14%);
--shadow-xl: 0 24px 80px oklch(0.2 0.04 45 / 18%);
/* Animation */
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}
/* ─── Subtle paper texture on body ─── */
body {
background:
var(--v2-bg)
url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.025'/%3E%3C/svg%3E");
background-size: 200px 200px;
color: var(--v2-fg);
}
/* ─── Animation Keyframes ─── */
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.92);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* ─── Shared heading scale — amplified ─── */
.section-heading-v2 h2,
.promise-v2__intro h2,
.final-cta-v2 h2 {
font-family: var(--font-heading);
font-size: clamp(3rem, 7vw, 7rem);
font-weight: 600;
line-height: 0.92;
margin: 0;
letter-spacing: -0.01em;
}
/* ─── Buttons — bolder presence ─── */
.button-link-v2 {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 3rem;
width: fit-content;
border: 2px solid var(--v2-primary);
border-radius: var(--v2-radius);
background: var(--v2-primary);
color: var(--v2-hero-ink);
font-weight: 700;
line-height: 1;
padding: 0.9rem 1.6rem;
text-decoration: none;
transition: transform 220ms var(--ease-out-expo), background-color 180ms ease, border-color 180ms ease, box-shadow 220ms var(--ease-out-expo);
box-shadow: var(--shadow-sm);
}
.button-link-v2:hover,
.button-link-v2:focus-visible {
background: var(--v2-primary-dim);
border-color: var(--v2-primary-dim);
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
.button-link-v2--compact {
min-height: 2.6rem;
padding: 0.6rem 1.1rem;
background: transparent;
border-color: color-mix(in oklch, currentColor 45%, transparent);
color: inherit;
box-shadow: none;
}
.button-link-v2--compact:hover,
.button-link-v2--compact:focus-visible {
background: var(--v2-fg);
border-color: var(--v2-fg);
color: var(--v2-bg);
box-shadow: var(--shadow-md);
}
.button-link-v2--ghost {
background: transparent;
color: var(--v2-fg);
border-color: var(--v2-fg);
box-shadow: none;
}
.button-link-v2--ghost:hover,
.button-link-v2--ghost:focus-visible {
background: var(--v2-fg);
color: var(--v2-bg);
box-shadow: var(--shadow-md);
}
.button-link-v2--light {
background: var(--v2-hero-ink);
border-color: var(--v2-hero-ink);
color: var(--v2-primary);
}
.button-link-v2--light:hover,
.button-link-v2--light:focus-visible {
background: var(--v2-accent);
border-color: var(--v2-accent);
color: var(--v2-fg);
}
/* Micro-interactions */
.button-link-v2:active {
transform: scale(0.97) translateY(-1px);
transition-duration: 100ms;
box-shadow: var(--shadow-sm);
}
.button-link-v2--compact:active,
.button-link-v2--ghost:active,
.button-link-v2--light:active {
transform: scale(0.97);
transition-duration: 100ms;
}
/* ─── Eyebrow — more presence ─── */
.eyebrow-v2 {
color: var(--v2-primary);
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.08em;
margin: 0;
text-transform: uppercase;
}
/* ─── Sections ─── */
.section-v2 {
padding: clamp(5rem, 11vw, 11rem) clamp(1rem, 5vw, 5rem);
}
.section-heading-v2 {
display: grid;
gap: 1.1rem;
max-width: 46rem;
}
.section-heading-v2--centered {
text-align: center;
margin: 0 auto;
}
.section-heading-v2 p:not(.eyebrow-v2) {
color: var(--v2-muted);
font-size: clamp(1.05rem, 1.6vw, 1.3rem);
line-height: 1.65;
margin: 0;
}
/* ─── Scroll-Triggered Reveals ─── */
.promise-v2__intro,
.promise-v2__card,
.editorial-v2,
.section-heading-v2,
.services-v2__card,
.team-image-v2,
.team-v2__card,
.hours-v2__row,
.trust-v2__rating,
.trust-v2__copy,
.reviews-v2__card,
.reviews-v2__footer,
.contact-v2__left,
.contact-v2__actions,
.final-cta-v2 {
opacity: 0;
transform: translateY(32px);
transition: opacity 0.8s var(--ease-out-quart), transform 0.8s var(--ease-out-quart);
}
.promise-v2__intro.is-visible,
.promise-v2__card.is-visible,
.editorial-v2.is-visible,
.section-heading-v2.is-visible,
.services-v2__card.is-visible,
.team-image-v2.is-visible,
.team-v2__card.is-visible,
.reviews-v2__card.is-visible,
.reviews-v2__footer.is-visible,
.hours-v2__row.is-visible,
.trust-v2__rating.is-visible,
.trust-v2__copy.is-visible,
.contact-v2__left.is-visible,
.contact-v2__actions.is-visible,
.final-cta-v2.is-visible {
opacity: 1;
transform: translateY(0);
}
/* Stagger delays */
.promise-v2__card:nth-child(1) { transition-delay: 0ms; }
.promise-v2__card:nth-child(2) { transition-delay: 120ms; }
.promise-v2__card:nth-child(3) { transition-delay: 240ms; }
.services-v2__card:nth-child(1) { transition-delay: 0ms; }
.services-v2__card:nth-child(2) { transition-delay: 140ms; }
.services-v2__card:nth-child(3) { transition-delay: 280ms; }
.services-v2__card:nth-child(4) { transition-delay: 420ms; }
.team-v2__card:nth-child(1) { transition-delay: 0ms; }
.team-v2__card:nth-child(2) { transition-delay: 120ms; }
.team-v2__card:nth-child(3) { transition-delay: 240ms; }
.hours-v2__row:nth-child(1) { transition-delay: 0ms; }
.hours-v2__row:nth-child(2) { transition-delay: 70ms; }
.hours-v2__row:nth-child(3) { transition-delay: 140ms; }
.hours-v2__row:nth-child(4) { transition-delay: 210ms; }
.hours-v2__row:nth-child(5) { transition-delay: 280ms; }
.hours-v2__row:nth-child(6) { transition-delay: 350ms; }
/* Trust number dramatic entrance — amplified */
.trust-v2__rating h2 {
transition: transform 1s var(--ease-out-expo), opacity 0.8s var(--ease-out-quart);
transform: scale(0.85);
opacity: 0;
}
.trust-v2__rating.is-visible h2 {
transform: scale(1);
opacity: 1;
}
/* Reviews stagger */
.reviews-v2__card:nth-child(1) { transition-delay: 0ms; }
.reviews-v2__card:nth-child(2) { transition-delay: 120ms; }
.reviews-v2__card:nth-child(3) { transition-delay: 240ms; }
.reviews-v2__card:nth-child(4) { transition-delay: 360ms; }
/* Reduced motion: ensure visibility */
@media (prefers-reduced-motion: reduce) {
.hero-deco,
.hero-v2__copy .eyebrow-v2,
.hero-v2__copy h1,
.hero-v2__intro,
.hero-v2__actions,
.hero-v2__meta,
.promise-v2__intro,
.promise-v2__card,
.editorial-v2,
.section-heading-v2,
.services-v2__card,
.team-image-v2,
.team-v2__card,
.hours-v2__row,
.trust-v2__rating,
.trust-v2__copy,
.reviews-v2__card,
.reviews-v2__footer,
.contact-v2__left,
.contact-v2__actions,
.final-cta-v2 {
opacity: 1;
transform: none;
animation: none;
transition: none;
}
.trust-v2__rating h2 {
transform: none;
opacity: 1;
}
}
/* ─── Mobile overrides ─── */
@media (max-width: 820px) {
.section-heading-v2 h2,
.promise-v2__intro h2,
.final-cta-v2 h2 {
font-size: clamp(2.4rem, 9vw, 4.5rem);
}
.promise-v2,
.hours-v2,
.trust-v2,
.contact-v2__layout {
grid-template-columns: 1fr;
}
.promise-v2__intro {
position: static;
}
.services-v2__grid {
grid-template-columns: 1fr;
}
.services-v2__card:nth-child(even) {
margin-top: 0;
}
.trust-v2 h2 {
font-size: clamp(5rem, 20vw, 9rem);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}