feat: single-page redesign with text-only hero, team section, services grid, mobile nav

- Replace component-based index with unified inline page
- Add text-only hero with warm editorial typography
- Add team section with avatars and bios
- Convert services to responsive grid layout
- Add contact labels and editorial images
- Implement mobile hamburger navigation with slide-in panel
- Polish typography, spacing, and accessibility
- Remove design variant pages (1-6)
This commit is contained in:
2026-05-17 17:40:50 +02:00
parent 3b34a7b2a4
commit 90c8762eb7
18 changed files with 2067 additions and 51 deletions

View File

@@ -1,17 +1,850 @@
---
import Layout from "@/layouts/main.astro"
import { Button } from "@/components/ui/button"
import { siteContent } from "@/content/site-content"
const navItems = [
{ label: "Leistungen", href: "#leistungen" },
{ label: "Team", href: "#team" },
{ label: "Zeiten", href: "#zeiten" },
{ label: "Kontakt", href: "#kontakt" },
]
---
<Layout>
<div class="flex min-h-svh p-6">
<div class="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
<div>
<h1 class="font-medium">Project ready!</h1>
<p>You may now add components and start building.</p>
<p>We've already added the button component for you.</p>
<Button client:load className="mt-2">Button</Button>
</div>
<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>
<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>
</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>
</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>