Neuaufbau: Astro 5, Anfrage statt Buchung, Kundenkarte, Design-Tokens, SEO/GEO, Security-Header

This commit is contained in:
till committed 2026-10-09 07:21:38 +02:00
1 parent 49afd192ef
commit 0aa72718a0
86 files changed
+9743 -1079

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
.astro
.git
*.md
!README.md
+68
View File
@@ -0,0 +1,68 @@
# CLAUDE.md · Website Friseur-Team VK
Architekturfakten für Agenten und Entwickler. Gestaltung: `DESIGN.md`. Entscheidungen: `DECISIONS.md`.
## Überblick
- Statische Website für Friseur-Team VK (Kastanienallee 1, 21255 Tostedt), gebaut von Heidrich Digital.
- **Astro 5**, `output: 'static'`, Vanilla CSS, kein Framework, kein Tailwind. Zwei kleine Skripte (Menü-Escape, Anfrageformular, Vorher/Nachher-Regler).
- Vorschau: https://vk-friseur.heidrich-digital.de (noindex). Repo: `till/vk-friseur` auf git.heidrich-digital.de.
- Deployment: Coolify-App `n101smck3800u4ajngvm2yjg`, Build über `Dockerfile` (node:22-alpine baut, nginx:1.27-alpine liefert auf Port 80).
## Befehle
```bash
npm ci
npm run dev # http://localhost:4321
npm run build # nach dist/
```
## Struktur
```
src/
data/site.ts EINZIGE Quelle für Adresse, Telefon, Öffnungszeiten, Team, Preise, Anliegen, Endpunkte
styles/tokens.css Farben (OKLCH), Schrift, Skala, Abstände, Bewegung
styles/global.css Grundlage, Typo, Knöpfe, Layout-Helfer, Scroll-Einblendung
styles/form.css Formular-Bausteine (nur Anfrage-Seite)
layouts/Base.astro <head>, Meta, noindex, JSON-LD (HairSalon + Breadcrumb + seiteneigene), Header/Footer/Leiste
components/ Header, Footer, MobileBar, Shot (Fotofläche), Reels, Rating, CtaBand, PageHero,
Steps, Faq, PriceList, LoyaltyCard, BeforeAfter, Hours, Pic, Icon, Logo, DraftNote
scripts/anfrage.ts Formularlogik (schrittweise, Prüfung, fetch, Bestätigung)
pages/ index, brautservice, balayage-farbe, haarverlaengerung, team, leistungen-preise,
kundenkarte, anfrage/index, anfrage/danke, kontakt, impressum, datenschutz, 404
assets/img, assets/logo Ausgangsbilder, werden von astro:assets zu AVIF/WebP/JPG in mehreren Breiten
public/
fonts/ Bodoni Moda + Archivo, selbst gehostet, beschnitten (LICENSE-FONTS.txt)
robots.txt, llms.txt, og.jpg, favicon.svg, favicon-32.png, apple-touch-icon.png, logo-vk.png
deploy/security-headers.conf CSP, HSTS, nosniff, Referrer-Policy, X-Robots-Tag
nginx.conf Caching, 301 alter Adressen, ohne Schrägstrich am Ende, 404
```
## Regeln
- Fakten nur in `src/data/site.ts` ändern. Footer, Kontakt, Impressum, Schema und Formular lesen von dort.
- Preise sind „ab“-Richtwerte (Platzhalter). Ein Wert in `priceGroups` ändert Preisliste und Premium-Seiten. Die Hinweise im Formular stehen in `anliegen[].hint` und müssen mitgezogen werden.
- Keine Inline-Styles und keine Inline-Skripte: Die CSP erlaubt nur `'self'`. Werte zur Laufzeit per `el.style.setProperty()` (CSSOM ist erlaubt). `build.inlineStylesheets: 'never'`, CSS als eine Datei (`cssCodeSplit: false`).
- Keine Fremdanfragen. Karten, Instagram, Google-Bewertungen sind nur Links. Neue Fremdquellen erfordern CSP- und Datenschutz-Änderung.
- Bilder immer über `Pic.astro` (astro:assets) mit `sizes`; Hero mit `eager`.
- Fehlende Arbeitsfotos über `Shot.astro` mit Motiv und Bildidee. Wenn Fotos kommen: Datei nach `src/assets/img/`, `Shot` durch `Pic` in `.frame` ersetzen (Bogen: Klasse `arch`).
- Texte: Stimme des Salons („wir“, „Sie“), konkret, keine Superlative, keine erfundenen Zahlen oder Zitate, keine Gedankenstriche.
## Anfrageformular
- `POST https://vk-karte.heidrich-digital.de/api/anfrage`, `multipart/form-data`. Feldnamen exakt wie im Backend: `anliegen, kundenstatus, haarlaenge, ausgangslage, wunsch, zeitraum, hochzeitsdatum, ort, personen, warteliste, preis_ok, name*, telefon*, email, rueckruf, datenschutz*, fotos (0 bis 3, je ≤ 6 MB), website (Honeypot)`.
- Ohne JS: normaler Submit, Backend antwortet mit 303 auf `/anfrage/danke`.
- Mit JS: Prüfung im Browser, `fetch` mit `Accept: application/json`, Antwort `{ok:true,id}` oder `{ok:false,error}`. Honeypot gefüllt: nichts senden, Bestätigung zeigen.
- Vorauswahl per `?anliegen=braut|balayage|verlaengerung|typveraenderung|schnitt|sonstiges`.
- Backend muss CORS für `https://vk-friseur.heidrich-digital.de` erlauben (und später für die Live-Domain).
## Livegang (Checkliste)
1. In `src/layouts/Base.astro` die Zeile `<meta name="robots" content="noindex, nofollow" />` löschen.
2. In `deploy/security-headers.conf` die Zeile `add_header X-Robots-Tag ...` löschen.
3. `site` in `astro.config.mjs` und `SITE_URL` in `src/data/site.ts` auf die Live-Domain setzen; `robots.txt` und `llms.txt` anpassen.
4. CORS und CSP (`connect-src`, `form-action`) für die Live-Domain prüfen.
5. Footer-Hinweis „Vorschau der neuen Website …“ in `Footer.astro` entfernen, Entwurfshinweise in Impressum und Datenschutz durch geprüfte Texte ersetzen.
6. 301-Weiterleitungen in `nginx.conf` mit der URL-Liste der Altseite abgleichen.
7. Preise, Öffnungszeiten, Team, WhatsApp-Nummer und Bildrechte vom Salon bestätigen lassen.
+23
View File
@@ -0,0 +1,23 @@
# DECISIONS.md
Entscheidungen mit Begründung. Neueste oben.
## 2026-10-09 · Neuaufbau der Website (ersetzt Juni-Entwurf)
**Ziel:** Portfolio und hochwertigere Kundschaft statt mehr Masse. Der Salon ist oft ausgebucht und hat 4,9 Sterne bei 193 Google-Bewertungen.
1. **Keine Online-Buchung, vorqualifizierte Anfrage.** Der Salon will e-cut als Terminbuch behalten. Das Formular fragt Anliegen, Haar, Fotos, Zeitraum, bei Braut Datum/Ort/Personen und Rückrufzeit ab; der Salon ruft zurück und trägt selbst ein. Der Juni-Stand mit e-cut-Buchungsanbindung wurde entfernt.
2. **Preisrahmen im Formular sichtbar** plus Häkchen „Preisrahmen gesehen“. Filtert reine Preisanfragen, ohne Pflichtfeld zu sein (das Backend erwartet es optional).
3. **Warteliste als Häkchen** („Ich kann auch kurzfristig“) statt eigener Seite: nutzt Absagen, ohne einen zweiten Prozess zu bauen.
4. **Telefon und WhatsApp für Bestandskundschaft, Anfrage für Neukundschaft.** Mobile Leiste unten: „Anrufen“ und „Anfragen“; auf Anfrage- und Kontaktseite „Anrufen“ und „WhatsApp“.
5. **Hero-Satz „Schön von außen, glücklich von innen.“** Er steht im Salon auf dem Aufsteller (Foto salon-innen-2) und stammt damit vom Salon, nicht von uns. Ersetzt „Für Ihre Haare das Beste“.
6. **Schriften Bodoni Moda + Archivo, selbst gehostet.** Bodoni für den Beauty-Magazin-Charakter, Archivo, weil die schmalen Schnitte die Logo-Groteske aufgreifen. Beschnitten auf Latin-1 und genutzte Achsen: 109 KB für drei Dateien, zwei davon vorgeladen.
7. **Bogen und Wandpaneel als einzige Motive**, beide aus der Architektur des Salons abgeleitet. Keine Karten-Raster.
8. **Fotoflächen statt Stockfotos.** Jede Fläche nennt Motiv und Bildidee und ist damit die Shotliste fürs Shooting.
9. **Keine erfundenen Bewertungstexte.** Nur 4,9 / 193 mit Link aufs Google-Profil. `aggregateRating` bewusst nicht im Schema (Richtlinien zu selbst bereitgestellten Bewertungen).
10. **Keine Fremdanfragen, kein Cookie-Banner nötig.** Karten nur als Link (Google Maps, Apple Karten), keine Einbettung. CSP nur `'self'` plus `vk-karte.heidrich-digital.de` für Formular.
11. **Strenge CSP ohne `'unsafe-inline'`.** Dafür CSS als eine externe Datei (`cssCodeSplit: false`, 10 KB gzip) und keine Inline-Styles.
12. **Preise als „ab“-Richtwerte mit Fußnote.** Platzhalter, bis der Salon echte Preise liefert. Werte bewusst moderat und in `src/data/site.ts` zentral.
13. **Noindex doppelt** (Meta und `X-Robots-Tag`), `robots.txt` erlaubt Crawling, damit das Noindex gelesen wird.
14. **Impressum und Datenschutz als ehrliche Entwürfe** mit sichtbarem Hinweis „vor Livegang prüfen“. Steuernummer aus dem alten Impressum nicht übernommen (nicht Pflicht); Hinweis auf die abgeschaltete EU-OS-Plattform entfernt.
15. **Teamzahl 8 laut Inhaltsliste**, obwohl auf dem Gruppenfoto mehr Personen zu sehen sind. Vom Salon bestätigen lassen.
+68
View File
@@ -0,0 +1,68 @@
# DESIGN.md · Friseur-Team VK
Gestaltungsregeln für die Website. Die Tokens stehen in `src/styles/tokens.css`; diese Datei erklärt, warum sie so sind.
## Haltung
Ruhiges Premium, wie ein gutes Magazin über einen echten Salon. Hell, handwerklich, selbstbewusst. Die Seite soll Menschen ansprechen, die gezielt für Brautstyling, Balayage und Great Lengths kommen, und Stammkundschaft den kurzen Weg zum Telefon lassen.
Szene: Eine Frau Anfang dreißig sitzt abends auf dem Sofa, das Handy in der Hand, und vergleicht Salons für ihre Hochzeit im nächsten Sommer. Daraus folgt: helles Thema, große Bilder, kurze Wege zu Preis und Anfrage, nichts, was nach Rabatt aussieht.
Drei Wörter für die Marke: **hell, präzise, nahbar**.
## Farbe
Alle Farben stammen aus dem Logo und aus dem Salon selbst (weiße Räume, blaue Wandpaneele). Strategie: **Restrained mit einer Committed-Fläche pro Seite**. Grund ist Papier-Weiß, Petrol trägt Akzente und genau eine vollflächige Zone (Anfrage-Band am Seitenende).
| Rolle | Token | OKLCH | Hex (Quelle) | Einsatz |
|---|---|---|---|---|
| Anthrazit | `--c-anthrazit` | `oklch(33.8% 0.012 140.5)` | #343933 | Text, Footer |
| Petrol | `--c-petrol` | `oklch(50.7% 0.105 234)` | #036E98 | Links, Knöpfe, Akzentwörter, Anfrage-Band |
| Hellblau | `--c-hellblau` | `oklch(64.9% 0.076 224.9)` | #5899B2 | nur Deko, nie für Text auf Weiß (3,2:1) |
| Silbergrau | `--c-silber` | `oklch(71.1% 0.009 225.1)` | #9CA3A6 | Rahmen, Formularränder |
| Nebelgrau | `--c-nebel` | `oklch(88.4% 0.008 216.6)` | #D3DADC | Bildgrund, Flächen |
| Papier | `--c-papier` | `oklch(98.7% 0.003 220)` | abgeleitet | Seitengrund, kein reines Weiß |
| Petrol tief | `--c-petrol-tief` | `oklch(36% 0.075 236)` | abgeleitet | Hover, Wandpaneel im Anfrage-Band |
| Text leise | `--c-text-leise` | `oklch(46% 0.012 200)` | abgeleitet | Nebentext, ≥ 4,5:1 auf Papier |
Kontraste (WCAG 2.1 AA): Anthrazit auf Papier 11,8:1, Petrol auf Papier 5,7:1, Papier auf Petrol 5,7:1. Hellblau und Silbergrau tragen nie Fließtext.
## Schrift
Selbst gehostet unter `public/fonts/` (OFL), auf Latin-1 beschnitten, Achsen begrenzt. Keine Anfragen an Google Fonts oder CDNs.
- **Bodoni Moda** (variabel, Optical Size 11 bis 96): Überschriften, Zahlen, Zitate. Hoher Kontrast wie im Mode- und Beauty-Magazin; bei großen Größen schaltet die optische Größe automatisch auf feine Haarstriche. Kursiv nur für das Akzentwort einer Überschrift (in Petrol).
- **Archivo** (variabel, Breite 68 bis 100 %, Gewicht 350 bis 700): Fließtext, Formulare. Labels in Großbuchstaben laufen schmal (72 % Breite) und greifen damit die schmale Groteskschrift des Logos („FRISEUR TEAM“) auf.
Skala (fließend, Verhältnis ca. 1,25 bis 1,33): Label 13 px · Klein 15 px · Text 17 px · Lead 19 bis 23 px · H4 20 bis 24 px · H3 24 bis 34 px · H2 34 bis 60 px · H1 44 bis 96 px. Untergrenze überall 12 px. Zeilenlänge Fließtext höchstens 38 rem (rund 70 Zeichen).
## Form und Motive
- **Bogen**: Bilder und Fotoflächen der Premium-Leistungen haben oben einen Halbkreis (`--arch`). Vorbild ist der Rundbogen-Durchgang im Salon (sichtbar auf salon-innen-2 und -3).
- **Wandpaneel**: Ein senkrechter Petrol-Block hinter Hero-Bildern und im Anfrage-Band, wie die blauen Wandflächen zwischen den Spiegeln.
- **Haarlinien**: Listen und Preise stehen auf 1 px Linien, Abschnittsanfänge auf 1,5 px Anthrazit. Keine Karten-Raster.
- **Knöpfe**: Pille, ein Hauptknopf pro Abschnitt (Petrol), Nebenknopf als Kontur.
- **Fotoflächen (Shotliste)**: Solange Arbeitsfotos fehlen, zeigt `Shot.astro` eine beschriftete Fläche mit Motiv und Bildidee. Das ist die Liste für das Shooting.
## Abstände
4-px-Raster (`--s-1` bis `--s-9`). Abschnitte atmen mit `--section` (72 bis 152 px). Seitenrand `--gutter` (20 bis 56 px). Rhythmus bewusst wechselnd: enge Gruppen, große Abstände zwischen Abschnitten.
## Bewegung
Ein Auftritt beim Laden (Hero-Text steigt, Wandpaneel wächst), dazu dezentes Einblenden beim Scrollen über CSS `animation-timeline: view()`. Kurve ease-out-quint, keine Federn. Bei `prefers-reduced-motion: reduce` ist alles aus. Die H1 wird nur verschoben, nie ausgeblendet (LCP).
## Barrierefreiheit
WCAG 2.1 AA. Fokus sichtbar (2 px Petrol), Touch-Ziele mindestens 44 px, Formularfelder 52 px hoch und 17 px Schrift (kein Zoom auf iOS), Fehlermeldungen als Text mit Fehlerübersicht, Menü und FAQ funktionieren ohne JavaScript (`<details>`).
## Nicht machen
- Beige, Taupe, Rosé, Lila-Verläufe, Glas-Effekte, Neon
- Inter, Roboto, Montserrat, Space Grotesk und andere Standardschriften
- Stockfotos oder KI-Bilder von Frisuren; erfundene Bewertungen oder Zitate
- Gleich große Karten im 3er- oder 6er-Raster für Leistungen
- Pulsierende Knöpfe, Pop-ups, Cookie-Banner (es gibt keine Cookies)
- Farbige Seitenstreifen an Karten, Verlaufstext, große Zahl mit kleinem Label als Statistikreihe
- Gedankenstriche als Satzzeichen in Texten, Superlative, „exklusiv“, „einzigartig“
- Inline-`style`-Attribute (die CSP erlaubt nur `style-src 'self'`)
+8 -4
View File
@@ -1,12 +1,16 @@
FROM node:20-alpine AS build
# --- Build ---
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
COPY . .
RUN npm run build
FROM nginx:alpine
# --- Auslieferung ---
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY deploy/security-headers.conf /etc/nginx/snippets/security-headers.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO /dev/null http://127.0.0.1/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
+21 -1
View File
@@ -1,7 +1,27 @@
// @ts-check
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://vk-demo.heidrich-digital.de',
site: 'https://vk-friseur.heidrich-digital.de',
output: 'static',
trailingSlash: 'never',
compressHTML: true,
build: {
format: 'directory',
// Keine Inline-Styles: erlaubt eine strenge CSP ohne 'unsafe-inline'.
inlineStylesheets: 'never',
},
vite: {
build: { assetsInlineLimit: 0, cssCodeSplit: false },
},
image: {
// Nur lokale Bilder; keine Fremdquellen.
domains: [],
},
integrations: [
sitemap({
filter: (page) => !page.includes('/anfrage/danke') && !page.includes('/404'),
}),
],
});
+14
View File
@@ -0,0 +1,14 @@
# Sicherheits-Header. Wird in jedem location-Block eingebunden,
# weil nginx add_header aus dem server-Block sonst nicht vererbt.
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://vk-karte.heidrich-digital.de; form-action 'self' https://vk-karte.heidrich-digital.de; frame-ancestors 'none'; base-uri 'self'; object-src 'none'; manifest-src 'self'; upgrade-insecure-requests" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
# VORSCHAU: nicht indexieren.
# Beim Livegang diese Zeile löschen UND in src/layouts/Base.astro das
# <meta name="robots" content="noindex, nofollow"> entfernen.
add_header X-Robots-Tag "noindex, nofollow" always;
+47 -5
View File
@@ -4,16 +4,58 @@ server {
root /usr/share/nginx/html;
index index.html;
server_tokens off;
absolute_redirect off;
charset utf-8;
charset_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
gzip on;
gzip_types text/css application/javascript image/svg+xml;
gzip_vary on;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types text/plain text/css application/javascript application/json application/ld+json application/xml text/xml image/svg+xml;
error_page 404 /404.html;
# Alte und frühere Adressen (Relaunch). Vor Livegang mit der Altseite abgleichen.
location = /leistungen { return 301 /leistungen-preise; }
location = /kontaktdaten { return 301 /kontakt; }
location = /hochzeitsservice { return 301 /brautservice; }
location = /braut { return 301 /brautservice; }
location = /balayage { return 301 /balayage-farbe; }
location = /haarverdichtung { return 301 /haarverlaengerung; }
location = /treuekarte { return 301 /kundenkarte; }
# Abschließenden Schrägstrich entfernen (kanonische URLs ohne /)
rewrite ^/(.+)/$ /$1 permanent;
# Gehashte Build-Dateien: ein Jahr, unveränderlich
location ^~ /_astro/ {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location ^~ /fonts/ {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location ~* \.(?:png|jpg|jpeg|svg|ico|webp|avif)$ {
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "public, max-age=604800";
try_files $uri =404;
}
location / {
try_files $uri $uri/ $uri.html /index.html;
include /etc/nginx/snippets/security-headers.conf;
add_header Cache-Control "public, max-age=0, must-revalidate";
try_files $uri $uri/index.html $uri.html =404;
}
location ~* \.(webp|jpg|jpeg|png|svg|woff2|css|js)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
location = /404.html {
internal;
include /etc/nginx/snippets/security-headers.conf;
}
}
+6123
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -1,7 +1,7 @@
{
"name": "vk-friseur-website",
"type": "module",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
@@ -9,6 +9,8 @@
"preview": "astro preview"
},
"dependencies": {
"astro": "^4.16.18"
"@astrojs/sitemap": "^3.7.4",
"astro": "^5.18.2",
"sharp": "^0.35.5"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><circle cx="32" cy="32" r="30" fill="#036E98"/><circle cx="32" cy="32" r="25.5" fill="none" stroke="#f9fbfc" stroke-width="1.5" opacity=".6"/><text x="32" y="41" text-anchor="middle" font-family="Didot, 'Bodoni 72', Georgia, serif" font-size="26" fill="#f9fbfc" letter-spacing="-1">VK</text></svg>

After

Width:  |  Height:  |  Size: 358 B

+3
View File
@@ -0,0 +1,3 @@
Archivo: SIL Open Font License 1.1, (c) The Archivo Project Authors
Bodoni Moda: SIL Open Font License 1.1, (c) The Bodoni Moda Project Authors
Subset (Latin-1 + Typografie) und Achsen beschnitten mit fontTools; Quelle: @fontsource-variable
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#343933" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>

After

Width:  |  Height:  |  Size: 184 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

+29
View File
@@ -0,0 +1,29 @@
# Friseur-Team VK
> Friseursalon und Meisterbetrieb in Tostedt (Landkreis Harburg, Niedersachsen), seit November 2017. Inhaberin: Veronika Sprangel, Friseurmeisterin. Schwerpunkte: Brautstyling, Balayage und Haarfarbe, Haarverlängerung und Haarverdichtung mit Great Lengths. Produkte: Redken, Paul Mitchell. Google: 4,9 Sterne bei 193 Bewertungen (Stand September 2026).
Termine werden nicht online gebucht, sondern telefonisch, per WhatsApp oder über ein Anfrageformular mit Rückruf vereinbart.
## Kontakt
- Adresse: Kastanienallee 1, 21255 Tostedt
- Telefon: +49 4182 2889535
- E-Mail: info@friseur-team-vk.de
- Öffnungszeiten: Di 9–18, Mi 9–20, Do 9–18, Fr 9–20, Sa 9–14 Uhr; Mo und So geschlossen
- Instagram: https://www.instagram.com/friseur.team_vk/
## Seiten
- [Start](https://vk-friseur.heidrich-digital.de/): Überblick, Schwerpunkte, Team, Öffnungszeiten
- [Brautservice](https://vk-friseur.heidrich-digital.de/brautservice): Beratung, Probetermin, Styling am Hochzeitstag, auch vor Ort; Frisur und Make-up
- [Balayage & Farbe](https://vk-friseur.heidrich-digital.de/balayage-farbe): Balayage, Foliensträhnen, Coloration, Glossing
- [Haarverlängerung](https://vk-friseur.heidrich-digital.de/haarverlaengerung): Great Lengths, Verlängerung und Verdichtung, Beratungstermin vorab
- [Leistungen & Preise](https://vk-friseur.heidrich-digital.de/leistungen-preise): alle Leistungen mit „ab“-Richtpreisen
- [Team](https://vk-friseur.heidrich-digital.de/team): Inhaberin und Team
- [Kundenkarte](https://vk-friseur.heidrich-digital.de/kundenkarte): digitale Stempelkarte (Apple Wallet, Google Wallet, Web-Karte, gedruckt); 6 Stempel = Pflege-Upgrade gratis
- [Anfrage](https://vk-friseur.heidrich-digital.de/anfrage): Formular mit Rückruf
- [Kontakt](https://vk-friseur.heidrich-digital.de/kontakt): Adresse, Telefon, WhatsApp, Öffnungszeiten
## Hinweis
Diese Adresse ist eine Vorschau der neuen Website. Preise sind Richtwerte und werden mit dem Salon abgestimmt.
File renamed without changes.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

+7
View File
@@ -0,0 +1,7 @@
# Friseur-Team VK
# Crawling ist erlaubt, damit Suchmaschinen das noindex der Vorschau sehen.
# Beim Livegang: noindex in Base.astro und X-Robots-Tag in nginx.conf entfernen (siehe CLAUDE.md).
User-agent: *
Allow: /
Sitemap: https://vk-friseur.heidrich-digital.de/sitemap-index.xml
Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

+57
View File
@@ -0,0 +1,57 @@
---
/** Vorher/Nachher-Vergleich. Ohne JS steht der Regler auf der Mitte. */
import Shot from './Shot.astro';
interface Props { before: string; after: string; detail?: string }
const { before, after, detail } = Astro.props;
---
<div class="ba" data-ba>
<div class="ba__stage">
<div class="ba__layer ba__after"><Shot title={after} ratio="4/5" tone="petrol" /></div>
<div class="ba__layer ba__before"><Shot title={before} ratio="4/5" /></div>
<div class="ba__line" aria-hidden="true"><span></span></div>
<span class="ba__tag ba__tag--l" aria-hidden="true">Vorher</span>
<span class="ba__tag ba__tag--r" aria-hidden="true">Nachher</span>
</div>
{detail && <p class="ba__note">{detail}</p>}
<label class="ba__ctrl">
<span class="visually-hidden">Vergleich verschieben: links vorher, rechts nachher</span>
<input type="range" min="0" max="100" value="50" step="1" />
</label>
</div>
<script>
document.querySelectorAll<HTMLElement>('[data-ba]').forEach((el) => {
const input = el.querySelector('input');
const set = (v: string) => el.style.setProperty('--pos', v + '%');
input?.addEventListener('input', () => set(input.value));
});
</script>
<style>
.ba { --pos: 50%; max-width: 34rem; }
.ba__stage { position: relative; aspect-ratio: 4 / 5; overflow: hidden; border-radius: var(--r-m); }
.ba__layer { position: absolute; inset: 0; }
.ba__layer :global(.shot) { height: 100%; }
.ba__layer :global(.shot__title) { font-size: 1.125rem; max-width: 10ch; }
.ba__after :global(.shot__in) { align-items: flex-end; text-align: right; }
.ba__note { margin-top: var(--s-2); font-size: var(--fs-small); color: var(--c-text-leise); }
.ba__before { clip-path: inset(0 calc(100% - var(--pos)) 0 0); }
.ba__line { position: absolute; top: 0; bottom: 0; left: var(--pos); width: 2px; margin-left: -1px; background: var(--c-papier); pointer-events: none; }
.ba__line span {
position: absolute; top: 50%; left: 50%; width: 2.75rem; height: 2.75rem; margin: -1.375rem 0 0 -1.375rem;
border-radius: 50%; background: var(--c-papier); box-shadow: 0 4px 14px oklch(20% 0.03 235 / 0.25);
}
.ba__line span::before, .ba__line span::after {
content: ''; position: absolute; top: 50%; width: 0.5rem; height: 0.5rem; margin-top: -0.25rem;
border: solid var(--c-petrol); border-width: 0 0 2px 2px;
}
.ba__line span::before { left: 0.7rem; transform: rotate(45deg); }
.ba__line span::after { right: 0.7rem; transform: rotate(-135deg); }
.ba__tag {
position: absolute; top: var(--s-4); padding: 0.3rem 0.7rem; border-radius: var(--r-pill);
background: var(--c-papier); color: var(--c-anthrazit);
font-size: var(--fs-label); font-weight: 600; font-stretch: 72%; text-transform: uppercase; letter-spacing: 0.1em;
}
.ba__tag--l { left: var(--s-4); }
.ba__tag--r { right: var(--s-4); }
.ba__ctrl { display: block; margin-top: var(--s-3); }
.ba__ctrl input { width: 100%; height: var(--tap); accent-color: var(--c-petrol); cursor: ew-resize; }
</style>
+48
View File
@@ -0,0 +1,48 @@
---
import Icon from './Icon.astro';
import { salon } from '../data/site';
interface Props { title?: string; text?: string; anliegen?: string; button?: string }
const {
title = 'Erzählen Sie uns, was Sie vorhaben.',
text = 'Für Brautstyling, Balayage und Haarverlängerung beginnen wir mit einer kurzen Anfrage. Wir melden uns telefonisch, besprechen alles und tragen den Termin für Sie ein.',
anliegen,
button = 'Anfrage stellen',
} = Astro.props;
const href = anliegen ? `/anfrage?anliegen=${anliegen}` : '/anfrage';
---
<section class="cta on-dark" aria-labelledby="cta-h">
<div class="wrap cta__in">
<div class="cta__panel" aria-hidden="true"></div>
<div class="cta__txt">
<p class="label cta__label">Anfrage</p>
<h2 id="cta-h">{title}</h2>
<p class="cta__p">{text}</p>
<div class="btn-row">
<a class="btn btn--light" href={href}>{button}<Icon name="arrow" /></a>
<a class="btn btn--outline-light" href={salon.phoneHref}><Icon name="phone" />{salon.phoneDisplay}</a>
</div>
<p class="cta__small">Schon Kundin oder Kunde bei uns? Rufen Sie einfach an oder schreiben Sie uns <a href={salon.whatsappHref} rel="noopener">per WhatsApp</a>.</p>
</div>
</div>
</section>
<style>
.cta { background: var(--c-petrol); color: var(--c-papier); position: relative; overflow: hidden; }
.cta__in { position: relative; padding-block: var(--section); }
.cta__panel {
position: absolute; top: 0; bottom: 0; right: var(--gutter);
width: clamp(3rem, 12vw, 11rem);
background: var(--c-petrol-tief);
}
.cta__panel::after {
content: ''; position: absolute; top: 0; bottom: 0; left: -0.75rem; width: 1px;
background: oklch(100% 0 0 / 0.28);
}
.cta__txt { position: relative; max-width: 44rem; }
@media (max-width: 47.99rem) { .cta__panel { opacity: 0.55; } }
.cta__label { color: oklch(95% 0.025 225); }
.cta h2 { color: var(--c-papier); margin-top: var(--s-4); }
.cta__p { margin-top: var(--s-5); font-size: var(--fs-lead); line-height: 1.5; color: oklch(96% 0.01 225); max-width: 36rem; }
.cta .btn-row { margin-top: var(--s-6); }
.cta__small { margin-top: var(--s-6); font-size: var(--fs-small); color: oklch(93% 0.015 225); max-width: 34rem; }
.cta__small a { color: var(--c-papier); }
</style>
+11
View File
@@ -0,0 +1,11 @@
---
interface Props { text: string }
const { text } = Astro.props;
---
<div class="draft" role="note">
<p class="label">Entwurf · vor Livegang prüfen</p>
<p>{text}</p>
</div>
<style>
.draft { padding: var(--s-5); border: 1.5px solid var(--c-petrol); border-radius: var(--r-m); background: var(--c-petrol-hell); display: grid; gap: var(--s-2); max-width: var(--measure); }
</style>
+43
View File
@@ -0,0 +1,43 @@
---
/** Echte, belegbare Fragen. Erzeugt zusätzlich FAQPage-Schema über die Seite. */
interface Props { items: { q: string; a: string }[]; title?: string }
const { items, title = 'Häufige Fragen' } = Astro.props;
---
<section class="section faq" aria-labelledby="faq-h">
<div class="wrap faq__grid">
<div class="head">
<p class="label">Fragen</p>
<h2 id="faq-h">{title}</h2>
</div>
<div class="faq__list">
{items.map((it) => (
<details class="faq__item">
<summary><span>{it.q}</span><span class="faq__ico" aria-hidden="true"></span></summary>
<div class="faq__a"><p>{it.a}</p></div>
</details>
))}
</div>
</div>
</section>
<style>
.faq__grid { display: grid; gap: var(--s-6); }
@media (min-width: 60rem) { .faq__grid { grid-template-columns: 1fr 1.6fr; gap: var(--s-8); } }
.faq__list { border-top: var(--hair); }
.faq__item { border-bottom: var(--hair); }
.faq__item summary {
list-style: none; cursor: pointer;
display: flex; justify-content: space-between; align-items: center; gap: var(--s-4);
min-height: 3.5rem; padding-block: var(--s-4);
font-weight: 600; font-size: 1.0625rem; line-height: 1.4;
}
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary:hover { color: var(--c-petrol); }
.faq__ico { position: relative; width: 1rem; height: 1rem; flex: none; }
.faq__ico::before, .faq__ico::after {
content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 1.5px; background: currentColor;
transition: transform var(--t-med) var(--ease-out);
}
.faq__ico::after { transform: rotate(90deg); }
.faq__item[open] .faq__ico::after { transform: rotate(0deg); }
.faq__a { padding-bottom: var(--s-5); max-width: 60ch; color: var(--c-text); }
</style>
+92
View File
@@ -0,0 +1,92 @@
---
import Logo from './Logo.astro';
import Icon from './Icon.astro';
import Hours from './Hours.astro';
import { salon, nav } from '../data/site';
const year = new Date().getFullYear();
---
<footer class="ftr on-dark">
<div class="wrap ftr__grid">
<div class="ftr__brand">
<div class="ftr__logo"><Logo width={120} /></div>
<p class="ftr__claim">Schön von außen, glücklich von innen.</p>
<p class="ftr__addr">
{salon.legalName}<br />
{salon.street}<br />
{salon.zip} {salon.city}
</p>
</div>
<div>
<h2 class="label ftr__h">Kontakt</h2>
<ul class="ftr__list">
<li><a href={salon.phoneHref}><Icon name="phone" />{salon.phoneDisplay}</a></li>
<li><a href={salon.whatsappHref} rel="noopener"><Icon name="whatsapp" />WhatsApp</a></li>
<li><a href={`mailto:${salon.email}`}><Icon name="mail" />{salon.email}</a></li>
<li><a href={salon.instagram} rel="noopener"><Icon name="instagram" />{salon.instagramHandle}</a></li>
<li><a href={salon.facebook} rel="noopener"><Icon name="facebook" />Facebook</a></li>
</ul>
</div>
<div>
<h2 class="label ftr__h">Öffnungszeiten</h2>
<Hours compact />
</div>
<nav aria-label="Seiten">
<h2 class="label ftr__h">Seiten</h2>
<ul class="ftr__list ftr__list--plain">
{nav.map((n) => <li><a href={n.href}>{n.label}</a></li>)}
<li><a href="/anfrage">Anfrage stellen</a></li>
</ul>
</nav>
</div>
<div class="wrap ftr__base">
<p>© {year} {salon.legalName}</p>
<p class="ftr__legal"><a href="/impressum">Impressum</a><a href="/datenschutz">Datenschutz</a></p>
<p class="ftr__demo">Vorschau der neuen Website. Arbeitsfotos, Preise und Rechtstexte werden vor dem Livegang mit dem Salon abgestimmt.</p>
</div>
</footer>
<style>
.ftr {
background: var(--c-anthrazit);
color: oklch(92% 0.006 220);
padding-block: var(--s-9) var(--s-6);
font-size: var(--fs-small);
}
.ftr a { color: var(--c-papier); text-decoration: none; }
.ftr a:hover { color: var(--c-nebel); text-decoration: underline; }
.ftr__grid { display: grid; gap: var(--s-7); grid-template-columns: 1fr; }
@media (min-width: 40rem) { .ftr__grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 68rem) { .ftr__grid { grid-template-columns: 1.4fr 1fr 1fr 0.9fr; } }
.ftr__logo {
display: inline-block; background: var(--c-papier);
padding: 0.6rem 0.9rem; border-radius: var(--r-m);
}
.ftr__logo :global(.logo) { width: 104px; }
.ftr__claim {
font-family: var(--font-display); font-style: italic;
font-size: var(--fs-h4); line-height: 1.3; color: var(--c-papier);
margin-top: var(--s-5); max-width: 18ch;
}
.ftr__addr { margin-top: var(--s-4); }
.ftr__h { color: var(--c-hellblau); margin-bottom: var(--s-4); }
/* Hellblau auf Anthrazit nur für Labels in Großbuchstaben ab 13px fett: Kontrast 3,7:1 reicht nicht für AA, daher aufgehellt */
.ftr__h { color: oklch(80% 0.06 225); }
.ftr__list { list-style: none; padding: 0; display: grid; gap: 0.15rem; }
.ftr__list a { display: inline-flex; align-items: center; gap: 0.6em; min-height: var(--tap); }
.ftr__list svg { width: 1.15em; height: 1.15em; color: oklch(80% 0.06 225); }
.ftr__list--plain a { min-height: 2.25rem; }
.ftr__note { margin-top: var(--s-3); color: oklch(80% 0.008 220); }
.ftr__base {
margin-top: var(--s-8); padding-top: var(--s-5);
border-top: 1px solid oklch(100% 0 0 / 0.14);
display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-6); align-items: center;
color: oklch(82% 0.008 220);
}
.ftr__legal { display: flex; gap: var(--s-5); }
.ftr__legal a { min-height: var(--tap); display: inline-flex; align-items: center; text-decoration: underline; text-underline-offset: 0.2em; }
.ftr__demo { flex-basis: 100%; font-size: var(--fs-label); color: oklch(78% 0.008 220); max-width: 60ch; }
</style>
+141
View File
@@ -0,0 +1,141 @@
---
import Logo from './Logo.astro';
import Icon from './Icon.astro';
import { nav, salon } from '../data/site';
interface Props { current: string }
const { current } = Astro.props;
const isCurrent = (href: string) => current === href || current.startsWith(href + '/');
---
<header class="hdr">
<div class="wrap hdr__in">
<a href="/" class="hdr__logo" aria-label="Friseur-Team VK, zur Startseite">
<Logo eager width={104} />
</a>
<nav class="hdr__nav" aria-label="Hauptnavigation">
<ul>
{nav.map((n) => (
<li><a href={n.href} aria-current={isCurrent(n.href) ? 'page' : undefined}>{n.href === '/leistungen-preise' ? 'Preise' : n.label}</a></li>
))}
</ul>
</nav>
<div class="hdr__cta">
<a class="btn btn--ghost hdr__tel" href={salon.phoneHref}><Icon name="phone" />{salon.phoneDisplay}</a>
<a class="btn" href="/anfrage">Anfragen</a>
</div>
<details class="mnav">
<summary class="mnav__btn">
<span class="mnav__open"><Icon name="menu" /> Menü</span>
<span class="mnav__close"><Icon name="close" /> Schließen</span>
</summary>
<nav class="mnav__panel" aria-label="Hauptnavigation mobil">
<ul>
<li><a href="/" aria-current={current === '/' ? 'page' : undefined}>Start</a></li>
{nav.map((n) => (
<li><a href={n.href} aria-current={isCurrent(n.href) ? 'page' : undefined}>{n.label}</a></li>
))}
<li><a href="/anfrage" aria-current={isCurrent('/anfrage') ? 'page' : undefined}>Anfrage stellen</a></li>
</ul>
<p class="mnav__meta">
<a href={salon.phoneHref}>{salon.phoneDisplay}</a><br />
{salon.street}, {salon.zip} {salon.city}
</p>
</nav>
</details>
</div>
</header>
<script>
// Menü mit Escape schließen; funktioniert auch ohne dieses Skript.
const d = document.querySelector<HTMLDetailsElement>('.mnav');
if (d) {
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && d.open) {
d.open = false;
d.querySelector('summary')?.focus();
}
});
}
</script>
<style>
.hdr {
position: relative;
z-index: 40;
background: var(--c-papier);
border-bottom: var(--hair);
}
.hdr__in {
display: flex;
align-items: center;
gap: var(--s-5);
min-height: 4.5rem;
padding-block: var(--s-2);
}
.hdr__logo { display: block; flex: none; }
.hdr__logo :global(.logo) { width: 88px; height: auto; }
.hdr__nav, .hdr__cta { display: none; }
/* Mobil: Menü als <details>, funktioniert ohne JavaScript */
.mnav { margin-left: auto; }
.mnav__btn {
list-style: none;
display: inline-flex; align-items: center; gap: 0.5em;
min-height: var(--tap); padding: 0 var(--s-4);
border: 1.5px solid var(--c-silber); border-radius: var(--r-pill);
font-weight: 600; font-stretch: 85%; cursor: pointer;
user-select: none;
}
.mnav__btn::-webkit-details-marker { display: none; }
.mnav__btn span { display: inline-flex; align-items: center; gap: 0.45em; }
.mnav__btn svg { width: 1.2em; height: 1.2em; }
.mnav__close { display: none !important; }
.mnav[open] .mnav__open { display: none !important; }
.mnav[open] .mnav__close { display: inline-flex !important; }
.mnav__panel {
position: absolute; left: 0; right: 0; top: 100%;
background: var(--c-papier);
border-bottom: var(--hair);
padding: var(--s-3) var(--gutter) var(--s-6);
box-shadow: 0 24px 40px -24px oklch(30% 0.02 230 / 0.25);
}
.mnav__panel ul { list-style: none; padding: 0; }
.mnav__panel li { border-bottom: var(--hair); }
.mnav__panel a {
display: flex; align-items: center; min-height: 3.25rem;
font-family: var(--font-display); font-size: 1.5rem; line-height: 1.2;
color: var(--c-anthrazit); text-decoration: none;
}
.mnav__panel a[aria-current='page'] { color: var(--c-petrol); font-style: italic; }
.mnav__meta { margin-top: var(--s-5); font-size: var(--fs-small); color: var(--c-text-leise); }
.mnav__meta a { font-family: var(--font-text); font-size: inherit; display: inline; min-height: 0; color: var(--c-petrol); }
@media (prefers-reduced-motion: no-preference) {
.mnav[open] .mnav__panel { animation: drop var(--t-med) var(--ease-out); }
@keyframes drop { from { opacity: 0; transform: translateY(-8px); } }
}
@media (min-width: 72rem) {
.hdr { position: sticky; top: 0; }
.hdr__logo :global(.logo) { width: 104px; }
.mnav { display: none; }
.hdr__nav { display: block; margin-left: auto; }
.hdr__nav ul { list-style: none; padding: 0; display: flex; gap: var(--s-1); }
.hdr__nav a {
display: inline-flex; align-items: center; min-height: var(--tap);
padding: 0 0.6rem;
font-size: 0.9375rem; font-weight: 500; font-stretch: 88%;
color: var(--c-anthrazit); text-decoration: none;
background-image: linear-gradient(var(--c-petrol), var(--c-petrol));
background-size: 0 1.5px; background-repeat: no-repeat; background-position: 0.6rem calc(100% - 0.55rem);
transition: background-size var(--t-med) var(--ease-out), color var(--t-fast);
}
.hdr__nav a:hover, .hdr__nav a[aria-current='page'] { color: var(--c-petrol); background-size: calc(100% - 1.2rem) 1.5px; }
.hdr__cta { display: flex; gap: var(--s-2); }
.hdr__cta .btn { font-size: 0.9375rem; padding-inline: 1.1em; }
}
@media (min-width: 72rem) and (max-width: 82rem) {
.hdr__tel { display: none; }
}
</style>
+24
View File
@@ -0,0 +1,24 @@
---
import { hours, fmtTime } from '../data/site';
interface Props { compact?: boolean }
const { compact = false } = Astro.props;
const shown = hours.filter((h) => h.schema !== 'Sunday');
---
<table class:list={['hours', { 'hours--compact': compact }]}>
<caption class="visually-hidden">Öffnungszeiten</caption>
<tbody>
{shown.map((h) => (
<tr>
<th scope="row">{compact ? h.short : h.day}</th>
<td class="num">{h.open ? `${fmtTime(h.open)}–${fmtTime(h.close!)} Uhr` : 'geschlossen'}</td>
</tr>
))}
</tbody>
</table>
<style>
.hours { border-collapse: collapse; width: 100%; max-width: 22rem; }
.hours th, .hours td { text-align: left; padding: 0.55rem 0; border-bottom: var(--hair); font-weight: 400; }
.hours th { padding-right: var(--s-5); }
.hours td { text-align: right; }
.hours--compact th, .hours--compact td { padding: 0.3rem 0; border-color: oklch(100% 0 0 / 0.14); }
</style>
+28
View File
@@ -0,0 +1,28 @@
---
/** Schlichte Linien-Icons, inline (keine Icon-Schrift, keine Fremdquelle). */
interface Props { name: 'phone' | 'whatsapp' | 'arrow' | 'star' | 'mail' | 'pin' | 'instagram' | 'facebook' | 'camera' | 'scissors' | 'menu' | 'close' | 'clock' | 'check' | 'play' | 'wallet' | 'external'; class?: string; }
const { name, class: cls } = Astro.props;
const paths: Record<string, string> = {
phone: '<path d="M5 4h3.2l1.6 4-2 1.3a10.5 10.5 0 0 0 5 5l1.3-2 4 1.6V17a2 2 0 0 1-2 2A15 15 0 0 1 3 6a2 2 0 0 1 2-2Z"/>',
whatsapp: '<path d="M4.5 19.5 5.6 16A8 8 0 1 1 8.4 18.6Z"/><path d="M9.2 8.6c0 3 2.6 5.7 5.6 5.8l1-1.3-1.7-.9-.8.8a4.4 4.4 0 0 1-2.3-2.3l.8-.8-.9-1.7-1.3 1Z"/>',
arrow: '<path d="M4 12h15M13 6l6 6-6 6"/>',
star: '<path d="m12 3.6 2.6 5.3 5.8.8-4.2 4.1 1 5.8-5.2-2.7-5.2 2.7 1-5.8L3.6 9.7l5.8-.8Z"/>',
mail: '<rect x="3.5" y="5.5" width="17" height="13" rx="1.5"/><path d="m4 6.5 8 6 8-6"/>',
pin: '<path d="M12 21s-6.5-5.6-6.5-11a6.5 6.5 0 0 1 13 0C18.5 15.4 12 21 12 21Z"/><circle cx="12" cy="10" r="2.3"/>',
instagram: '<rect x="3.5" y="3.5" width="17" height="17" rx="4.5"/><circle cx="12" cy="12" r="3.8"/><circle cx="17.2" cy="6.8" r=".6" fill="currentColor"/>',
facebook: '<path d="M14 8.5h2.5V5H14a3.5 3.5 0 0 0-3.5 3.5V11H8v3.5h2.5V21H14v-6.5h2.5L17 11h-3V9a.5.5 0 0 1 .5-.5Z"/>',
camera: '<path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h2l1.5-2h6l1.5 2h2A1.5 1.5 0 0 1 20 8.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5Z"/><circle cx="12" cy="12.5" r="3.5"/>',
scissors: '<circle cx="6.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/><path d="M8.3 15.7 17 4M15.7 15.7 7 4"/>',
menu: '<path d="M4 8h16M4 16h16"/>',
close: '<path d="m6 6 12 12M18 6 6 18"/>',
clock: '<circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/>',
check: '<path d="m5 12.5 4.5 4.5L19 7.5"/>',
play: '<path d="M8 5.5v13l10-6.5Z"/>',
wallet: '<rect x="3.5" y="6" width="17" height="13" rx="2"/><path d="M3.5 10h17M15.5 14.5h2"/>',
external: '<path d="M14 5h5v5M19 5l-8 8M17 14v4a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h4"/>',
};
const filled = name === 'star';
---
<svg class={cls} viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"
fill={filled ? 'currentColor' : 'none'} stroke={filled ? 'none' : 'currentColor'}
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" set:html={paths[name]} />
+7
View File
@@ -0,0 +1,7 @@
---
import { Image } from 'astro:assets';
import logo from '../assets/logo/logo.png';
interface Props { width?: number; eager?: boolean }
const { width = 112, eager = false } = Astro.props;
---
<Image src={logo} width={width} densities={[1, 2]} format="webp" alt="Friseur-Team VK, Tostedt" loading={eager ? 'eager' : 'lazy'} class="logo" />
+76
View File
@@ -0,0 +1,76 @@
---
/** Illustration der digitalen Stempelkarte (Muster, keine echten Daten). */
import Logo from './Logo.astro';
import Icon from './Icon.astro';
interface Props { stamps?: number }
const { stamps = 4 } = Astro.props;
// Deterministisches Muster im Stil eines QR-Codes (Illustration, nicht scannbar)
const N = 25;
let seed = 7;
const rnd = () => ((seed = (seed * 9301 + 49297) % 233280) / 233280);
const cells: [number, number][] = [];
const finder = (x: number, y: number) => (x < 8 && y < 8) || (x > N - 9 && y < 8) || (x < 8 && y > N - 9);
for (let y = 0; y < N; y++) for (let x = 0; x < N; x++) if (!finder(x, y) && rnd() > 0.52) cells.push([x, y]);
const fp = (x: number, y: number) => `M${x} ${y}h7v7h-7Z M${x + 1} ${y + 1}v5h5v-5Z M${x + 2} ${y + 2}h3v3h-3Z`;
---
<div class="lcard" role="img" aria-label={`Muster der digitalen Kundenkarte: ${stamps} von 6 Stempeln gesammelt, beim sechsten gibt es ein Pflege-Upgrade gratis.`}>
<div class="lcard__top" aria-hidden="true">
<div class="lcard__logo"><Logo width={84} /></div>
<div class="lcard__count"><span class="lcard__k">Stempel</span><span class="lcard__v">{stamps} / 6</span></div>
</div>
<ol class="lcard__stamps" aria-hidden="true">
{Array.from({ length: 6 }).map((_, i) => (
<li class:list={['lcard__stamp', { 'is-on': i < stamps, 'is-gift': i === 5 }]}>
{i === 5 ? <span>Pflege</span> : <Icon name="scissors" />}
</li>
))}
</ol>
<div class="lcard__fields" aria-hidden="true">
<div><span class="lcard__k">Prämie</span><span class="lcard__v2">Pflege-Upgrade gratis</span></div>
<div><span class="lcard__k">Noch</span><span class="lcard__v2">{6 - stamps} Besuche</span></div>
</div>
<div class="lcard__qr" aria-hidden="true">
<svg viewBox={`-2 -2 ${N + 4} ${N + 4}`} width="120" height="120" shape-rendering="crispEdges">
<rect x="-2" y="-2" width={N + 4} height={N + 4} fill="#fff" />
<path fill="currentColor" fill-rule="evenodd" d={`${fp(0, 0)} ${fp(N - 7, 0)} ${fp(0, N - 7)}`} />
<path fill="currentColor" d={cells.map(([x, y]) => `M${x} ${y}h1v1h-1Z`).join('')} />
</svg>
<span class="lcard__hint">Muster · an der Kasse scannen lassen</span>
</div>
</div>
<style>
.lcard {
width: min(100%, 21rem);
background: var(--c-papier);
border-radius: 1.1rem;
padding: 1.1rem 1.1rem 1.25rem;
box-shadow: 0 1px 0 oklch(100% 0 0 / 0.6) inset, 0 30px 60px -30px oklch(25% 0.05 235 / 0.45), 0 2px 6px oklch(25% 0.03 235 / 0.12);
color: var(--c-anthrazit);
font-size: 0.875rem;
}
.lcard__top { display: flex; justify-content: space-between; align-items: center; }
.lcard__logo :global(.logo) { width: 76px; }
.lcard__count { text-align: right; display: grid; }
.lcard__k { font-size: 0.75rem; font-weight: 600; font-stretch: 72%; text-transform: uppercase; letter-spacing: 0.12em; color: var(--c-text-leise); }
.lcard__v { font-family: var(--font-display); font-size: 1.5rem; line-height: 1.1; }
.lcard__stamps {
list-style: none; padding: 0.9rem; margin-top: 0.9rem;
display: grid; grid-template-columns: repeat(6, 1fr); gap: 0.4rem;
background: var(--c-petrol-hell); border-radius: 0.7rem;
}
.lcard__stamp {
aspect-ratio: 1; border-radius: 50%;
display: grid; place-items: center;
border: 1.5px dashed var(--c-silber); color: var(--c-silber);
}
.lcard__stamp :global(svg) { width: 58%; height: 58%; }
.lcard__stamp.is-on { background: var(--c-petrol); border: 0; color: var(--c-papier); }
.lcard__stamp.is-gift { border-style: solid; border-color: var(--c-petrol); color: var(--c-petrol); font-size: 0.75rem; font-weight: 700; font-stretch: 70%; text-transform: uppercase; letter-spacing: 0.02em; }
.lcard__fields { display: flex; justify-content: space-between; gap: 1rem; margin-top: 0.9rem; }
.lcard__fields > div { display: grid; }
.lcard__fields > div:last-child { text-align: right; }
.lcard__v2 { font-weight: 600; }
.lcard__qr { display: grid; justify-items: center; gap: 0.5rem; margin-top: 1rem; color: var(--c-anthrazit); }
.lcard__qr svg { width: 7.5rem; height: 7.5rem; }
.lcard__hint { font-size: 0.75rem; color: var(--c-text-leise); }
</style>
+28
View File
@@ -0,0 +1,28 @@
---
import Icon from './Icon.astro';
import { salon } from '../data/site';
interface Props { variant?: 'default' | 'kontakt' }
const { variant = 'default' } = Astro.props;
---
<div class="mbar" role="region" aria-label="Schnellkontakt">
<a class="btn btn--ghost" href={salon.phoneHref}><Icon name="phone" />Anrufen</a>
{variant === 'kontakt'
? <a class="btn" href={salon.whatsappHref} rel="noopener"><Icon name="whatsapp" />WhatsApp</a>
: <a class="btn" href="/anfrage">Anfragen<Icon name="arrow" /></a>}
</div>
<style>
.mbar {
position: fixed; z-index: 50; left: 0; right: 0; bottom: 0;
display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-2);
padding: var(--s-2) var(--s-3) calc(var(--s-2) + env(safe-area-inset-bottom));
background: oklch(98.7% 0.003 220 / 0.97);
border-top: var(--hair);
}
.mbar .btn { width: 100%; min-height: 3rem; }
.mbar .btn--ghost { background: var(--c-papier); }
:global(body) { padding-bottom: calc(var(--bar-h) + env(safe-area-inset-bottom)); }
@media (min-width: 72rem) {
.mbar { display: none; }
:global(body) { padding-bottom: 0; }
}
</style>
+45
View File
@@ -0,0 +1,45 @@
---
/** Kopf für Unterseiten: Text links, Bild oder Platzhalter rechts mit Wandpaneel. */
interface Props { label: string; title: string; lead?: string }
const { label, title, lead } = Astro.props;
---
<section class="phero">
<div class="wrap phero__grid">
<div class="phero__txt">
<p class="label phero__label">{label}</p>
<h1 set:html={title} />
{lead && <p class="lead phero__lead">{lead}</p>}
<div class="phero__actions"><slot name="actions" /></div>
</div>
<div class="phero__media">
<div class="phero__panel" aria-hidden="true"></div>
<div class="phero__img"><slot name="media" /></div>
</div>
</div>
</section>
<style>
.phero { padding-block: var(--s-7) var(--section); overflow: hidden; }
.phero__grid { display: grid; gap: var(--s-7); align-items: end; }
@media (min-width: 60rem) { .phero__grid { grid-template-columns: 1.15fr 0.85fr; gap: var(--s-8); padding-top: var(--s-6); } }
.phero__label { margin-bottom: var(--s-5); }
.phero__lead { margin-top: var(--s-5); }
.phero__actions:not(:empty) { margin-top: var(--s-6); }
.phero__media { position: relative; padding: 0 0 var(--s-5) var(--s-5); }
.phero__panel {
position: absolute; left: 0; bottom: 0; top: 18%; width: 46%;
background: var(--c-petrol);
}
.phero__img { position: relative; }
@media (prefers-reduced-motion: no-preference) {
.phero__txt > * { animation: rise var(--t-slow) var(--ease-out) both; }
.phero__txt > :nth-child(2) { animation-delay: 80ms; }
.phero__txt > :nth-child(3) { animation-delay: 160ms; }
.phero__txt > :nth-child(4) { animation-delay: 240ms; }
.phero__panel { animation: grow 1.1s var(--ease-out) both 120ms; transform-origin: bottom; }
/* Überschrift nur bewegen, nicht ausblenden: zählt sofort als LCP */
.phero__txt > h1 { animation-name: rise-t; }
@keyframes rise { from { opacity: 0; transform: translateY(18px); } }
@keyframes rise-t { from { transform: translateY(14px); } }
@keyframes grow { from { transform: scaleY(0); } }
}
</style>
+29
View File
@@ -0,0 +1,29 @@
---
import { Picture } from 'astro:assets';
import type { ImageMetadata } from 'astro';
interface Props {
src: ImageMetadata;
alt: string;
sizes: string;
widths?: number[];
eager?: boolean;
class?: string;
position?: string;
}
const { src, alt, sizes, widths = [400, 640, 900, 1280, 1700], eager = false, class: cls } = Astro.props;
const w = widths.filter((x) => x <= src.width);
if (!w.includes(src.width) && src.width < Math.max(...widths)) w.push(src.width);
---
<Picture
src={src}
alt={alt}
formats={['avif', 'webp']}
fallbackFormat="jpg"
widths={w}
sizes={sizes}
loading={eager ? 'eager' : 'lazy'}
fetchpriority={eager ? 'high' : 'auto'}
decoding={eager ? 'sync' : 'async'}
quality={eager ? 62 : 58}
class={cls}
/>
+40
View File
@@ -0,0 +1,40 @@
---
import { priceGroups, eur } from '../data/site';
interface Props { ids?: string[]; compact?: boolean; headingLevel?: 2 | 3 }
const { ids, compact = false, headingLevel = 2 } = Astro.props;
const groups = ids ? priceGroups.filter((g) => ids.includes(g.id)) : priceGroups;
const H = headingLevel === 2 ? 'h2' : 'h3';
---
<div class:list={['plist', { 'plist--compact': compact }]}>
{groups.map((g) => (
<section class="plist__group" id={`preise-${g.id}`} aria-labelledby={`pg-${g.id}`}>
<H class="plist__h" id={`pg-${g.id}`}>{g.title}</H>
{g.lead && !compact && <p class="plist__lead leise">{g.lead}</p>}
<dl class="plist__dl">
{g.items.map((i) => (
<div class="plist__row">
<dt>{i.label}{i.note && <span class="plist__note"> · {i.note}</span>}</dt>
<dd class="num">{i.from !== null ? <>ab {eur(i.from)}<sup aria-label="Richtwert, siehe Hinweis">*</sup></> : 'nach Absprache'}</dd>
</div>
))}
</dl>
</section>
))}
<p class="plist__foot small leise">* Richtwerte. Preise werden mit dem Salon abgestimmt. Der genaue Preis hängt von Haarlänge, Haardichte und Aufwand ab; wir nennen ihn Ihnen vor der Behandlung.</p>
</div>
<style>
.plist { display: grid; gap: var(--s-7); }
.plist__h { font-size: var(--fs-h3); }
.plist--compact .plist__h { font-size: var(--fs-h4); font-family: var(--font-text); font-weight: 600; }
.plist__lead { margin-top: var(--s-2); }
.plist__dl { margin-top: var(--s-4); border-top: 1.5px solid var(--c-anthrazit); }
.plist__row {
display: flex; justify-content: space-between; align-items: baseline; gap: var(--s-4);
padding-block: 0.8rem; border-bottom: var(--hair);
}
.plist__row dt { flex: 1; }
.plist__row dd { flex: none; font-weight: 600; white-space: nowrap; }
.plist__note { color: var(--c-text-leise); font-size: var(--fs-small); }
sup { font-size: 0.75em; color: var(--c-text-leise); margin-left: 1px; }
.plist__foot { max-width: 60ch; }
</style>
+44
View File
@@ -0,0 +1,44 @@
---
import Icon from './Icon.astro';
import { salon } from '../data/site';
interface Props { variant?: 'chip' | 'block' }
const { variant = 'chip' } = Astro.props;
const g = salon.google;
---
{variant === 'chip' ? (
<a class="rchip" href={g.url} rel="noopener">
<span class="rchip__stars" aria-hidden="true">{[1,2,3,4,5].map(() => <Icon name="star" />)}</span>
<span><strong>{g.rating}</strong> bei {g.count} Google-Bewertungen</span>
</a>
) : (
<div class="rblock">
<p class="rblock__num display" aria-hidden="true">{g.rating}</p>
<div class="rblock__txt">
<span class="rblock__stars" aria-hidden="true">{[1,2,3,4,5].map(() => <Icon name="star" />)}</span>
<p class="rblock__sent">{g.count} Menschen haben uns auf Google bewertet, im Schnitt mit {g.rating} von 5 Sternen.</p>
<p class="small leise">Stand {g.asOf}. Die einzelnen Bewertungen finden Sie bei Google.</p>
<a class="more" href={g.url} rel="noopener"><span>Bewertungen auf Google lesen</span><Icon name="external" /></a>
</div>
</div>
)}
<style>
.rchip {
display: inline-flex; align-items: center; flex-wrap: wrap; gap: 0.25rem 0.6rem;
min-height: var(--tap);
color: var(--c-anthrazit); text-decoration: none; font-size: var(--fs-small);
}
.rchip:hover { color: var(--c-petrol); }
.rchip strong { font-weight: 700; }
.rchip > span:last-child { text-decoration: underline; text-decoration-color: var(--c-silber); text-underline-offset: 0.25em; }
.rchip__stars, .rblock__stars { display: inline-flex; gap: 2px; color: var(--c-petrol); }
.rchip__stars :global(svg) { width: 1rem; height: 1rem; }
.rblock { display: grid; gap: var(--s-5); align-items: end; }
@media (min-width: 48rem) { .rblock { grid-template-columns: auto 1fr; gap: var(--s-7); } }
.rblock__num {
font-size: var(--fs-mega); line-height: 0.82; letter-spacing: -0.04em;
color: var(--c-petrol);
}
.rblock__txt { max-width: 34rem; display: grid; gap: var(--s-3); justify-items: start; }
.rblock__stars :global(svg) { width: 1.4rem; height: 1.4rem; }
.rblock__sent { font-family: var(--font-display); font-size: var(--fs-h3); line-height: 1.25; }
</style>
+36
View File
@@ -0,0 +1,36 @@
---
import Shot from './Shot.astro';
import Icon from './Icon.astro';
import { salon } from '../data/site';
interface Props { title?: string; items: { title: string; detail?: string }[] }
const { title = 'Kurz gezeigt', items } = Astro.props;
---
<section class="reels section--tight" aria-labelledby="reels-h">
<div class="wrap reels__head">
<div>
<p class="label">Reels · {salon.instagramHandle}</p>
<h2 id="reels-h" class="reels__h">{title}</h2>
</div>
<a class="more" href={salon.instagram} rel="noopener"><span>Auf Instagram ansehen</span><Icon name="external" /></a>
</div>
<div class="reels__track" tabindex="0" role="group" aria-label="Reels, seitlich wischen">
{items.map((it) => <Shot kind="Reel" ratio="9/16" title={it.title} detail={it.detail} class="reels__item" />)}
</div>
</section>
<style>
.reels__head { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: end; gap: var(--s-4); }
.reels__h { font-size: var(--fs-h3); margin-top: var(--s-3); }
.reels__track {
margin-top: var(--s-6);
display: grid; grid-auto-flow: column;
grid-auto-columns: min(62vw, 15.5rem);
gap: var(--s-4);
overflow-x: auto; overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
padding-inline: max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter)));
scroll-padding-inline: max(var(--gutter), calc((100vw - var(--maxw)) / 2 + var(--gutter)));
padding-bottom: var(--s-4);
scrollbar-width: thin;
}
.reels__track :global(.reels__item) { scroll-snap-align: start; border-radius: var(--r-m); }
</style>
+66
View File
@@ -0,0 +1,66 @@
---
/**
* Platzhalter für ein Foto aus dem Shooting (= Shotliste).
* Bewusst sichtbar beschriftet, keine Stockfotos.
*/
import Icon from './Icon.astro';
interface Props {
title: string; // Motiv, z. B. "Braut-Hochsteckfrisur"
detail?: string; // Ausschnitt/Bildidee
ratio?: '4/5' | '3/4' | '3/2' | '1/1' | '9/16' | '16/9';
arch?: boolean;
tone?: 'nebel' | 'petrol';
kind?: 'Foto' | 'Reel';
class?: string;
}
const { title, detail, ratio = '4/5', arch = false, tone = 'nebel', kind = 'Foto', class: cls } = Astro.props;
const r = ratio.replace('/', '-');
---
<figure class:list={['shot', `shot--${r}`, `shot--${tone}`, { arch }, cls]} role="img" aria-label={`${kind} folgt: ${title}${detail ? ', ' + detail : ''}`}>
<div class="shot__in" aria-hidden="true">
<span class="shot__tag">{kind === 'Reel' ? <Icon name="play" /> : <Icon name="camera" />}{kind} folgt</span>
<span class="shot__title">{title}</span>
{detail && <span class="shot__detail">{detail}</span>}
</div>
</figure>
<style>
.shot {
position: relative; overflow: hidden; margin: 0;
background:
radial-gradient(120% 80% at 50% 0%, oklch(97% 0.006 220) 0%, transparent 70%),
var(--c-nebel-hell);
color: var(--c-anthrazit);
}
.shot--petrol {
background: radial-gradient(120% 80% at 50% 0%, oklch(56% 0.1 232) 0%, transparent 70%), var(--c-petrol);
color: var(--c-papier);
}
.shot--4-5 { aspect-ratio: 4 / 5; }
.shot--3-4 { aspect-ratio: 3 / 4; }
.shot--3-2 { aspect-ratio: 3 / 2; }
.shot--1-1 { aspect-ratio: 1; }
.shot--9-16 { aspect-ratio: 9 / 16; }
.shot--16-9 { aspect-ratio: 16 / 9; }
.shot__in {
position: absolute; inset: 0.75rem;
border: 1px solid oklch(50% 0.02 230 / 0.22);
display: flex; flex-direction: column; justify-content: flex-end; gap: 0.35rem;
padding: clamp(1rem, 0.6rem + 1.5vw, 1.75rem);
}
.shot--petrol .shot__in { border-color: oklch(100% 0 0 / 0.3); }
.arch .shot__in { border-radius: var(--arch); }
.shot__tag {
display: inline-flex; align-items: center; gap: 0.45em;
font-size: var(--fs-label); font-weight: 600; font-stretch: 72%;
text-transform: uppercase; letter-spacing: 0.12em;
color: var(--c-petrol);
}
.shot--petrol .shot__tag { color: var(--c-papier); }
.shot__tag :global(svg) { width: 1.15em; height: 1.15em; }
.shot__title {
font-family: var(--font-display); font-style: italic;
font-size: clamp(1.25rem, 1.05rem + 0.8vw, 1.75rem); line-height: 1.2;
}
.shot__detail { font-size: var(--fs-small); color: var(--c-text-leise); line-height: 1.4; }
.shot--petrol .shot__detail { color: oklch(93% 0.02 225); }
</style>
+21
View File
@@ -0,0 +1,21 @@
---
interface Props { steps: { title: string; text: string }[]; label?: string }
const { steps } = Astro.props;
---
<ol class="steps">
{steps.map((s, i) => (
<li class="steps__item reveal">
<span class="steps__n display" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
<h3 class="steps__t">{s.title}</h3>
<p class="steps__p">{s.text}</p>
</li>
))}
</ol>
<style>
.steps { list-style: none; padding: 0; display: grid; gap: var(--s-7); counter-reset: s; }
@media (min-width: 52rem) { .steps { grid-template-columns: repeat(3, 1fr); gap: var(--s-6); } }
.steps__item { border-top: 1.5px solid var(--c-anthrazit); padding-top: var(--s-4); }
.steps__n { display: block; font-size: var(--fs-h2); line-height: 1; color: var(--c-petrol); font-style: italic; }
.steps__t { font-size: var(--fs-h4); margin-top: var(--s-4); font-family: var(--font-text); font-weight: 600; }
.steps__p { margin-top: var(--s-3); color: var(--c-text); max-width: 32ch; }
</style>
+174
View File
@@ -0,0 +1,174 @@
/**
* Eine Quelle für alle Salon-Fakten.
* Footer, Kontaktseite, Impressum und JSON-LD lesen von hier,
* damit Adresse, Telefon und Zeiten überall gleich sind.
* Mit "pruefen: true" markierte Angaben vor Livegang mit dem Salon bestätigen.
*/
export const SITE_URL = 'https://vk-friseur.heidrich-digital.de';
export const salon = {
name: 'Friseur-Team VK',
legalName: 'Friseur-Team VK Veronika Sprangel',
owner: 'Veronika Sprangel',
ownerTitle: 'Friseurmeisterin',
since: 'November 2017',
foundingDate: '2017-11',
street: 'Kastanienallee 1',
zip: '21255',
city: 'Tostedt',
region: 'Niedersachsen',
country: 'DE',
phoneDisplay: '04182 2889535',
phoneHref: 'tel:+4941822889535',
phoneIntl: '+49 4182 2889535',
// WhatsApp-Nummer vom Salon bestätigen lassen (pruefen)
whatsappHref: 'https://wa.me/4941822889535',
email: 'info@friseur-team-vk.de',
instagram: 'https://www.instagram.com/friseur.team_vk/',
instagramHandle: '@friseur.team_vk',
facebook: 'https://www.facebook.com/FriseurTeamVK',
google: {
rating: '4,9',
count: 193,
asOf: 'September 2026',
url: 'https://www.google.com/maps/search/?api=1&query=FRISEUR-TEAM+VK&query_place_id=ChIJoaTlnmahsUcRFbgiyxAG0Fs',
},
routeUrl:
'https://www.google.com/maps/dir/?api=1&destination=Kastanienallee+1,+21255+Tostedt',
appleMapsUrl: 'https://maps.apple.com/?daddr=Kastanienallee+1,+21255+Tostedt',
chamber: 'Handwerkskammer Braunschweig-Lüneburg-Stade',
chamberNo: '2250821757',
brands: ['Redken', 'Paul Mitchell', 'Great Lengths'],
};
/** Öffnungszeiten (Stand März 2026, pruefen) */
export const hours = [
{ day: 'Montag', short: 'Mo', schema: 'Monday', open: null, close: null },
{ day: 'Dienstag', short: 'Di', schema: 'Tuesday', open: '09:00', close: '18:00' },
{ day: 'Mittwoch', short: 'Mi', schema: 'Wednesday', open: '09:00', close: '20:00' },
{ day: 'Donnerstag', short: 'Do', schema: 'Thursday', open: '09:00', close: '18:00' },
{ day: 'Freitag', short: 'Fr', schema: 'Friday', open: '09:00', close: '20:00' },
{ day: 'Samstag', short: 'Sa', schema: 'Saturday', open: '09:00', close: '14:00' },
{ day: 'Sonntag', short: 'So', schema: 'Sunday', open: null, close: null },
] as const;
export const fmtTime = (t: string) => {
const [h, m] = t.split(':');
return m === '00' ? String(Number(h)) : `${Number(h)}.${m}`;
};
/** Team (Stand März 2026, Namen und Rollen pruefen) */
export const team = [
{ name: 'Veronika Sprangel', role: 'Inhaberin, Friseurmeisterin' },
{ name: 'Iris Winterstein', role: 'Friseurin' },
{ name: 'Christin Gottschlich', role: 'Friseurin' },
{ name: 'Marco Gollnast-Heise', role: 'Friseur und Barber' },
{ name: 'Tina Sahling', role: 'Friseurin' },
{ name: 'Ella Schizipin', role: 'Friseurin' },
{ name: 'Fabian Gidom', role: 'Auszubildender' },
{ name: 'Dennis Sprangel', role: 'Organisation' },
];
/**
* Preise: "ab"-Richtwerte als Platzhalter. Werden mit dem Salon abgestimmt.
* Ein Wert hier ändert Preisliste, Premium-Seiten und Anfrageformular zugleich.
*/
export type Price = { label: string; from: number | null; note?: string };
export type PriceGroup = { id: string; title: string; lead?: string; items: Price[] };
export const priceGroups: PriceGroup[] = [
{
id: 'braut',
title: 'Brautservice',
lead: 'Frisur und Make-up, auf Wunsch am Ort Ihrer Hochzeit.',
items: [
{ label: 'Beratung und Probetermin Frisur', from: 75 },
{ label: 'Brautfrisur am Hochzeitstag', from: 120 },
{ label: 'Braut-Make-up', from: 80 },
{ label: 'Frisur für Begleitpersonen', from: 45 },
{ label: 'Styling vor Ort', from: null, note: 'Anfahrt nach Absprache' },
],
},
{
id: 'farbe',
title: 'Balayage und Farbe',
lead: 'Mit Produkten von Redken und Paul Mitchell.',
items: [
{ label: 'Balayage inkl. Glossing, Schnitt und Styling', from: 140 },
{ label: 'Foliensträhnen', from: 75 },
{ label: 'Coloration Ansatz', from: 50 },
{ label: 'Coloration komplett', from: 60 },
{ label: 'Glossing', from: 30 },
],
},
{
id: 'verlaengerung',
title: 'Haarverlängerung und -verdichtung',
lead: 'Mit Great Lengths. Vorher immer ein Beratungstermin.',
items: [
{ label: 'Haarverdichtung', from: 350 },
{ label: 'Haarverlängerung', from: 650 },
{ label: 'Nachpflege und Umsetzen', from: 90 },
],
},
{
id: 'damen',
title: 'Damen',
items: [
{ label: 'Waschen, Schneiden, Föhnen', from: 45 },
{ label: 'Waschen und Föhnen', from: 28 },
{ label: 'Hochsteckfrisur', from: 50 },
{ label: 'Dauerwelle', from: 70 },
],
},
{
id: 'herren',
title: 'Herren und Barbershop',
items: [
{ label: 'Haarschnitt', from: 26 },
{ label: 'Bart trimmen und konturieren', from: 15 },
{ label: 'Nassrasur', from: 20 },
],
},
{
id: 'kinder',
title: 'Kinder',
items: [{ label: 'Haarschnitt bis 12 Jahre', from: 18 }],
},
{
id: 'augen',
title: 'Augenbrauen und Wimpern',
items: [
{ label: 'Augenbrauen zupfen', from: 10 },
{ label: 'Augenbrauen färben', from: 10 },
{ label: 'Wimpern färben', from: 12 },
],
},
];
export const eur = (n: number) => `${n.toLocaleString('de-DE')} €`;
/** Anliegen im Anfrageformular (Werte exakt wie im Backend) */
export const anliegen = [
{ value: 'braut', label: 'Brautstyling', from: 75, hint: 'Probetermin ab 75 €, Brautfrisur ab 120 €' },
{ value: 'balayage', label: 'Balayage oder Farbe', from: 50, hint: 'Coloration ab 50 €, Balayage ab 140 €' },
{ value: 'verlaengerung', label: 'Verlängerung oder Verdichtung', from: 350, hint: 'Verdichtung ab 350 €, Verlängerung ab 650 €' },
{ value: 'typveraenderung', label: 'Typveränderung', from: 90, hint: 'Schnitt und Farbe zusammen ab 90 €' },
{ value: 'schnitt', label: 'Schnitt und Styling', from: 26, hint: 'Damen ab 45 €, Herren ab 26 €' },
{ value: 'sonstiges', label: 'Etwas anderes', from: null, hint: 'Preis nach Absprache' },
] as const;
export const ANFRAGE_ENDPOINT = 'https://vk-karte.heidrich-digital.de/api/anfrage';
export const KARTE_START = 'https://vk-karte.heidrich-digital.de/start';
export const KARTE_DEMO = 'https://vk-karte.heidrich-digital.de/demo';
export const nav = [
{ href: '/brautservice', label: 'Brautservice' },
{ href: '/balayage-farbe', label: 'Balayage & Farbe' },
{ href: '/haarverlaengerung', label: 'Haarverlängerung' },
{ href: '/leistungen-preise', label: 'Leistungen & Preise' },
{ href: '/team', label: 'Team' },
{ href: '/kundenkarte', label: 'Kundenkarte' },
{ href: '/kontakt', label: 'Kontakt' },
];
-1
View File
@@ -1 +0,0 @@
/// <reference path="../.astro/types.d.ts" />
+119 -102
View File
@@ -1,111 +1,128 @@
---
import "../styles/site.css";
const { title = "Friseur-Team VK · Veronika Sprangel · Friseur in Tostedt", description = "Friseur-Team VK in Tostedt – Schnitt, Farbe, Balayage, Barbershop, Braut & Haarverlängerung. Meisterbetrieb seit 2017, 8-köpfiges Team." } = Astro.props;
const tel = "+4941822889535";
const telDisplay = "04182 / 28 89 535";
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import MobileBar from '../components/MobileBar.astro';
import { salon, hours, SITE_URL, priceGroups } from '../data/site';
interface Crumb { name: string; href: string }
interface Props {
title: string;
description: string;
crumbs?: Crumb[];
schema?: Record<string, unknown>[];
bar?: 'default' | 'kontakt';
ogImage?: string;
}
const { title, description, crumbs = [], schema = [], bar = 'default', ogImage = '/og.jpg' } = Astro.props;
const path = Astro.url.pathname.replace(/\/$/, '') || '/';
const canonical = new URL(path, SITE_URL).href.replace(/\/$/, path === '/' ? '/' : '');
const fullTitle = path === '/' ? title : `${title} · Friseur-Team VK Tostedt`;
const salonId = `${SITE_URL}/#salon`;
const hairSalon = {
'@context': 'https://schema.org',
'@type': 'HairSalon',
'@id': salonId,
name: salon.name,
legalName: salon.legalName,
url: `${SITE_URL}/`,
logo: `${SITE_URL}/logo-vk.png`,
image: [`${SITE_URL}/og.jpg`],
telephone: salon.phoneIntl,
email: salon.email,
foundingDate: salon.foundingDate,
founder: { '@type': 'Person', name: salon.owner, jobTitle: salon.ownerTitle },
address: {
'@type': 'PostalAddress',
streetAddress: salon.street,
postalCode: salon.zip,
addressLocality: salon.city,
addressRegion: salon.region,
addressCountry: salon.country,
},
areaServed: { '@type': 'City', name: 'Tostedt' },
openingHoursSpecification: hours
.filter((h) => h.open)
.map((h) => ({
'@type': 'OpeningHoursSpecification',
dayOfWeek: `https://schema.org/${h.schema}`,
opens: h.open,
closes: h.close,
})),
sameAs: [salon.instagram, salon.facebook, salon.google.url],
brand: salon.brands.map((b) => ({ '@type': 'Brand', name: b })),
priceRange: '€€',
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: 'Leistungen',
itemListElement: priceGroups.map((g) => ({
'@type': 'OfferCatalog',
name: g.title,
itemListElement: g.items.map((i) => ({
'@type': 'Offer',
itemOffered: { '@type': 'Service', name: i.label },
})),
})),
},
};
const breadcrumb = crumbs.length
? {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [{ name: 'Start', href: '/' }, ...crumbs].map((c, i) => ({
'@type': 'ListItem',
position: i + 1,
name: c.name,
item: new URL(c.href, SITE_URL).href.replace(/\/$/, c.href === '/' ? '/' : ''),
})),
}
: null;
const graph = [hairSalon, ...(breadcrumb ? [breadcrumb] : []), ...schema];
---
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="icon" href="/img/logo.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600;9..40,700&display=swap" rel="stylesheet" />
</head>
<body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{fullTitle}</title>
<meta name="description" content={description} />
<!-- Vorschau: nicht indexieren. Beim Livegang diese Zeile entfernen (siehe CLAUDE.md). -->
<meta name="robots" content="noindex, nofollow" />
<link rel="canonical" href={canonical} />
<meta name="theme-color" content="#f9fbfc" />
<meta name="format-detection" content="telephone=no" />
<div class="topbar">
<div class="inner">
<span><span class="dot"></span>Willkommen bei Friseur-Team VK · Ihr Friseur im Herzen von Tostedt</span>
<span class="right">
<a href={`tel:${tel}`}>{telDisplay}</a>
<span class="muted">Di–Sa · Termine nach Vereinbarung</span>
</span>
</div>
</div>
<link rel="preload" href="/fonts/bodoni-moda-var.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/archivo-var.woff2" as="font" type="font/woff2" crossorigin />
<header class="header">
<div class="inner">
<a href="/" class="brand" aria-label="Friseur-Team VK Startseite">
<img src="/img/logo.png" alt="Friseur-Team VK Tostedt" height="46" style="height:46px;width:auto" />
</a>
<nav class="nav">
<a href="/#leistungen">Leistungen</a>
<a href="/haarverlaengerung">Haarverlängerung</a>
<a href="/braut">Braut</a>
<a href="/treuekarte">Treuekarte</a>
<a href="/#kontakt">Kontakt</a>
</nav>
<div class="header-cta">
<a href={`tel:${tel}`} class="tel">{telDisplay}</a>
<a href="/#buchen" class="btn btn-dark btn-sm">Termin buchen</a>
</div>
</div>
</header>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/favicon-32.png" sizes="32x32" type="image/png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="sitemap" href="/sitemap-index.xml" />
<main>
<slot />
</main>
<meta property="og:type" content="website" />
<meta property="og:locale" content="de_DE" />
<meta property="og:site_name" content="Friseur-Team VK" />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={new URL(ogImage, SITE_URL).href} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<footer class="footer">
<div class="container" style="padding-top:clamp(48px,7vw,80px);padding-bottom:clamp(48px,7vw,80px)">
<div class="grid">
<div>
<span style="display:inline-flex;background:#fff;border-radius:12px;padding:10px 14px">
<img src="/img/logo.png" alt="Friseur-Team VK" height="40" style="height:40px;width:auto" />
</span>
<p class="lead">Ihr Meisterbetrieb für schöne Haare in Tostedt – mit Zeit, Handwerk und einem guten Gefühl. Inhaberin: Veronika Sprangel.</p>
<a class="wa" href={`https://wa.me/${tel.replace('+','')}`} target="_blank" rel="noopener">💬 WhatsApp</a>
</div>
<div>
<h4>Salon</h4>
<div class="col">
<a href="/#leistungen">Leistungen</a>
<a href="/haarverlaengerung">Haarverlängerung</a>
<a href="/braut">Braut & Event</a>
<a href="/treuekarte">Treuekarte</a>
<a href="/#galerie">Galerie</a>
</div>
</div>
<div>
<h4>Kontakt</h4>
<div class="col">
<a href={`tel:${tel}`}>{telDisplay}</a>
<a href="mailto:info@friseur-team-vk.de">info@friseur-team-vk.de</a>
<span>Kastanienallee 1</span>
<span>21255 Tostedt</span>
</div>
</div>
<div>
<h4>Öffnungszeiten</h4>
<div class="col">
<span class="hours-row closed"><span>Montag</span><span>geschlossen</span></span>
<span class="hours-row"><span>Di · Do</span><span>9–18 Uhr</span></span>
<span class="hours-row"><span>Mi · Fr</span><span>9–20 Uhr</span></span>
<span class="hours-row"><span>Samstag</span><span>9–14 Uhr</span></span>
</div>
</div>
</div>
<div class="bottom">
<span>© 2026 Friseur-Team VK · Veronika Sprangel · Tostedt</span>
<div style="display:flex;align-items:center;gap:22px">
<a href="#">Impressum</a><a href="#">Datenschutz</a>
<span style="color:#74758a">Demo · Heidrich Digital</span>
</div>
</div>
</div>
</footer>
<div class="toast" id="toast">Aktion – im Live-System aktiv ✨</div>
<script is:inline>
const io=new IntersectionObserver((es)=>{es.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in');io.unobserve(e.target)}})},{threshold:.1});
document.querySelectorAll('.reveal').forEach(el=>io.observe(el));
window.__toast=(m)=>{const t=document.getElementById('toast');if(!t)return;if(m)t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2600)};
document.querySelectorAll('[data-wallet]').forEach(b=>b.addEventListener('click',()=>window.__toast('Zu Apple/Google Wallet hinzufügen – im Live-System aktiv ✨')));
</script>
</body>
{graph.map((g) => <script type="application/ld+json" set:html={JSON.stringify(g)} />)}
</head>
<body data-bar={bar}>
<a class="skip" href="#inhalt">Zum Inhalt springen</a>
<Header current={path} />
<main id="inhalt" tabindex="-1">
<slot />
</main>
<Footer />
<MobileBar variant={bar} />
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
---
import Base from '../layouts/Base.astro';
import Icon from '../components/Icon.astro';
import { salon } from '../data/site';
---
<Base title="Seite nicht gefunden" description="Diese Seite gibt es nicht. Hier geht es zurück zum Friseur-Team VK in Tostedt.">
<section class="section nf">
<div class="wrap nf__in">
<p class="label">Fehler 404</p>
<h1>Diese Seite <em>gibt es nicht.</em></h1>
<p class="lead">Vielleicht hat sich die Adresse geändert, die Website ist neu. Von hier aus finden Sie alles Wichtige:</p>
<ul class="nf__links">
<li><a class="more" href="/"><span>Startseite</span><Icon name="arrow" /></a></li>
<li><a class="more" href="/leistungen-preise"><span>Leistungen und Preise</span><Icon name="arrow" /></a></li>
<li><a class="more" href="/anfrage"><span>Anfrage stellen</span><Icon name="arrow" /></a></li>
<li><a class="more" href="/kontakt"><span>Kontakt und Öffnungszeiten</span><Icon name="arrow" /></a></li>
</ul>
<p>Oder rufen Sie uns an: <a href={salon.phoneHref}>{salon.phoneDisplay}</a></p>
</div>
</section>
</Base>
<style>
.nf__in { display: grid; gap: var(--s-5); justify-items: start; max-width: 46rem; }
.nf__links { list-style: none; padding: 0; }
</style>
+25
View File
@@ -0,0 +1,25 @@
---
import Base from '../../layouts/Base.astro';
import Icon from '../../components/Icon.astro';
import { salon } from '../../data/site';
---
<Base title="Danke für Ihre Anfrage" description="Ihre Anfrage an das Friseur-Team VK ist angekommen. Wir rufen Sie zurück." bar="kontakt">
<section class="section">
<div class="wrap thx">
<span class="thx__ico" aria-hidden="true"><Icon name="check" /></span>
<p class="label">Anfrage gesendet</p>
<h1>Danke, <em>Ihre Anfrage ist da.</em></h1>
<p class="lead">Wir melden uns telefonisch zu der Zeit, die Sie angegeben haben, und besprechen alles Weitere. Den Termin tragen wir dann für Sie ein.</p>
<p>Falls Sie noch etwas ergänzen möchten, erreichen Sie uns unter <a href={salon.phoneHref}>{salon.phoneDisplay}</a> oder <a href={salon.whatsappHref} rel="noopener">per WhatsApp</a>.</p>
<div class="btn-row">
<a class="btn" href="/">Zur Startseite<Icon name="arrow" /></a>
<a class="btn btn--ghost" href="/kundenkarte">Zur Kundenkarte</a>
</div>
</div>
</section>
</Base>
<style>
.thx { display: grid; gap: var(--s-5); justify-items: start; max-width: 46rem; }
.thx__ico { width: 3.5rem; height: 3.5rem; border-radius: 50%; background: var(--c-petrol); color: var(--c-papier); display: grid; place-items: center; }
.thx__ico :global(svg) { width: 1.8rem; height: 1.8rem; }
</style>
+237
View File
@@ -0,0 +1,237 @@
---
import Base from '../../layouts/Base.astro';
import Icon from '../../components/Icon.astro';
import '../../styles/form.css';
import { anliegen, salon, ANFRAGE_ENDPOINT, eur } from '../../data/site';
---
<Base
title="Anfrage stellen"
description="Anfrage an das Friseur-Team VK in Tostedt für Brautstyling, Balayage, Haarverlängerung und mehr. Mit Fotos, Wunschzeitraum und Rückrufzeit. Wir melden uns telefonisch."
crumbs={[{ name: 'Anfrage', href: '/anfrage' }]}
bar="kontakt"
>
<section class="aq">
<div class="wrap aq__grid">
<div class="aq__side">
<p class="label">Anfrage</p>
<h1>Erzählen Sie uns, <em>was Sie vorhaben.</em></h1>
<p class="lead">Wir vergeben Termine nicht online, sondern im Gespräch. Mit diesen Angaben können wir vorab einschätzen, wie viel Zeit wir einplanen und wer aus dem Team am besten passt.</p>
</div>
<div class="aq__main">
<div id="f-summary" class="f__summary" role="alert" tabindex="-1" hidden>
<h2>Bitte prüfen Sie Ihre Angaben</h2>
<ul></ul>
</div>
<form id="anfrage" class="f" action={ANFRAGE_ENDPOINT} method="post" enctype="multipart/form-data" data-endpoint={ANFRAGE_ENDPOINT}>
<!-- 1 Anliegen -->
<fieldset>
<legend><span class="f__n">1</span>Worum geht es?</legend>
<div class="choices choices--2" role="radiogroup" aria-describedby="err-anliegen">
{anliegen.map((a) => (
<label class="choice">
<input type="radio" name="anliegen" value={a.value} required data-hint={a.hint} data-label={a.label} />
<span class="choice__t">{a.label}</span>
<span class="choice__h">{a.from !== null ? `ab ${eur(a.from)}` : 'Preis nach Absprache'}</span>
</label>
))}
</div>
<p class="f__err" id="err-anliegen" data-for="anliegen"></p>
</fieldset>
<!-- 2 Braut (nur bei Brautstyling) -->
<fieldset class="step-later" data-locked data-only="braut">
<legend><span class="f__n">2</span>Ihre Hochzeit <span class="f__opt nojs-only">(nur bei Brautstyling)</span></legend>
<div class="f__row3">
<div class="f__field">
<label class="f__label" for="hochzeitsdatum">Datum der Hochzeit</label>
<input type="date" id="hochzeitsdatum" name="hochzeitsdatum" />
</div>
<div class="f__field">
<label class="f__label" for="ort">Ort oder Location <span class="f__opt">(optional)</span></label>
<input type="text" id="ort" name="ort" autocomplete="off" placeholder="Name der Location oder Adresse" />
</div>
<div class="f__field">
<label class="f__label" for="personen">Personen mit Styling</label>
<input type="number" id="personen" name="personen" min="1" max="15" inputmode="numeric" placeholder="1" aria-describedby="help-personen" />
<p class="f__help" id="help-personen">Braut plus Begleitung, zum Beispiel 3</p>
</div>
</div>
</fieldset>
<!-- 3 Haar -->
<fieldset class="step-later" data-locked>
<legend><span class="f__n" data-num>2</span>Ihr Haar heute</legend>
<fieldset class="f__group">
<legend class="f__sub">Haarlänge</legend>
<div class="choices choices--inline">
<label class="choice"><input type="radio" name="haarlaenge" value="kurz" /><span class="choice__t">Kurz</span><span class="choice__h">bis Kinn</span></label>
<label class="choice"><input type="radio" name="haarlaenge" value="mittel" /><span class="choice__t">Mittel</span><span class="choice__h">bis Schulter</span></label>
<label class="choice"><input type="radio" name="haarlaenge" value="lang" /><span class="choice__t">Lang</span><span class="choice__h">über Schulter</span></label>
</div>
</fieldset>
<fieldset class="f__group">
<legend class="f__sub">Ausgangslage</legend>
<div class="choices choices--inline">
<label class="choice"><input type="radio" name="ausgangslage" value="natur" /><span class="choice__t">Naturhaar</span></label>
<label class="choice"><input type="radio" name="ausgangslage" value="gefaerbt" /><span class="choice__t">Gefärbt</span></label>
<label class="choice"><input type="radio" name="ausgangslage" value="blondiert" /><span class="choice__t">Blondiert</span></label>
<label class="choice"><input type="radio" name="ausgangslage" value="unbekannt" /><span class="choice__t">Weiß nicht</span></label>
</div>
</fieldset>
<div class="f__field">
<label class="f__label" for="wunsch">Ihr Wunsch <span class="f__opt">(optional)</span></label>
<textarea id="wunsch" name="wunsch" rows="4" maxlength="2000" aria-describedby="help-wunsch"></textarea>
<p class="f__help" id="help-wunsch">Was soll sich ändern? Was hat Ihnen beim letzten Mal nicht gefallen?</p>
</div>
<div class="f__field">
<span class="f__label" id="lbl-fotos">Fotos <span class="f__opt">(optional, bis zu 3, je höchstens 6 MB)</span></span>
<div class="drop">
<input type="file" id="fotos" name="fotos" accept="image/jpeg,image/png,image/webp,image/heic,image/heif" multiple aria-labelledby="lbl-fotos" aria-describedby="help-fotos err-fotos" />
<p class="f__help" id="help-fotos">Am besten ein Foto vom Ist-Zustand, bei Tageslicht von hinten, und eins von Ihrem Wunsch.</p>
</div>
<ul class="files" id="files" aria-live="polite"></ul>
<p class="f__err" id="err-fotos" data-for="fotos"></p>
</div>
</fieldset>
<!-- 4 Zeit -->
<fieldset class="step-later" data-locked>
<legend><span class="f__n" data-num>3</span>Wann passt es Ihnen?</legend>
<div class="f__field">
<label class="f__label" for="zeitraum">Wunschzeitraum</label>
<select id="zeitraum" name="zeitraum">
<option value="">Bitte wählen</option>
<option>So bald wie möglich</option>
<option>In den nächsten zwei Wochen</option>
<option>In diesem Monat</option>
<option>In ein bis drei Monaten</option>
<option>Später oder fester Termin</option>
</select>
</div>
<label class="check check--box">
<input type="checkbox" name="warteliste" value="on" />
<span><strong>Ich kann auch kurzfristig.</strong><br /><span class="small">Wenn ein Termin frei wird, rufen wir Sie an. So kommen Sie oft schneller dran.</span></span>
</label>
<fieldset class="f__group">
<legend class="f__sub">Waren Sie schon bei uns?</legend>
<div class="choices choices--inline">
<label class="choice"><input type="radio" name="kundenstatus" value="neu" /><span class="choice__t">Zum ersten Mal</span></label>
<label class="choice"><input type="radio" name="kundenstatus" value="bestand" /><span class="choice__t">Ja, schon öfter</span></label>
</div>
</fieldset>
</fieldset>
<!-- 5 Kontakt -->
<fieldset class="step-later" data-locked>
<legend><span class="f__n" data-num>4</span>Wie erreichen wir Sie?</legend>
<div class="f__row2">
<div class="f__field">
<label class="f__label" for="name">Name <span class="f__req" aria-hidden="true">*</span></label>
<input type="text" id="name" name="name" required autocomplete="name" maxlength="120" aria-describedby="err-name" />
<p class="f__err" id="err-name" data-for="name"></p>
</div>
<div class="f__field">
<label class="f__label" for="telefon">Telefon <span class="f__req" aria-hidden="true">*</span></label>
<input type="tel" id="telefon" name="telefon" required autocomplete="tel" inputmode="tel" pattern="[0-9 +\(\)\/\-]{6,}" maxlength="40" aria-describedby="err-telefon" />
<p class="f__err" id="err-telefon" data-for="telefon"></p>
</div>
</div>
<div class="f__field">
<label class="f__label" for="email">E-Mail <span class="f__opt">(optional)</span></label>
<input type="email" id="email" name="email" autocomplete="email" maxlength="160" aria-describedby="err-email" />
<p class="f__err" id="err-email" data-for="email"></p>
</div>
<fieldset class="f__group">
<legend class="f__sub">Wann dürfen wir zurückrufen?</legend>
<div class="choices choices--inline">
<label class="choice"><input type="radio" name="rueckruf" value="vormittags" /><span class="choice__t">Vormittags</span></label>
<label class="choice"><input type="radio" name="rueckruf" value="nachmittags" /><span class="choice__t">Nachmittags</span></label>
<label class="choice"><input type="radio" name="rueckruf" value="abends" /><span class="choice__t">Abends</span></label>
<label class="choice"><input type="radio" name="rueckruf" value="egal" checked /><span class="choice__t">Egal</span></label>
</div>
</fieldset>
<div class="pricebox" aria-live="polite">
<p class="label">Preisrahmen</p>
<p class="pricebox__v" id="pricebox-v" hidden></p>
<ul class="pricebox__all" id="pricebox-all">
{anliegen.map((a) => <li>{a.label}: {a.hint}</li>)}
</ul>
<p class="small">Richtwerte. Den genauen Preis nennen wir im Gespräch. <a href="/leistungen-preise">Alle Preise</a></p>
</div>
<label class="check">
<input type="checkbox" name="preis_ok" value="on" />
<span>Ich habe den Preisrahmen gesehen und möchte beraten werden.</span>
</label>
<label class="check">
<input type="checkbox" name="datenschutz" value="on" required aria-describedby="err-datenschutz" />
<span>Ich bin einverstanden, dass der Salon meine Angaben und Fotos speichert, um meine Anfrage zu bearbeiten. Mehr in der <a href="/datenschutz">Datenschutzerklärung</a>. <span class="f__req" aria-hidden="true">*</span></span>
</label>
<p class="f__err" id="err-datenschutz" data-for="datenschutz"></p>
<div class="hp" aria-hidden="true">
<label for="website">Website (bitte leer lassen)</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
</div>
<div class="f__submit">
<button type="submit" class="btn" id="f-btn"><span class="f__spin" aria-hidden="true"></span><span class="f__btn-t">Anfrage senden</span><Icon name="arrow" /></button>
<p class="f__help">* Pflichtfeld. Wir melden uns telefonisch, eine Online-Buchung gibt es bewusst nicht.</p>
<p class="f__err" id="f-net" role="alert"></p>
</div>
</fieldset>
</form>
<div id="f-done" class="done" tabindex="-1" hidden>
<span class="done__ico"><Icon name="check" /></span>
<h2 id="f-done-h">Danke, Ihre Anfrage ist da.</h2>
<p id="f-done-p">Wir rufen Sie zurück, um alles Weitere zu besprechen.</p>
<p class="small leise" id="f-done-id"></p>
<a class="more" href="/"><span>Zur Startseite</span><Icon name="arrow" /></a>
</div>
</div>
<aside class="aq__more" aria-label="So geht es weiter">
<ol class="aq__how">
<li><span>1</span>Sie schicken die Anfrage, gern mit Fotos.</li>
<li><span>2</span>Wir rufen Sie zu Ihrer Wunschzeit zurück.</li>
<li><span>3</span>Wir tragen den Termin ein und bestätigen ihn.</li>
</ol>
<div class="aq__regular">
<p class="f__label">Schon Kundin oder Kunde bei uns?</p>
<p class="small">Dann geht es am schnellsten direkt:</p>
<div class="btn-row">
<a class="btn btn--ghost" href={salon.phoneHref}><Icon name="phone" />{salon.phoneDisplay}</a>
<a class="btn btn--ghost" href={salon.whatsappHref} rel="noopener"><Icon name="whatsapp" />WhatsApp</a>
</div>
</div>
</aside>
</div>
</section>
</Base>
<script>
import { initAnfrage } from '../../scripts/anfrage';
initAnfrage();
</script>
<style>
.aq { padding-block: var(--s-7) var(--section); }
.aq__grid { display: grid; gap: var(--s-7); grid-template-areas: 'side' 'main' 'more'; }
.aq__side { grid-area: side; display: grid; gap: var(--s-5); align-content: start; }
.aq__side h1 { font-size: var(--fs-h2); }
.aq__main { grid-area: main; min-width: 0; display: grid; gap: var(--s-5); align-content: start; }
.aq__more { grid-area: more; display: grid; gap: var(--s-6); align-content: start; }
.aq__how { list-style: none; padding: 0; display: grid; gap: var(--s-3); border-top: var(--hair); padding-top: var(--s-5); }
.aq__how li { display: grid; grid-template-columns: 2rem 1fr; align-items: baseline; }
.aq__how span { font-family: var(--font-display); font-style: italic; color: var(--c-petrol); font-size: 1.25rem; }
.aq__regular { display: grid; gap: var(--s-3); padding: var(--s-5); background: var(--c-nebel-hell); border-radius: var(--r-m); }
@media (min-width: 64rem) {
.aq__grid { grid-template-columns: 0.85fr 1.15fr; grid-template-areas: 'side main' 'more main'; grid-template-rows: auto 1fr; column-gap: var(--s-9); row-gap: var(--s-6); }
}
:global(.f__row2), :global(.f__row3) { display: grid; gap: var(--s-5); }
@media (min-width: 40rem) { :global(.f__row2) { grid-template-columns: 1fr 1fr; } }
@media (min-width: 52rem) { :global(.f__row3) { grid-template-columns: 1fr 1.3fr 1fr; } }
:global(.f.is-js .nojs-only) { display: none; }
</style>
+145
View File
@@ -0,0 +1,145 @@
---
import Base from '../layouts/Base.astro';
import PageHero from '../components/PageHero.astro';
import Shot from '../components/Shot.astro';
import Pic from '../components/Pic.astro';
import Reels from '../components/Reels.astro';
import Faq from '../components/Faq.astro';
import PriceList from '../components/PriceList.astro';
import CtaBand from '../components/CtaBand.astro';
import BeforeAfter from '../components/BeforeAfter.astro';
import Icon from '../components/Icon.astro';
import { SITE_URL } from '../data/site';
import redken from '../assets/img/produkte-redken.jpg';
const techniken = [
{ t: 'Balayage', d: 'Die Farbe wird frei Hand aufgemalt. Der Verlauf von dunkel zu hell wirkt weich, der Ansatz wächst unauffällig heraus. Gut für alle, die nicht alle sechs Wochen nachfärben möchten.' },
{ t: 'Foliensträhnen', d: 'Einzelne Partien werden in Folie aufgehellt. Das Ergebnis ist gleichmäßiger und heller als bei einer Balayage und lässt sich gezielt um das Gesicht setzen.' },
{ t: 'Coloration', d: 'Ansatz oder komplette Farbe, deckend oder als Tönung. Auch graue Haare decken wir ab.' },
{ t: 'Glossing', d: 'Eine Pflegetönung für Glanz und den letzten Farbton, oft nach einer Aufhellung. Sie frischt auch eine Balayage zwischen zwei Terminen auf.' },
];
const faq = [
{ q: 'Wie lange dauert eine Balayage?', a: 'Das hängt von Haarlänge, Haardichte und Ausgangsfarbe ab. Rechnen Sie mit mehreren Stunden. Die genaue Dauer sagen wir Ihnen bei der Terminabsprache.' },
{ q: 'Geht Balayage auch auf gefärbtem oder blondiertem Haar?', a: 'Oft ja, aber nicht immer in einem Termin. Schreiben Sie in der Anfrage, wie Ihr Haar vorbehandelt ist, und schicken Sie ein Foto mit. Dann können wir vorher einschätzen, was möglich ist.' },
{ q: 'Mit welchen Produkten arbeiten Sie?', a: 'Mit Farben und Pflege von Redken und Paul Mitchell. Die passende Pflege für zu Hause bekommen Sie bei uns im Salon.' },
{ q: 'Was kostet eine Balayage?', a: 'Eine Balayage beginnt bei 140 Euro inklusive Glossing, Schnitt und Styling (Richtwert). Der genaue Preis hängt von Länge und Aufwand ab, wir nennen ihn Ihnen vorher.' },
];
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faq.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })),
};
const service = {
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Balayage und Haarfarbe',
serviceType: 'Balayage, Foliensträhnen, Coloration, Glossing',
provider: { '@id': `${SITE_URL}/#salon` },
areaServed: 'Tostedt und Umgebung',
};
---
<Base
title="Balayage und Haarfarbe in Tostedt"
description="Balayage, Foliensträhnen, Coloration und Glossing beim Friseur-Team VK in Tostedt, mit Redken und Paul Mitchell. Mit Fotos anfragen, wir beraten vorher."
crumbs={[{ name: 'Balayage & Farbe', href: '/balayage-farbe' }]}
schema={[service, faqSchema]}
>
<PageHero
label="Balayage und Farbe"
title="Farbe, die <em>zu Ihrem Haar passt.</em>"
lead="Wir planen jede Farbe nach Ihrem Ausgangshaar: Naturton, Vorbehandlung, Länge. Dafür hilft ein Foto vom Ist-Zustand schon bei der Anfrage."
>
<div slot="actions" class="btn-row">
<a class="btn" href="/anfrage?anliegen=balayage">Farbe anfragen<Icon name="arrow" /></a>
</div>
<Shot slot="media" arch tone="petrol" title="Balayage, Ergebnis" detail="Rückenansicht, lange Wellen, Tageslicht am Fenster" ratio="4/5" />
</PageHero>
<section class="section section--nebel" aria-labelledby="tech-h">
<div class="wrap">
<div class="head">
<p class="label">Techniken</p>
<h2 id="tech-h">Vier Wege <em>zur Farbe.</em></h2>
</div>
<dl class="tech">
{techniken.map((x, i) => (
<div class="tech__row reveal">
<dt><span class="tech__n display" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>{x.t}</dt>
<dd>{x.d}</dd>
</div>
))}
</dl>
</div>
</section>
<section class="section" aria-labelledby="ba-h">
<div class="wrap ba-grid">
<div class="head">
<p class="label">Vorher und nachher</p>
<h2 id="ba-h">Der Unterschied <em>auf einen Blick.</em></h2>
<p>Ziehen Sie den Regler, um vorher und nachher zu vergleichen. Die Bildpaare entstehen beim Shooting mit echten Kundinnen, die zugestimmt haben.</p>
</div>
<BeforeAfter before="Dunkelblond, heraus­gewachsen" after="Balayage mit Glossing" detail="Fotopaar folgt: gleiche Pose, gleiches Licht, vorher und nachher." />
</div>
</section>
<section class="section--tight brands" aria-labelledby="brands-h">
<div class="wrap brands__grid">
<figure class="brands__img reveal">
<div class="frame ratio-32">
<Pic src={redken} alt="Produktregal mit Redken-Shampoos und -Pflege im Salon" sizes="(min-width: 60rem) 55vw, 100vw" />
</div>
</figure>
<div class="brands__txt">
<p class="label">Produkte</p>
<h2 id="brands-h" class="brands__h">Redken und <em>Paul Mitchell.</em></h2>
<p>Mit diesen beiden Marken färben und pflegen wir im Salon. Welche Pflege zu Ihrer neuen Farbe passt, besprechen wir am Ende des Termins.</p>
</div>
</div>
</section>
<Reels title="Farbe in Bewegung" items={[
{ title: 'Balayage vorher und nachher', detail: 'Drehung im Tageslicht' },
{ title: 'Freihand-Technik', detail: 'Pinsel und Farbe, Nahaufnahme' },
{ title: 'Glossing', detail: 'Glanz nach dem Föhnen' },
{ title: 'Kühles Blond', detail: 'Ergebnis nach Foliensträhnen' },
]} />
<section class="section" aria-labelledby="preise-h">
<div class="wrap split">
<div class="head">
<p class="label">Preise</p>
<h2 id="preise-h">Was es <em>kostet.</em></h2>
<p>Richtwerte für die häufigsten Farbbehandlungen. Bei langem oder sehr dichtem Haar wird es mehr, das sagen wir Ihnen vorher.</p>
</div>
<PriceList ids={['farbe']} headingLevel={3} compact />
</div>
</section>
<Faq items={faq} />
<CtaBand
title="Zeigen Sie uns Ihr Haar."
text="Ein Foto vom Ist-Zustand und eins von Ihrem Wunsch reichen für eine erste Einschätzung. Wir rufen Sie zurück und besprechen Technik, Dauer und Preis."
anliegen="balayage"
button="Farbe anfragen"
/>
</Base>
<style>
.tech { margin-top: var(--s-7); border-top: 1.5px solid var(--c-anthrazit); }
.tech__row { display: grid; gap: var(--s-2); padding-block: var(--s-5); border-bottom: var(--hair); }
.tech__row dt { font-family: var(--font-display); font-size: var(--fs-h3); line-height: 1.15; display: flex; gap: var(--s-4); align-items: baseline; }
.tech__n { font-size: 0.6em; font-style: italic; color: var(--c-petrol); min-width: 2ch; }
.tech__row dd { max-width: 58ch; }
@media (min-width: 52rem) { .tech__row { grid-template-columns: 1fr 1.4fr; gap: var(--s-7); align-items: baseline; } }
.ba-grid { display: grid; gap: var(--s-7); }
@media (min-width: 60rem) { .ba-grid { grid-template-columns: 1fr 1fr; gap: var(--s-9); align-items: center; } }
.brands__grid { display: grid; gap: var(--s-6); }
.brands__img { margin: 0; }
.brands__txt { display: grid; gap: var(--s-4); max-width: 30rem; align-content: center; }
.brands__h { font-size: var(--fs-h3); }
@media (min-width: 60rem) { .brands__grid { grid-template-columns: 1.3fr 1fr; gap: var(--s-8); } .brands__img { margin-left: calc(var(--gutter) * -1); } }
.split { display: grid; gap: var(--s-7); }
@media (min-width: 60rem) { .split { grid-template-columns: 1fr 1.2fr; gap: var(--s-9); align-items: start; } }
</style>
-59
View File
@@ -1,59 +0,0 @@
---
import Base from "../layouts/Base.astro";
---
<Base title="Balayage & Farbe in Tostedt · Friseur-Team VK" description="Balayage, Foliensträhnen und Coloration bei Friseur-Team VK Tostedt – natürliche Farbverläufe mit Redken & Paul Mitchell. Jetzt Farbtermin anfragen.">
<section class="lp-hero">
<div class="container grid">
<div>
<span class="eyebrow"><span class="ln"></span>Farbe & Balayage</span>
<h1>Farbe, die wie Sonne im Haar aussieht.</h1>
<p class="lead">Weiche Verläufe, feine Strähnen, lebendiger Glanz. Wir mischen Ihre Farbe individuell – schonend, hochwertig und so natürlich, dass der Ansatz mitwächst statt aufzufallen.</p>
<div class="pill-row">
<span class="pill">✓ Redken & Paul Mitchell</span>
<span class="pill">✓ Schonende Technik</span>
<span class="pill">✓ Individuell gemischt</span>
</div>
<div class="actions" style="margin-top:28px;display:flex;gap:14px;flex-wrap:wrap">
<a href="/#buchen" class="btn btn-dark">Farbtermin anfragen</a>
<a href="/#galerie" class="btn btn-outline">Ergebnisse ansehen</a>
</div>
</div>
<img src="/img/g3.webp" alt="Balayage bei Friseur-Team VK" />
</div>
</section>
<section class="section">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center"><span class="ln"></span>Unsere Farbleistungen<span class="ln"></span></span>
<h2>Für jeden Typ der richtige Ton</h2>
</div>
<div class="feature-grid">
<div class="fcard reveal"><div class="ic">🌿</div><h3>Balayage</h3><p>Frei Hand gemalte Highlights für natürliche Tiefe – pflegeleicht, weil der Ansatz weich herauswächst.</p></div>
<div class="fcard reveal"><div class="ic">✨</div><h3>Foliensträhnen</h3><p>Präzise Aufhellung für mehr Kontrast und Leuchtkraft – klassisch oder modern abgestuft.</p></div>
<div class="fcard reveal"><div class="ic">💧</div><h3>Glossing & Pflege</h3><p>Farbauffrischung und Glanz-Veredelung – für gesunde, spiegelnde Haaroberfläche.</p></div>
</div>
</div>
</section>
<section class="section bg-soft">
<div class="container">
<div class="banner reveal">
<div>
<span class="eyebrow"><span class="ln"></span>Vorher / Nachher</span>
<h3>Sehen Sie Ergebnisse, bevor Sie kommen.</h3>
<p style="margin-top:10px;color:var(--body)">Auf Instagram zeigen wir laufend echte Vorher/Nachher-Transformationen – als Reel im Hochformat.</p>
</div>
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener" class="btn btn-dark">@friseur.team_vk</a>
</div>
<div class="gal reveal" style="margin-top:32px">
<a href="/#galerie"><img src="/img/g1.webp" alt="Farbergebnis" loading="lazy" /></a>
<a href="/#galerie"><img src="/img/g3.webp" alt="Farbergebnis" loading="lazy" /></a>
<a href="/#galerie"><img src="/img/g4.webp" alt="Farbergebnis" loading="lazy" /></a>
<a href="/#galerie"><img src="/img/hv.jpg" alt="Farbergebnis" loading="lazy" /></a>
</div>
</div>
</section>
</Base>
-53
View File
@@ -1,53 +0,0 @@
---
import Base from "../layouts/Base.astro";
---
<Base title="Brautstyling & Hochsteckfrisuren in Tostedt · Friseur-Team VK" description="Brautstyling mit Probetermin bei Friseur-Team VK Tostedt – entspannt zum perfekten Look für Ihren großen Tag. Jetzt Termin sichern.">
<section class="lp-hero">
<div class="container grid">
<div>
<span class="eyebrow"><span class="ln"></span>Braut & Event</span>
<h1>Ihr schönster Tag beginnt mit dem perfekten Look.</h1>
<p class="lead">Vom Probetermin bis zum großen Moment: Wir planen Ihr Brautstyling in Ruhe, stimmen jedes Detail ab und sorgen dafür, dass am Hochzeitstag alles sitzt – ohne Stress.</p>
<div class="pill-row">
<span class="pill">✓ Probetermin inklusive</span>
<span class="pill">✓ Hochsteckfrisuren</span>
<span class="pill">✓ Termin früh sichern</span>
</div>
<div class="actions" style="margin-top:28px;display:flex;gap:14px;flex-wrap:wrap">
<a href="#anfrage" class="btn btn-dark">Brauttermin sichern</a>
<a href="https://wa.me/4941822889535" target="_blank" rel="noopener" class="btn btn-outline">Per WhatsApp fragen</a>
</div>
</div>
<img src="/img/g6.webp" alt="Brautstyling bei Friseur-Team VK" />
</div>
</section>
<section class="section">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center"><span class="ln"></span>So planen wir<span class="ln"></span></span>
<h2>Entspannt zum großen Tag</h2>
</div>
<div class="feature-grid">
<div class="fcard reveal"><div class="ic">📅</div><h3>1 · Probetermin</h3><p>Wir testen Frisur und Styling in Ruhe – mit Zeit für Wünsche, Accessoires und Anpassungen.</p></div>
<div class="fcard reveal"><div class="ic">💍</div><h3>2 · Der große Tag</h3><p>Pünktlich, entspannt und perfekt vorbereitet – auf Wunsch auch früh morgens vor der Trauung.</p></div>
<div class="fcard reveal"><div class="ic">📸</div><h3>3 · Perfekt auf Fotos</h3><p>Wir stylen so, dass es den ganzen Tag und auf jedem Foto hält.</p></div>
</div>
</div>
</section>
<section class="section bg-soft">
<div class="container">
<div class="banner reveal">
<div>
<span class="eyebrow"><span class="ln"></span>Frühzeitig sichern</span>
<h3 id="anfrage">Hochzeitstermine sind begehrt – sichern Sie Ihren früh.</h3>
<p style="margin-top:10px;color:var(--body)">Brauttermine reservieren wir verbindlich mit einer kleinen Anzahlung. Das hält Ihren Wunschtermin frei und gibt beiden Seiten Planungssicherheit.</p>
</div>
<a href="/#buchen" class="btn btn-dark">Termin anfragen</a>
</div>
</div>
</section>
</Base>
+127
View File
@@ -0,0 +1,127 @@
---
import Base from '../layouts/Base.astro';
import PageHero from '../components/PageHero.astro';
import Shot from '../components/Shot.astro';
import Steps from '../components/Steps.astro';
import Reels from '../components/Reels.astro';
import Faq from '../components/Faq.astro';
import PriceList from '../components/PriceList.astro';
import CtaBand from '../components/CtaBand.astro';
import Icon from '../components/Icon.astro';
import Rating from '../components/Rating.astro';
import { SITE_URL } from '../data/site';
const faq = [
{ q: 'Kommen Sie am Hochzeitstag auch zu mir?', a: 'Ja. Das Styling am Hochzeitstag ist im Salon oder bei Ihnen vor Ort möglich, zum Beispiel zu Hause oder in der Location. Die Anfahrt besprechen wir bei der Beratung.' },
{ q: 'Gibt es einen Probetermin?', a: 'Ja, vor der Hochzeit gibt es einen Probetermin. Dort probieren wir Frisur und auf Wunsch Make-up aus, damit am Hochzeitstag alles sitzt und nichts mehr überraschend ist.' },
{ q: 'Können Begleitpersonen mitgestylt werden?', a: 'Ja. Geben Sie in der Anfrage an, wie viele Personen eine Frisur oder ein Make-up möchten. Dann planen wir die Zeit am Morgen entsprechend.' },
{ q: 'Wie läuft die Anfrage ab?', a: 'Sie schicken uns über das Formular Datum, Ort und Ihre Wünsche, gern mit Inspirationsfotos. Wir rufen Sie zurück, klären die Details und tragen Beratung, Probetermin und Hochzeitstag bei uns ein.' },
{ q: 'Was kostet das Brautstyling?', a: 'Der Probetermin beginnt bei 75 Euro, die Brautfrisur am Hochzeitstag bei 120 Euro (Richtwerte). Den genauen Preis nennen wir Ihnen nach der Beratung, er hängt von Frisur, Haarlänge und Anfahrt ab.' },
];
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faq.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })),
};
const service = {
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Brautstyling',
serviceType: 'Brautfrisur und Braut-Make-up',
provider: { '@id': `${SITE_URL}/#salon` },
areaServed: 'Tostedt und Umgebung',
description: 'Beratung, Probetermin und Styling am Hochzeitstag, im Salon oder vor Ort.',
};
---
<Base
title="Brautservice: Brautfrisur und Make-up in Tostedt"
description="Brautstyling beim Friseur-Team VK in Tostedt: Beratung, Probetermin und Styling am Hochzeitstag, auf Wunsch bei Ihnen vor Ort. Jetzt Anfrage mit Datum und Ort stellen."
crumbs={[{ name: 'Brautservice', href: '/brautservice' }]}
schema={[service, faqSchema]}
>
<PageHero
label="Brautservice"
title="Ihr Hochzeitstag, <em>in Ruhe vorbereitet.</em>"
lead="Frisur und Make-up für die Braut und auf Wunsch für die Begleitung. Wir beginnen mit einem Gespräch, proben die Frisur vorab und sind am Hochzeitstag im Salon oder bei Ihnen vor Ort."
>
<div slot="actions" class="btn-row">
<a class="btn" href="/anfrage?anliegen=braut">Brautstyling anfragen<Icon name="arrow" /></a>
<Rating />
</div>
<Shot slot="media" arch title="Braut-Hochsteckfrisur" detail="Detail Nacken mit Haarschmuck, weiches Fensterlicht" ratio="4/5" />
</PageHero>
<section class="section section--nebel" aria-labelledby="ablauf-h">
<div class="wrap">
<div class="head">
<p class="label">Ablauf</p>
<h2 id="ablauf-h">Drei Termine bis <em>zum Ja.</em></h2>
</div>
<div class="steps-wrap">
<Steps steps={[
{ title: 'Beratung', text: 'Wir sprechen über Kleid, Stil und Haar und schauen uns Ihre Inspirationsbilder an. Dabei klären wir auch, ob wir am Hochzeitstag zu Ihnen kommen.' },
{ title: 'Probetermin', text: 'Wir setzen die Frisur einmal komplett um, auf Wunsch mit Make-up. Was nicht passt, ändern wir an diesem Termin, damit am Morgen der Hochzeit alles sitzt.' },
{ title: 'Der Hochzeitstag', text: 'Im Salon oder vor Ort: Wir planen die Zeit so, dass Braut und Begleitung fertig sind, bevor es losgeht.' },
]} />
</div>
</div>
</section>
<section class="section" aria-labelledby="galerie-h">
<div class="wrap">
<div class="head">
<p class="label">Galerie</p>
<h2 id="galerie-h">Brautfrisuren aus <em>unserem Salon.</em></h2>
<p class="leise">Die Fotos entstehen beim Shooting mit dem Team. Bis dahin zeigen diese Flächen, welche Motive hier stehen werden.</p>
</div>
<div class="gal">
<Shot class="gal__a" title="Hochsteckfrisur" detail="Rückenansicht mit Schleier" ratio="3/4" arch />
<div class="gal__r">
<Shot title="Offene Wellen" detail="Halbseitlich, Haarband" ratio="1/1" />
<Shot title="Detail" detail="Haarschmuck und Flechtwerk, Makro" ratio="1/1" />
<Shot class="gal__c" title="Probetermin" detail="Braut vor dem Spiegel, Team im Hintergrund" ratio="3/2" tone="petrol" />
</div>
</div>
</div>
</section>
<Reels title="Vom Probetermin bis zum großen Tag" items={[
{ title: 'Eine Brautfrisur entsteht', detail: 'Zeitraffer, 20 Sekunden' },
{ title: 'Haarschmuck einsetzen', detail: 'Nahaufnahme Hände' },
{ title: 'Styling vor Ort', detail: 'Morgen der Hochzeit, Location' },
{ title: 'Begleitung', detail: 'Trauzeugin und Brautmutter' },
]} />
<section class="section" aria-labelledby="preise-h">
<div class="wrap split">
<div class="head">
<p class="label">Preise</p>
<h2 id="preise-h">Was es <em>kostet.</em></h2>
<p>Damit Sie vorher wissen, womit Sie rechnen können: Das sind unsere Richtwerte. Den genauen Preis nennen wir nach der Beratung.</p>
</div>
<PriceList ids={['braut']} headingLevel={3} compact />
</div>
</section>
<Faq items={faq} />
<CtaBand
title="Wann und wo wird geheiratet?"
text="Schicken Sie uns Datum, Ort und die Zahl der Personen, gern mit Inspirationsfotos. Wir rufen Sie zurück und planen Beratung, Probetermin und Hochzeitstag mit Ihnen."
anliegen="braut"
button="Brautstyling anfragen"
/>
</Base>
<style>
.steps-wrap { margin-top: var(--s-8); }
.gal { margin-top: var(--s-7); display: grid; gap: var(--s-4); }
.gal__r { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }
.gal__r :global(.gal__c) { grid-column: 1 / -1; }
@media (min-width: 60rem) {
.gal { grid-template-columns: 5fr 7fr; gap: var(--s-5); align-items: end; }
.gal__r { gap: var(--s-5); }
}
.split { display: grid; gap: var(--s-7); }
@media (min-width: 60rem) { .split { grid-template-columns: 1fr 1.2fr; gap: var(--s-9); align-items: start; } }
</style>
+48
View File
@@ -0,0 +1,48 @@
---
import Base from '../layouts/Base.astro';
import DraftNote from '../components/DraftNote.astro';
import { salon } from '../data/site';
---
<Base title="Datenschutz" description="Datenschutzerklärung des Friseur-Team VK in Tostedt: welche Daten die Website und das Anfrageformular verarbeiten." crumbs={[{ name: 'Datenschutz', href: '/datenschutz' }]}>
<section class="section legal">
<div class="wrap">
<p class="label">Rechtliches</p>
<h1>Datenschutz</h1>
<DraftNote text="Entwurf, der beschreibt, was diese Website technisch tatsächlich tut. Er ersetzt keine Rechtsberatung und muss vor dem Livegang geprüft und mit dem Rechtstexte-Dienst des Salons abgeglichen werden. Dazu gehört auch der Vertrag zur Auftragsverarbeitung mit Heidrich Digital." />
<div class="prose">
<h2>Verantwortlich</h2>
<p>{salon.legalName}, Inhaberin {salon.owner}<br />{salon.street}, {salon.zip} {salon.city}<br />Telefon {salon.phoneIntl}, E-Mail <a href={`mailto:${salon.email}`}>{salon.email}</a></p>
<h2>Das Wichtigste in Kürze</h2>
<ul>
<li>Diese Website setzt keine Cookies und nutzt kein Tracking und keine Analysewerkzeuge.</li>
<li>Schriften und Bilder liegen auf unserem eigenen Server. Beim Aufruf werden keine Daten an Google, Meta oder andere Dritte übertragen.</li>
<li>Karten, Instagram, Facebook, WhatsApp und Google-Bewertungen sind nur verlinkt. Erst wenn Sie einen solchen Link anklicken, verlassen Sie unsere Seite.</li>
</ul>
<h2>Hosting und Server-Protokolle</h2>
<p>Die Website liegt auf einem Server in Deutschland, betrieben von Heidrich Digital (Anbieter des Rechenzentrums vor Livegang eintragen). Beim Aufruf speichert der Server technisch notwendige Angaben: IP-Adresse, Datum und Uhrzeit, aufgerufene Seite, Browsertyp. Das dient der sicheren Bereitstellung (Art. 6 Abs. 1 lit. f DSGVO). Die Protokolle werden nach spätestens 14 Tagen gelöscht (Frist bitte bestätigen).</p>
<h2>Anfrageformular</h2>
<p>Wenn Sie eine Anfrage senden, verarbeiten wir die Angaben, die Sie eintragen: Name, Telefonnummer, freiwillig E-Mail, Ihr Anliegen, Angaben zu Haar und Wunschtermin, bei Brautstyling Datum, Ort und Personenzahl sowie bis zu drei Fotos. Wir nutzen diese Daten nur, um Sie zurückzurufen und einen Termin abzusprechen (Art. 6 Abs. 1 lit. b DSGVO, vorvertragliche Maßnahmen). Fotos laden Sie freiwillig hoch, auf Grundlage Ihrer Einwilligung (Art. 6 Abs. 1 lit. a DSGVO).</p>
<p>Die Anfrage wird über einen Dienst von Heidrich Digital auf einem Server in Deutschland entgegengenommen und an den Salon weitergegeben. Heidrich Digital handelt dabei als Auftragsverarbeiter. Anfragen ohne Termin löschen wir nach sechs Monaten, Fotos spätestens nach dem Termin (Fristen bitte bestätigen).</p>
<p>Wenn Sie „Ich kann auch kurzfristig“ ankreuzen, speichern wir Ihre Anfrage auf einer Warteliste und rufen Sie an, wenn ein Termin frei wird. Sie können das jederzeit telefonisch beenden.</p>
<h2>Kundenkarte</h2>
<p>Für die digitale Stempelkarte gelten eigene Hinweise, die Sie bei der Anmeldung sehen. Gespeichert werden Name und Stempelstand, Telefon und E-Mail nur freiwillig. Nachrichten erhalten Sie nur mit gesonderter Einwilligung.</p>
<h2>Kontakt per Telefon, E-Mail oder WhatsApp</h2>
<p>Wenn Sie uns anrufen oder schreiben, verarbeiten wir Ihre Angaben, um Ihr Anliegen zu bearbeiten. Bei WhatsApp gelten zusätzlich die Datenschutzbestimmungen von WhatsApp (Meta). Wenn Sie das nicht möchten, nutzen Sie bitte Telefon oder E-Mail.</p>
<h2>Ihre Rechte</h2>
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit und Widerspruch. Eine Einwilligung können Sie jederzeit für die Zukunft widerrufen. Schreiben Sie dazu an <a href={`mailto:${salon.email}`}>{salon.email}</a>. Außerdem können Sie sich bei einer Datenschutz-Aufsichtsbehörde beschweren, in Niedersachsen bei der Landesbeauftragten für den Datenschutz Niedersachsen.</p>
<p class="small leise">Stand: Entwurf Oktober 2026</p>
</div>
</div>
</section>
</Base>
<style>
.legal .wrap { display: grid; gap: var(--s-6); }
.legal h1 { font-size: var(--fs-h2); }
</style>
+134 -54
View File
@@ -1,71 +1,151 @@
---
import Base from "../layouts/Base.astro";
import Base from '../layouts/Base.astro';
import PageHero from '../components/PageHero.astro';
import Shot from '../components/Shot.astro';
import Pic from '../components/Pic.astro';
import Steps from '../components/Steps.astro';
import Reels from '../components/Reels.astro';
import Faq from '../components/Faq.astro';
import PriceList from '../components/PriceList.astro';
import CtaBand from '../components/CtaBand.astro';
import Icon from '../components/Icon.astro';
import { SITE_URL } from '../data/site';
import greatLengths from '../assets/img/great-lengths.jpg';
const faq = [
{ q: 'Was ist der Unterschied zwischen Verlängerung und Verdichtung?', a: 'Bei der Verlängerung werden Strähnen so eingesetzt, dass das Haar länger wird. Bei der Verdichtung bleibt die Länge, das Haar wird voller. Beides geht mit Great Lengths und auch kombiniert.' },
{ q: 'Warum gibt es vorher einen Beratungstermin?', a: 'Weil Haarstruktur, Farbton und Pflege zusammenpassen müssen. In der Beratung sehen wir uns Ihr Haar an, wählen Farbe und Menge der Strähnen aus und erklären, wie Sie die Verlängerung zu Hause pflegen.' },
{ q: 'Wie lange hält eine Haarverlängerung?', a: 'Das hängt von Ihrem Haarwachstum und der Pflege ab. Nach einigen Monaten setzen wir die Strähnen nach, den Zeitpunkt besprechen wir bei der Beratung.' },
{ q: 'Was kostet das?', a: 'Eine Verdichtung beginnt bei 350 Euro, eine Verlängerung bei 650 Euro (Richtwerte). Der Preis hängt vor allem von der Anzahl der Strähnen ab. Nach der Beratung bekommen Sie einen genauen Preis, bevor etwas bestellt wird.' },
];
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faq.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })),
};
const service = {
'@context': 'https://schema.org',
'@type': 'Service',
name: 'Haarverlängerung und Haarverdichtung mit Great Lengths',
serviceType: 'Haarverlängerung',
brand: { '@type': 'Brand', name: 'Great Lengths' },
provider: { '@id': `${SITE_URL}/#salon` },
areaServed: 'Tostedt und Umgebung',
};
---
<Base title="Haarverlängerung & Haarverdichtung in Tostedt · Friseur-Team VK" description="Premium-Haarverlängerung und Haarverdichtung bei Friseur-Team VK Tostedt – ausführliche Beratung, Echthaar, natürliches Ergebnis. Jetzt Beratungstermin sichern.">
<section class="lp-hero">
<div class="container grid">
<div>
<span class="eyebrow"><span class="ln"></span>Spezialität des Hauses</span>
<h1>Mehr Länge. Mehr Fülle. Natürlich schön.</h1>
<p class="lead">Haarverlängerung und Haarverdichtung sind unser Steckenpferd. Mit hochwertigem Echthaar, ehrlicher Beratung und viel Erfahrung holen wir das Beste aus Ihrem Haar – so, dass niemand es sieht und jeder es bemerkt.</p>
<div class="pill-row">
<span class="pill">✓ Ausführliche Beratung</span>
<span class="pill">✓ Echthaar-Qualität</span>
<span class="pill">✓ Natürliches Ergebnis</span>
</div>
<div class="actions" style="margin-top:28px;display:flex;gap:14px;flex-wrap:wrap">
<a href="#beratung" class="btn btn-dark">Beratungstermin sichern</a>
<a href="/#galerie" class="btn btn-outline">Ergebnisse ansehen</a>
</div>
</div>
<img src="/img/hv.jpg" alt="Haarverlängerung bei Friseur-Team VK" />
<Base
title="Haarverlängerung und Haarverdichtung mit Great Lengths in Tostedt"
description="Haarverlängerung und Haarverdichtung mit Great Lengths beim Friseur-Team VK in Tostedt. Mit Beratungstermin, transparenten Richtpreisen und Nachpflege."
crumbs={[{ name: 'Haarverlängerung', href: '/haarverlaengerung' }]}
schema={[service, faqSchema]}
>
<PageHero
label="Haarverlängerung und -verdichtung"
title="Mehr Länge, <em>mehr Fülle.</em>"
lead="Wir arbeiten mit Great Lengths. Bevor eine Strähne eingesetzt wird, sehen wir uns Ihr Haar in einem Beratungstermin genau an und besprechen Farbe, Menge und Pflege."
>
<div slot="actions" class="btn-row">
<a class="btn" href="/anfrage?anliegen=verlaengerung">Beratung anfragen<Icon name="arrow" /></a>
</div>
</section>
<div slot="media" class="frame arch ratio-45">
<Pic src={greatLengths} eager alt="Great Lengths Schriftzug auf glänzendem, blondem Echthaar" sizes="(min-width: 60rem) 38vw, 88vw" widths={[400, 640, 900, 1080]} />
</div>
</PageHero>
<section class="section">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center"><span class="ln"></span>So läuft's<span class="ln"></span></span>
<h2>Ehrlich beraten, sorgfältig umgesetzt</h2>
<section class="section section--nebel" aria-labelledby="zwei-h">
<div class="wrap">
<div class="head">
<p class="label">Zwei Möglichkeiten</p>
<h2 id="zwei-h">Verlängern oder <em>verdichten.</em></h2>
</div>
<div class="feature-grid">
<div class="fcard reveal"><div class="ic">💬</div><h3>1 · Beratung</h3><p>Wir schauen uns Ihr Haar an, klären Wünsche, Methode, Pflege und Budget – unverbindlich und transparent.</p></div>
<div class="fcard reveal"><div class="ic">💇‍♀️</div><h3>2 · Anwendung</h3><p>Schonende Anbringung mit Echthaar, abgestimmt auf Farbe und Struktur – für ein Ergebnis, das sich echt anfühlt.</p></div>
<div class="fcard reveal"><div class="ic">✨</div><h3>3 · Pflege</h3><p>Sie bekommen einen klaren Pflegeplan und Folgetermine – damit das Ergebnis lange schön bleibt.</p></div>
<div class="two">
<article class="two__item reveal">
<Shot title="Verlängerung, Ergebnis" detail="Schulterlang zu rückenlang, Seitenansicht" ratio="3/2" />
<h3>Haarverlängerung</h3>
<p>Für mehr Länge, zum Beispiel nach einem zu kurzen Schnitt oder für einen besonderen Anlass. Die Strähnen werden farblich an Ihr Haar angepasst.</p>
<p class="num two__p">ab 650 €<sup>*</sup></p>
</article>
<article class="two__item reveal">
<Shot title="Verdichtung, Ergebnis" detail="Feines Haar, vorher und nachher von oben" ratio="3/2" tone="petrol" />
<h3>Haarverdichtung</h3>
<p>Für mehr Fülle bei gleicher Länge, etwa bei feinem Haar oder lichten Stellen. Oft reichen dafür weniger Strähnen als bei einer Verlängerung.</p>
<p class="num two__p">ab 350 €<sup>*</sup></p>
</article>
</div>
</div>
</section>
<section class="section bg-soft">
<div class="container">
<div class="banner reveal">
<div>
<span class="eyebrow"><span class="ln"></span>Beratung sichern</span>
<h3 id="beratung">Kostenlose Erstberatung – mit verbindlichem Termin per WhatsApp.</h3>
<p style="margin-top:10px;color:var(--body)">Für aufwendige Termine sichern wir den Platz mit einer kleinen Anzahlung – das reduziert Ausfälle und hält den Kalender frei für ernsthafte Anfragen.</p>
</div>
<div style="display:flex;flex-direction:column;gap:10px">
<a href="/#buchen" class="btn btn-dark">Termin anfragen</a>
<a href="https://wa.me/4941822889535" target="_blank" rel="noopener" class="btn btn-outline">Per WhatsApp fragen</a>
</div>
<section class="section" aria-labelledby="ablauf-h">
<div class="wrap">
<div class="head">
<p class="label">Ablauf</p>
<h2 id="ablauf-h">Von der Beratung <em>bis zur Nachpflege.</em></h2>
</div>
<div class="steps-wrap">
<Steps steps={[
{ title: 'Beratung', text: 'Wir sehen uns Ihr Haar an, bestimmen den Farbton und die Menge der Strähnen. Danach kennen Sie den Preis.' },
{ title: 'Einarbeitung', text: 'Die Strähnen werden bestellt und an einem eigenen Termin eingesetzt. Danach schneiden wir alles in Form.' },
{ title: 'Pflege und Nachsetzen', text: 'Sie bekommen eine Pflegeanleitung für zu Hause. Wenn das Haar nachgewachsen ist, setzen wir die Strähnen neu.' },
]} />
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="reels-head reveal" style="margin-top:0">
<h3>Vorher / Nachher · 9:16</h3>
<span>Echte Ergebnisse als Reel – ideal für Instagram.</span>
</div>
<div class="reels reveal">
<figure class="reel"><img src="/img/hv.jpg" alt="Reel" loading="lazy" /><span class="lab">Verlängerung</span></figure>
<figure class="reel"><img src="/img/g1.webp" alt="Reel" loading="lazy" /><span class="lab">Vorher / Nachher</span></figure>
<figure class="reel"><img src="/img/g3.webp" alt="Reel" loading="lazy" /><span class="lab">Farbe & Glanz</span></figure>
<figure class="reel"><img src="/img/g4.webp" alt="Reel" loading="lazy" /><span class="lab">Im Salon</span></figure>
</div>
<section class="section--tight" aria-labelledby="pflege-h">
<div class="wrap pflege">
<p class="label">Pflege zu Hause</p>
<h2 id="pflege-h" class="pflege__h">Damit die Verlängerung lange schön bleibt.</h2>
<ul class="pflege__list">
<li>Vor dem Waschen gründlich durchbürsten, am besten mit einer weichen Bürste für Extensions.</li>
<li>Die Ansätze nicht mit Spülung oder Öl einarbeiten.</li>
<li>Nachts locker zu einem Zopf flechten.</li>
<li>Föhnen und Glätten mit Hitzeschutz und nicht direkt an den Verbindungen.</li>
</ul>
<p class="small leise">Welche Produkte zu Ihrem Haar passen, erklären wir beim Termin.</p>
</div>
</section>
<Reels title="Great Lengths im Salon" items={[
{ title: 'Verdichtung, vorher und nachher', detail: 'Draufsicht, Tageslicht' },
{ title: 'Farbabgleich der Strähnen', detail: 'Nahaufnahme am Ansatz' },
{ title: 'Ergebnis in Bewegung', detail: 'Haare werden ausgeschüttelt' },
]} />
<section class="section" aria-labelledby="preise-h">
<div class="wrap split">
<div class="head">
<p class="label">Preise</p>
<h2 id="preise-h">Was es <em>kostet.</em></h2>
<p>Der größte Posten sind die Strähnen selbst. Deshalb nennen wir den genauen Preis erst nach der Beratung, wenn Menge und Länge feststehen.</p>
</div>
<PriceList ids={['verlaengerung']} headingLevel={3} compact />
</div>
</section>
<Faq items={faq} />
<CtaBand
title="Beratungstermin anfragen."
text="Schicken Sie uns ein Foto Ihrer Haare von hinten und schreiben Sie, ob Sie mehr Länge oder mehr Fülle möchten. Wir rufen Sie zurück und vereinbaren die Beratung."
anliegen="verlaengerung"
button="Beratung anfragen"
/>
</Base>
<style>
.two { margin-top: var(--s-7); display: grid; gap: var(--s-8); }
@media (min-width: 52rem) { .two { grid-template-columns: 1fr 1fr; gap: var(--s-7); } .two__item:nth-child(2) { margin-top: var(--s-9); } }
.two__item { display: grid; gap: var(--s-3); align-content: start; }
.two__item h3 { margin-top: var(--s-4); }
.two__item p { max-width: 44ch; }
.two__p { font-weight: 600; }
sup { font-size: 0.75em; color: var(--c-text-leise); }
.steps-wrap { margin-top: var(--s-8); }
.pflege { max-width: 52rem; }
.pflege__h { font-size: var(--fs-h3); margin-top: var(--s-3); }
.pflege__list { margin-block: var(--s-5); padding: 0; list-style: none; counter-reset: p; }
.pflege__list li { counter-increment: p; display: grid; grid-template-columns: 2.5rem 1fr; padding-block: var(--s-3); border-bottom: var(--hair); }
.pflege__list li::before { content: counter(p); font-family: var(--font-display); font-style: italic; color: var(--c-petrol); font-size: 1.25rem; line-height: 1.3; }
.split { display: grid; gap: var(--s-7); }
@media (min-width: 60rem) { .split { grid-template-columns: 1fr 1.2fr; gap: var(--s-9); align-items: start; } }
</style>
+46
View File
@@ -0,0 +1,46 @@
---
import Base from '../layouts/Base.astro';
import DraftNote from '../components/DraftNote.astro';
import { salon } from '../data/site';
---
<Base title="Impressum" description="Impressum des Friseur-Team VK Veronika Sprangel, Kastanienallee 1, 21255 Tostedt." crumbs={[{ name: 'Impressum', href: '/impressum' }]}>
<section class="section legal">
<div class="wrap">
<p class="label">Rechtliches</p>
<h1>Impressum</h1>
<DraftNote text="Dieser Text ist ein ehrlicher Entwurf auf Basis der bisherigen Website. Er muss vor dem Livegang vom Salon geprüft werden, idealerweise über einen Rechtstexte-Dienst. Offene Punkte sind im Text benannt." />
<div class="prose">
<h2>Angaben gemäß § 5 DDG</h2>
<p>{salon.legalName}<br />Inhaberin: {salon.owner}<br />{salon.street}<br />{salon.zip} {salon.city}</p>
<h2>Kontakt</h2>
<p>Telefon: <a href={salon.phoneHref}>{salon.phoneIntl}</a><br />E-Mail: <a href={`mailto:${salon.email}`}>{salon.email}</a></p>
<h2>Berufsrechtliche Angaben</h2>
<p>Berufsbezeichnung: {salon.ownerTitle} (verliehen in der Bundesrepublik Deutschland)<br />
Zuständige Kammer: {salon.chamber}<br />
Eingetragen in der Handwerksrolle, Betriebsnummer {salon.chamberNo}<br />
Berufsrechtliche Regelung: Gesetz zur Ordnung des Handwerks (Handwerksordnung), abrufbar unter <a href="https://www.gesetze-im-internet.de/hwo/" rel="noopener">gesetze-im-internet.de/hwo</a></p>
<h2>Umsatzsteuer</h2>
<p>Eine Umsatzsteuer-Identifikationsnummer wird hier angegeben, sofern vorhanden. Bitte vom Salon bestätigen lassen; die Steuernummer muss nicht im Impressum stehen.</p>
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
<p>{salon.owner}, Anschrift wie oben</p>
<h2>Verbraucherstreitbeilegung</h2>
<p>Wir sind nicht bereit und nicht verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>
<h2>Bildnachweis</h2>
<p>Fotos vom Salon und vom Team: Friseur-Team VK (Fotograf oder Fotografin bitte ergänzen). Bild mit Great-Lengths-Schriftzug: Great Lengths, Nutzungsrecht bitte bestätigen.</p>
<h2>Umsetzung</h2>
<p>Website: Heidrich Digital, Till Heidrich</p>
</div>
</div>
</section>
</Base>
<style>
.legal .wrap { display: grid; gap: var(--s-6); }
.legal h1 { font-size: var(--fs-h2); }
</style>
+280 -251
View File
@@ -1,280 +1,309 @@
---
import Base from "../layouts/Base.astro";
const scissors = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"></circle><circle cx="6" cy="18" r="3"></circle><line x1="20" y1="4" x2="8.12" y2="15.88"></line><line x1="14.47" y1="14.48" x2="20" y2="20"></line><line x1="8.12" y1="8.12" x2="12" y2="12"></line></svg>`;
const services = [
{ img: "/img/g4.webp", h: "Damen – Schnitt & Föhnen", p: "Waschen, individueller Schnitt und Styling – abgestimmt auf Haarstruktur und Typ.", price: "ab 42 €", href: "#buchen" },
{ img: "/img/g5.webp", h: "Herren & Barbershop", p: "Präziser Schnitt, saubere Übergänge, Bartpflege und klassische Rasur.", price: "ab 24 €", href: "#buchen" },
{ img: "/img/g1.webp", h: "Coloration & Strähnen", p: "Glanzvolle Farben und feine Strähnen – schonend mit Redken & Paul Mitchell.", price: "ab 59 €", href: "#buchen" },
{ img: "/img/g3.webp", h: "Balayage & Highlights", p: "Natürliche Farbverläufe und sonnige Akzente für mehr Tiefe und Dimension.", price: "ab 89 €", href: "/balayage" },
{ img: "/img/hv.jpg", h: "Haarverlängerung & -verdichtung", p: "Unsere Spezialität: mehr Länge und Fülle, natürlich schön – mit ausführlicher Beratung.", price: "auf Anfrage", href: "/haarverlaengerung", feature: true },
{ img: "/img/g6.webp", h: "Braut & Hochsteckfrisuren", p: "Festliche Stylings für Hochzeit und besondere Anlässe – inkl. Probetermin.", price: "ab 95 €", href: "/braut" },
import Base from '../layouts/Base.astro';
import Pic from '../components/Pic.astro';
import Shot from '../components/Shot.astro';
import Icon from '../components/Icon.astro';
import Rating from '../components/Rating.astro';
import Reels from '../components/Reels.astro';
import Hours from '../components/Hours.astro';
import CtaBand from '../components/CtaBand.astro';
import LoyaltyCard from '../components/LoyaltyCard.astro';
import { salon, eur } from '../data/site';
import salonInnen1 from '../assets/img/salon-innen-1.jpg';
import salonInnen4 from '../assets/img/salon-innen-4.jpg';
import salonEmpfang from '../assets/img/salon-empfang.jpg';
import teamFoto from '../assets/img/team-gruppenfoto.jpg';
import greatLengths from '../assets/img/great-lengths.jpg';
const alltag = [
{ t: 'Damen', d: 'Schneiden, Föhnen, Hochsteckfrisuren, Dauerwelle', from: 45, href: '/leistungen-preise#preise-damen' },
{ t: 'Herren und Barbershop', d: 'Haarschnitt, Bart trimmen, Nassrasur', from: 26, href: '/leistungen-preise#preise-herren' },
{ t: 'Kinder', d: 'Haarschnitt bis 12 Jahre', from: 18, href: '/leistungen-preise#preise-kinder' },
{ t: 'Augenbrauen und Wimpern', d: 'Zupfen und Färben', from: 10, href: '/leistungen-preise#preise-augen' },
];
---
<Base>
<!-- HERO -->
<section class="hero" id="top">
<div class="container grid">
<div>
<span class="eyebrow"><span class="ln"></span>Ihr Friseur in Tostedt</span>
<h1>Zeit für schöne Haare.</h1>
<p class="lead">Persönliche Beratung, präzise Schnitte und moderne Farbtechniken – mitten in Tostedt. Unser 8-köpfiges Meisterteam nimmt sich Zeit für Sie, Ihren Typ und Ihren Alltag.</p>
<div class="actions">
<a href="#buchen" class="btn btn-dark">Termin buchen</a>
<a href="#leistungen" class="btn btn-outline">Unsere Leistungen</a>
</div>
<div class="stats">
<span>Meisterbetrieb seit 2017</span>
<span class="sep"></span>
<span>Redken &amp; Paul Mitchell</span>
<span class="sep"></span>
<span>Damen · Herren · Kinder</span>
<Base
title="Friseur-Team VK · Friseur in Tostedt für Braut, Balayage und Great Lengths"
description="Meisterbetrieb in der Kastanienallee 1 in Tostedt, seit 2017. Schwerpunkte: Brautstyling, Balayage und Haarverlängerung mit Great Lengths. 4,9 Sterne bei 193 Google-Bewertungen."
>
<!-- Hero -->
<section class="hero">
<div class="wrap hero__grid">
<div class="hero__txt">
<p class="label">Friseur in Tostedt · Meisterbetrieb seit 2017</p>
<h1>Schön von außen, <em>glücklich von innen.</em></h1>
<p class="lead">Wir sind das Friseur-Team VK aus der Kastanienallee 1. Unsere Schwerpunkte sind Brautstyling, Balayage und Haarverlängerung mit Great Lengths. Für Schnitt und Farbe im Alltag sind wir genauso da.</p>
<div class="btn-row hero__btns">
<a class="btn" href="/anfrage">Anfrage stellen<Icon name="arrow" /></a>
<a class="btn btn--ghost" href={salon.phoneHref}><Icon name="phone" />{salon.phoneDisplay}</a>
</div>
<Rating />
</div>
<div class="hero-media">
<img src="/img/hero.webp" alt="Friseur-Team VK Salon in Tostedt" fetchpriority="high" />
<div class="float-card">
<span class="ic" set:html={scissors}></span>
<span style="line-height:1.2"><span style="display:block;font-weight:700;color:var(--ink);font-size:15px">Größtes Team in Tostedt</span><span style="font-size:13px;color:var(--muted)">8 Profis · seit 2017</span></span>
<figure class="hero__media">
<div class="hero__panel" aria-hidden="true"></div>
<div class="frame arch hero__img">
<Pic src={salonInnen1} eager alt="Der helle Salon in der Kastanienallee mit Bedienplätzen und blauen Wandpaneelen" sizes="(min-width: 60rem) 42vw, 92vw" widths={[420, 640, 860, 1100, 1400]} />
</div>
</div>
<figcaption>Der Salon in der Kastanienallee 1</figcaption>
</figure>
</div>
</section>
<!-- INFO STRIP -->
<section class="infostrip">
<div class="container">
<div class="grid">
<div class="col"><h4>Kontakt</h4><p>04182 / 28 89 535<br>info@friseur-team-vk.de</p></div>
<div class="col"><h4>Öffnungszeiten</h4><p>Di–Sa geöffnet<br>Mi &amp; Fr bis 20 Uhr · Mo geschlossen</p></div>
<div class="col"><h4>Standort</h4><p>Kastanienallee 1<br>21255 Tostedt</p></div>
<!-- Schwerpunkte -->
<section class="section focus" aria-labelledby="focus-h">
<div class="wrap">
<div class="head">
<p class="label">Schwerpunkte</p>
<h2 id="focus-h">Braut, Farbe, <em>Länge.</em></h2>
<p class="lead">Drei Leistungen, bei denen wir vor dem ersten Handgriff in Ruhe beraten.</p>
</div>
</div>
</section>
<!-- ABOUT -->
<section id="ueber" class="section">
<div class="container split reveal">
<img class="media" src="/img/salon.webp" alt="Im Salon von Friseur-Team VK" />
<div>
<span class="eyebrow"><span class="ln"></span>Warum Friseur-Team VK?</span>
<h2>Mehr als ein Friseursalon – ein Ort zum Wohlfühlen</h2>
<p class="bodytext">Bei uns stehen Sie im Mittelpunkt. In heller, ruhiger Atmosphäre beraten wir Sie ehrlich, hören zu und finden gemeinsam Schnitt und Farbe, die wirklich zu Ihnen passen. Unser eingespieltes Team aus acht Profis bildet sich regelmäßig weiter – für Handwerk, das man sieht und fühlt.</p>
<div class="checks">
<div class="check"><span class="tick">✓</span>Persönliche Typberatung</div>
<div class="check"><span class="tick">✓</span>Redken &amp; Paul Mitchell</div>
<div class="check"><span class="tick">✓</span>Experten für Haarverlängerung</div>
<div class="check"><span class="tick">✓</span>Damen · Herren · Kinder</div>
</div>
<a href="#leistungen" class="btn btn-dark" style="margin-top:34px">Unsere Leistungen</a>
</div>
</div>
</section>
<!-- SERVICES -->
<section id="leistungen" class="section bg-soft">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center"><span class="ln"></span>Unsere Leistungen<span class="ln"></span></span>
<h2>Alles rund um Ihr Haar</h2>
<p>Vom klassischen Schnitt bis zur aufwendigen Farbe – alles aus einer Hand, mit Sorgfalt und Zeit.</p>
</div>
<div class="svc-grid">
{services.map((s) => (
<a href={s.href} class="scard reveal" style={s.feature ? "border-color:var(--accent)" : ""}>
<div class="imgwrap">
<img src={s.img} alt={s.h} loading="lazy" />
<span class="badge" set:html={scissors}></span>
<ol class="focus__list">
<li class="frow reveal">
<div class="frow__media">
<Shot arch title="Braut-Hochsteckfrisur" detail="Detail Nacken, weiches Fensterlicht" ratio="4/5" />
</div>
<div class="frow__txt">
<span class="frow__n display" aria-hidden="true">01</span>
<h3>Brautservice</h3>
<p>Vom ersten Gespräch über den Probetermin bis zum Styling am Hochzeitstag, Frisur und Make-up aus einer Hand. Auf Wunsch kommen wir am großen Tag zu Ihnen.</p>
<p class="frow__price num">Probetermin ab {eur(75)}<sup>*</sup></p>
<a class="more" href="/brautservice"><span>Zum Brautservice</span><Icon name="arrow" /></a>
</div>
</li>
<li class="frow frow--alt reveal">
<div class="frow__media">
<Shot arch tone="petrol" title="Balayage, Ergebnis" detail="Rückenansicht, lange Wellen, Tageslicht" ratio="4/5" />
</div>
<div class="frow__txt">
<span class="frow__n display" aria-hidden="true">02</span>
<h3>Balayage und Farbe</h3>
<p>Balayage, Foliensträhnen, Coloration und Glossing. Wir planen die Farbe nach Ihrem Ausgangshaar und arbeiten mit Produkten von Redken und Paul Mitchell.</p>
<p class="frow__price num">Balayage ab {eur(140)}<sup>*</sup></p>
<a class="more" href="/balayage-farbe"><span>Zu Balayage und Farbe</span><Icon name="arrow" /></a>
</div>
</li>
<li class="frow reveal">
<div class="frow__media">
<div class="frame arch ratio-45">
<Pic src={greatLengths} alt="Great Lengths Schriftzug auf glänzendem, blondem Echthaar" sizes="(min-width: 60rem) 34vw, 88vw" widths={[400, 640, 900, 1080]} />
</div>
<div class="body">
<h3>{s.h}</h3>
<p>{s.p}</p>
<div class="row">
<span class="price">{s.price}</span>
<span class="more">{s.feature || s.href.startsWith("/") ? "Mehr erfahren →" : "Termin →"}</span>
</div>
</div>
</a>
))}
</div>
<p style="text-align:center;margin-top:26px"><span class="demo-flag">Demo-Richtwerte</span> &nbsp;echte Preise tragen wir gemeinsam ein.</p>
<div class="center-actions">
<a href="#buchen" class="btn btn-dark">Termin buchen</a>
<a href="/treuekarte" class="btn btn-outline">Digitale Treuekarte ansehen</a>
</div>
<div class="frow__txt">
<span class="frow__n display" aria-hidden="true">03</span>
<h3>Haarverlängerung und -verdichtung</h3>
<p>Mehr Länge oder mehr Fülle mit Great Lengths. Vor jeder Verlängerung steht ein Beratungstermin, in dem wir Haarstruktur, Farbton und Pflege besprechen.</p>
<p class="frow__price num">Verdichtung ab {eur(350)}<sup>*</sup></p>
<a class="more" href="/haarverlaengerung"><span>Zur Haarverlängerung</span><Icon name="arrow" /></a>
</div>
</li>
</ol>
<p class="small leise focus__foot">* Richtwerte. Preise werden mit dem Salon abgestimmt.</p>
</div>
</section>
<Reels
title="Arbeiten in Bewegung"
items={[
{ title: 'Balayage vorher und nachher', detail: '15 Sekunden, Drehung im Tageslicht' },
{ title: 'Eine Brautfrisur entsteht', detail: 'Zeitraffer vom Probetermin' },
{ title: 'Great Lengths', detail: 'Ergebnis nach der Verdichtung' },
{ title: 'Herrenschnitt mit Fade', detail: 'Barbershop, Nahaufnahme' },
]}
/>
<!-- Über uns -->
<section class="section about" aria-labelledby="about-h">
<div class="wrap about__grid">
<figure class="about__img reveal">
<div class="frame ratio-32">
<Pic src={teamFoto} alt="Das Team des Friseur-Team VK in schwarzer Kleidung im Salon" sizes="(min-width: 60rem) 58vw, 100vw" />
</div>
<figcaption>Das Team im Salon in der Kastanienallee</figcaption>
</figure>
<div class="about__txt">
<p class="label">Über uns</p>
<h2 id="about-h">Ein Meisterbetrieb, <em>seit 2017.</em></h2>
<p>Veronika Sprangel ist Friseurmeisterin und hat den Salon im November 2017 eröffnet. Heute arbeiten wir zu acht: Friseurinnen und Friseure, ein Barber, ein Auszubildender und Dennis Sprangel, der sich um die Organisation kümmert.</p>
<p>Für Farbe und Pflege nutzen wir Redken und Paul Mitchell, für Haarverlängerungen Great Lengths.</p>
<a class="more" href="/team"><span>Das Team kennenlernen</span><Icon name="arrow" /></a>
</div>
</div>
</section>
<!-- DARK CTA BAND -->
<section class="cta-band">
<div class="container inner">
<!-- Alltag -->
<section class="section section--nebel daily" aria-labelledby="daily-h">
<div class="wrap daily__grid">
<div>
<span class="eyebrow">Bereit für einen frischen Look?</span>
<h2>Vereinbaren Sie jetzt Ihren Termin bei Friseur-Team VK.</h2>
<div class="head">
<p class="label">Für jeden Tag</p>
<h2 id="daily-h">Der Termin zwischendurch.</h2>
</div>
<ul class="daily__list">
{alltag.map((a) => (
<li>
<a href={a.href} class="daily__row">
<span class="daily__t">{a.t}</span>
<span class="daily__d">{a.d}</span>
<span class="daily__p num">ab {eur(a.from)}<sup>*</sup></span>
</a>
</li>
))}
</ul>
<a class="more" href="/leistungen-preise"><span>Alle Leistungen und Preise</span><Icon name="arrow" /></a>
</div>
<div style="display:flex;flex-wrap:wrap;gap:14px;align-items:center">
<a href="#buchen" class="btn btn-light">Termin buchen</a>
<a href="tel:+4941822889535" class="btn btn-outline" style="border-color:rgba(255,255,255,.35);color:#fff">04182 / 28 89 535</a>
<figure class="daily__img reveal">
<div class="frame arch ratio-34">
<Pic src={salonInnen4} alt="Bedienplätze vor großen Fenstern, an der Wand der grüne VK-Schriftzug" sizes="(min-width: 60rem) 36vw, 92vw" widths={[400, 640, 803]} />
</div>
</figure>
</div>
</section>
<!-- Bewertungen -->
<section class="section rating" aria-labelledby="rating-h">
<div class="wrap">
<h2 id="rating-h" class="label">Google-Bewertungen</h2>
<div class="rating__body"><Rating variant="block" /></div>
</div>
</section>
<!-- Kundenkarte -->
<section class="section--tight card-teaser" aria-labelledby="card-h">
<div class="wrap card-teaser__grid">
<div class="card-teaser__visual reveal"><LoyaltyCard stamps={4} /></div>
<div class="card-teaser__txt">
<p class="label">Kundenkarte</p>
<h2 id="card-h">Ihre Stempelkarte, jetzt auch <em>im Handy.</em></h2>
<p>Für jeden Besuch gibt es einen Stempel, beim sechsten ein Pflege-Upgrade gratis. Die Karte liegt in Apple Wallet oder Google Wallet, als Web-Karte im Browser oder weiterhin gedruckt im Portemonnaie. Ihre bisherige Papierkarte gilt weiter, wir übertragen die Stempel.</p>
<a class="more" href="/kundenkarte"><span>So funktioniert die Kundenkarte</span><Icon name="arrow" /></a>
</div>
</div>
</section>
<!-- TREUEKARTE TEASER -->
<section class="section" id="treue">
<div class="container wallet-grid">
<div class="reveal">
<span class="eyebrow"><span class="ln"></span>Treuekarte – digital</span>
<h2 style="margin:16px 0 0;font-size:clamp(28px,3.4vw,42px)">Die Stempelkarte, die man nie vergisst.</h2>
<p class="bodytext">Die Papier-Stempelkarte – jetzt direkt im Handy. Ein Klick, und die Karte liegt in Apple Wallet oder Google Wallet. Beim Bezahlen einfach zeigen, wir stempeln digital. Optional mit Push, sobald die Karte voll ist.</p>
<div class="wallet-btns">
<button class="wbtn" data-wallet> Apple Wallet</button>
<button class="wbtn g" data-wallet>Google Wallet</button>
<a href="/treuekarte" class="btn btn-outline btn-sm">So funktioniert's →</a>
<!-- Besuch -->
<section class="section visit" aria-labelledby="visit-h">
<div class="wrap visit__grid">
<figure class="visit__img reveal">
<div class="frame ratio-32">
<Pic src={salonEmpfang} alt="Empfangstresen des Salons mit Produktregal und Eingangstür" sizes="(min-width: 60rem) 50vw, 100vw" />
</div>
</div>
<div class="reveal">
<div class="pass">
<div class="ptop"><span class="pbrand">Friseur-Team VK<span class="s">Tostedt</span></span><span class="chip">Treuekarte</span></div>
<span class="plabel">Ihre Stempel</span>
<div class="preward">Jeder 10. Schnitt – Pflege gratis 🎁</div>
<div class="stamps">
<div class="stamp on">✂</div><div class="stamp on">✂</div><div class="stamp on">✂</div><div class="stamp on">✂</div><div class="stamp on">✂</div>
<div class="stamp on">✂</div><div class="stamp on">✂</div><div class="stamp">✂</div><div class="stamp">✂</div><div class="stamp">🎁</div>
</div>
<div class="pfoot"><span>7 / 10 Stempel</span><span>Kastanienallee 1</span></div>
</figure>
<div class="visit__txt">
<p class="label">Besuch</p>
<h2 id="visit-h">Kastanienallee 1, <em>Tostedt.</em></h2>
<address>
{salon.legalName}<br />{salon.street}<br />{salon.zip} {salon.city}
</address>
<Hours />
<div class="btn-row">
<a class="btn btn--ghost" href={salon.routeUrl} rel="noopener"><Icon name="pin" />Route planen</a>
<a class="btn btn--ghost" href={salon.whatsappHref} rel="noopener"><Icon name="whatsapp" />WhatsApp</a>
</div>
<p class="small leise">Öffnungszeiten werden vor dem Livegang mit dem Salon bestätigt.</p>
</div>
</div>
</section>
<!-- BUCHEN -->
<section class="section bg-dark" id="buchen">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center" style="color:var(--accent-2)"><span class="ln"></span>Online buchen<span class="ln"></span></span>
<h2 style="color:#fff">Termin buchen – rund um die Uhr</h2>
<p style="color:rgba(255,255,255,.7)">In drei Schritten zum Termin. Im Live-System öffnet der Button eure <b style="color:#fff">e-cut Online-Buchung</b> – der Termin landet direkt im e-cut-Kalender, ganz ohne Systemwechsel.</p>
</div>
<div class="booker reveal" style="margin-top:42px">
<div class="steps-nav"><div class="step-dot active" data-dot="0"></div><div class="step-dot" data-dot="1"></div><div class="step-dot" data-dot="2"></div></div>
<div class="step active" data-step="0">
<h3>1 · Leistung wählen</h3>
<div class="opt-grid">
<div class="opt" data-pick="svc" data-val="Damenschnitt"><span class="o-name">Damenschnitt</span><span class="o-meta">ca. 60 Min</span></div>
<div class="opt" data-pick="svc" data-val="Herren / Barber"><span class="o-name">Herren / Barber</span><span class="o-meta">ca. 40 Min</span></div>
<div class="opt" data-pick="svc" data-val="Balayage & Farbe"><span class="o-name">Balayage &amp; Farbe</span><span class="o-meta">ca. 150 Min</span></div>
<div class="opt" data-pick="svc" data-val="Beratung Verlängerung"><span class="o-name">Beratung Verlängerung</span><span class="o-meta">ca. 30 Min</span></div>
</div>
</div>
<div class="step" data-step="1">
<h3>2 · Termin wählen</h3>
<p style="color:rgba(255,255,255,.6);font-size:.85rem;margin-bottom:1rem">Freie Zeiten · Mittwoch (bis 20 Uhr)</p>
<div class="slots">
<div class="slot" data-pick="time" data-val="09:30">09:30</div><div class="slot" data-pick="time" data-val="11:00">11:00</div>
<div class="slot" data-pick="time" data-val="13:30">13:30</div><div class="slot" data-pick="time" data-val="15:00">15:00</div>
<div class="slot" data-pick="time" data-val="16:30">16:30</div><div class="slot" data-pick="time" data-val="17:15">17:15</div>
<div class="slot" data-pick="time" data-val="18:30">18:30</div><div class="slot" data-pick="time" data-val="19:15">19:15</div>
</div>
</div>
<div class="step confirm" data-step="2" style="text-align:center">
<div class="ok">✓</div>
<h3 style="text-align:center">Fast geschafft!</h3>
<p style="color:rgba(255,255,255,.7)">Ihre Anfrage <b style="color:var(--accent-2)" id="sumLine">…</b> geht an den Salon. Sie erhalten die Bestätigung per WhatsApp – und 24 h vorher eine Erinnerung zum Bestätigen oder Absagen.</p>
</div>
<div class="book-foot">
<div class="book-summary">Auswahl: <b id="sumSvc">–</b> · <b id="sumTime">–</b></div>
<div>
<button class="btn btn-outline btn-sm" id="backBtn" style="color:#fff;border-color:rgba(255,255,255,.3);display:none">Zurück</button>
<button class="btn btn-accent btn-sm" id="nextBtn">Weiter</button>
</div>
</div>
<p class="note-line">Demo-Ablauf · im Live-System angebunden an eure e-cut Online-Buchung (Termine direkt im e-cut-Kalender)</p>
</div>
</div>
</section>
<!-- STANDORT -->
<section id="standort" class="section">
<div class="container split rev reveal">
<div>
<span class="eyebrow"><span class="ln"></span>Standort</span>
<h2 id="kontakt">Besuchen Sie uns in Tostedt</h2>
<p class="bodytext">Mitten im Ort, gut erreichbar und mit Parkplätzen in der Nähe. Rufen Sie an oder schreiben Sie uns – wir finden den passenden Termin für Sie.</p>
<div class="checks" style="grid-template-columns:1fr 1fr">
<div><h4 class="eyebrow-label">Adresse</h4><p style="margin-top:8px;color:var(--text)">Kastanienallee 1<br>21255 Tostedt</p></div>
<div><h4 class="eyebrow-label">Kontakt</h4><p style="margin-top:8px;color:var(--text)">04182 / 28 89 535<br>info@friseur-team-vk.de</p></div>
<div><h4 class="eyebrow-label">Öffnungszeiten</h4><p style="margin-top:8px;color:var(--text)">Di · Do 9–18 · Mi · Fr 9–20<br>Sa 9–14 Uhr</p></div>
<div><h4 class="eyebrow-label">Ruhetag</h4><p style="margin-top:8px;color:var(--text)">Montag</p></div>
</div>
<div style="margin-top:34px;display:flex;flex-wrap:wrap;gap:14px">
<a href="tel:+4941822889535" class="btn btn-dark">Jetzt anrufen</a>
<a href="https://maps.google.com/?q=Kastanienallee+1+21255+Tostedt" target="_blank" rel="noopener" class="btn btn-outline">Route planen</a>
</div>
</div>
<img class="media" src="/img/g2.webp" alt="Friseur-Team VK Tostedt" />
</div>
</section>
<!-- GALERIE + REELS -->
<section id="galerie" class="section bg-soft">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center"><span class="ln"></span>Galerie<span class="ln"></span></span>
<h2>Eindrücke & Ergebnisse</h2>
<p>Ein Blick in Salon und Handwerk. Bald hier: Ihr eigenes Foto- &amp; Video-Shooting.</p>
</div>
<div class="gal reveal">
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener"><img src="/img/g1.webp" alt="Eindruck" loading="lazy" /></a>
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener"><img src="/img/g3.webp" alt="Eindruck" loading="lazy" /></a>
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener"><img src="/img/salon.webp" alt="Eindruck" loading="lazy" /></a>
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener"><img src="/img/g4.webp" alt="Eindruck" loading="lazy" /></a>
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener"><img src="/img/g6.webp" alt="Eindruck" loading="lazy" /></a>
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener"><img src="/img/g5.webp" alt="Eindruck" loading="lazy" /></a>
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener"><img src="/img/hv.jpg" alt="Eindruck" loading="lazy" /></a>
<a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener"><img src="/img/hero.webp" alt="Eindruck" loading="lazy" /></a>
</div>
<div class="reels-head reveal">
<h3>Reels &amp; Motion · 9:16</h3>
<span>Hochformat-Content fürs Handy – direkt für Instagram &amp; TikTok.</span>
</div>
<div class="reels reveal">
<figure class="reel"><img src="/img/g1.webp" alt="Reel" loading="lazy" /><span class="lab">Vorher / Nachher</span></figure>
<figure class="reel"><img src="/img/hero.webp" alt="Reel" loading="lazy" /><span class="lab">Salon-Tour</span></figure>
<figure class="reel"><img src="/img/g3.webp" alt="Reel" loading="lazy" /><span class="lab">Styling-Clip</span></figure>
<figure class="reel"><img src="/img/g2.webp" alt="Reel" loading="lazy" /><span class="lab">Behind the Scenes</span></figure>
</div>
<div style="text-align:center;margin-top:28px"><span class="demo-flag">Demo · hier laufen später echte 9:16-Clips</span><div style="margin-top:14px"><a href="https://www.instagram.com/friseur.team_vk/" target="_blank" rel="noopener" class="btn btn-dark">@friseur.team_vk folgen</a></div></div>
</div>
</section>
<!-- FAQ -->
<section class="section">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center"><span class="ln"></span>FAQ<span class="ln"></span></span>
<h2>Häufige Fragen</h2>
</div>
<div class="faq reveal">
<details><summary>Muss ich vorher anrufen?</summary><div class="ans">Nein. Sie können rund um die Uhr online anfragen – auch abends und am Wochenende. Telefonisch erreichen Sie uns natürlich weiterhin zu den Öffnungszeiten.</div></details>
<details><summary>Welche Produkte verwendet ihr?</summary><div class="ans">Wir arbeiten mit Redken und Paul Mitchell – hochwertige Marken für Schnitt, Pflege und Coloration.</div></details>
<details><summary>Macht ihr auch Haarverlängerungen?</summary><div class="ans">Ja, das ist eine unserer Spezialitäten – Haarverlängerung und Haarverdichtung mit ausführlicher Beratung. <a href="/haarverlaengerung" style="color:var(--accent);font-weight:600">Mehr erfahren →</a></div></details>
<details><summary>Bekomme ich eine Erinnerung an meinen Termin?</summary><div class="ans">Ja. Vor dem Termin erhalten Sie eine Erinnerung – künftig per WhatsApp mit der Möglichkeit, direkt zu bestätigen oder abzusagen.</div></details>
<details><summary>Wie funktioniert die digitale Treuekarte?</summary><div class="ans">Sie fügen die Karte einmal zu Apple oder Google Wallet hinzu. Bei jedem Besuch stempeln wir digital – der zehnte Schnitt bringt eine Pflege gratis. <a href="/treuekarte" style="color:var(--accent);font-weight:600">So funktioniert's →</a></div></details>
<details><summary>Wo finde ich euch?</summary><div class="ans">Kastanienallee 1, 21255 Tostedt. Parkmöglichkeiten sind in der Nähe vorhanden.</div></details>
</div>
</div>
</section>
<script is:inline>
let step=0;const picks={svc:null,time:null};
const steps=document.querySelectorAll('.step'),dots=document.querySelectorAll('.step-dot');
const nextBtn=document.getElementById('nextBtn'),backBtn=document.getElementById('backBtn');
const sumSvc=document.getElementById('sumSvc'),sumTime=document.getElementById('sumTime');
function render(){steps.forEach((s,i)=>s.classList.toggle('active',i===step));dots.forEach((d,i)=>d.classList.toggle('active',i<=step));backBtn.style.display=step===0?'none':'inline-flex';nextBtn.textContent=step===2?'Anfrage senden':'Weiter';if(step===2){document.getElementById('sumLine').textContent=`${picks.svc||'Termin'} · Mi ${picks.time||''}`}}
document.querySelectorAll('[data-pick]').forEach(el=>el.addEventListener('click',()=>{const t=el.dataset.pick;el.parentElement.querySelectorAll('[data-pick]').forEach(x=>x.classList.remove('sel'));el.classList.add('sel');picks[t]=el.dataset.val;if(t==='svc')sumSvc.textContent=picks.svc;if(t==='time')sumTime.textContent=picks.time}));
nextBtn.addEventListener('click',()=>{if(step===0&&!picks.svc){sumSvc.textContent='bitte wählen';return}if(step===1&&!picks.time){sumTime.textContent='bitte wählen';return}if(step===2){window.__toast('Anfrage gesendet – Demo ✨');return}step++;render()});
backBtn.addEventListener('click',()=>{if(step>0){step--;render()}});
render();
</script>
<CtaBand />
</Base>
<style>
/* Hero */
.hero { padding-block: var(--s-6) var(--section); overflow: hidden; }
.hero__grid { display: grid; gap: var(--s-7); }
.hero__txt { display: grid; gap: var(--s-5); align-content: center; justify-items: start; }
.hero__txt h1 { font-size: var(--fs-h1); max-width: 11ch; }
.hero__txt h1 em { display: inline; }
.hero__btns { margin-top: var(--s-2); }
.hero__media { position: relative; margin: 0; padding: 0 0 var(--s-5) var(--s-5); }
.hero__panel { position: absolute; left: 0; bottom: var(--s-7); top: 22%; width: 52%; background: var(--c-petrol); }
.hero__img { position: relative; aspect-ratio: 4 / 5; }
.hero__img :global(img) { object-position: 38% 50%; }
.hero__media figcaption { position: relative; }
@media (min-width: 60rem) {
.hero { padding-top: var(--s-8); }
.hero__grid { grid-template-columns: 1.08fr 0.92fr; gap: var(--s-8); align-items: center; }
.hero__img { aspect-ratio: 5 / 6; }
}
@media (prefers-reduced-motion: no-preference) {
.hero__txt > * { animation: h-rise var(--t-slow) var(--ease-out) both; }
.hero__txt > h1 { animation-name: h-rise-t; animation-delay: 60ms; }
.hero__txt > :nth-child(3) { animation-delay: 140ms; }
.hero__txt > :nth-child(4) { animation-delay: 220ms; }
.hero__txt > :nth-child(5) { animation-delay: 300ms; }
.hero__panel { animation: h-grow 1.2s var(--ease-out) both 150ms; transform-origin: bottom; }
@keyframes h-rise { from { opacity: 0; transform: translateY(18px); } }
@keyframes h-rise-t { from { transform: translateY(16px); } }
@keyframes h-grow { from { transform: scaleY(0); } }
}
/* Schwerpunkte */
.focus { padding-top: 0; }
.focus__list { list-style: none; padding: 0; margin-top: var(--s-8); display: grid; gap: var(--s-9); }
.frow { display: grid; gap: var(--s-6); align-items: center; }
.frow__media { max-width: 30rem; width: 100%; }
.frow__txt { display: grid; gap: var(--s-3); justify-items: start; max-width: 34rem; }
.frow__n { font-size: var(--fs-h2); font-style: italic; color: var(--c-petrol); line-height: 1; }
.frow h3 { font-size: var(--fs-h2); line-height: 1.05; }
.frow__price { font-weight: 600; margin-top: var(--s-2); }
sup { font-size: 0.75em; color: var(--c-text-leise); margin-left: 1px; }
@media (min-width: 60rem) {
.frow { grid-template-columns: 5fr 6fr; gap: var(--s-9); }
.frow--alt .frow__media { order: 2; justify-self: end; }
.frow--alt { grid-template-columns: 6fr 5fr; }
.frow--alt .frow__txt { justify-self: end; }
}
.focus__foot { margin-top: var(--s-8); }
/* Über uns */
.about__grid { display: grid; gap: var(--s-6); }
.about__img { margin: 0; }
.about__txt { display: grid; gap: var(--s-4); justify-items: start; max-width: 32rem; }
.about__txt h2 { margin-bottom: var(--s-2); }
@media (min-width: 60rem) {
.about__grid { grid-template-columns: 7fr 5fr; gap: var(--s-8); align-items: end; }
.about__img { margin-left: calc(var(--gutter) * -1); }
}
/* Alltag */
.daily__grid { display: grid; gap: var(--s-7); }
.daily__list { list-style: none; padding: 0; margin-block: var(--s-6) var(--s-4); border-top: 1.5px solid var(--c-anthrazit); }
.daily__row {
display: grid; grid-template-columns: 1fr auto; gap: 0.15rem var(--s-4);
padding-block: var(--s-4); border-bottom: var(--hair);
color: inherit; text-decoration: none;
}
.daily__row:hover .daily__t { color: var(--c-petrol); }
.daily__t { font-family: var(--font-display); font-size: var(--fs-h4); line-height: 1.25; transition: color var(--t-fast); }
.daily__d { grid-column: 1; color: var(--c-text-leise); font-size: var(--fs-small); }
.daily__p { grid-column: 2; grid-row: 1 / span 2; align-self: center; font-weight: 600; white-space: nowrap; }
.daily__img { margin: 0; max-width: 26rem; }
@media (min-width: 60rem) {
.daily__grid { grid-template-columns: 1.3fr 1fr; gap: var(--s-9); align-items: center; }
.daily__img { justify-self: end; width: 100%; }
}
/* Bewertungen */
.rating__body { margin-top: var(--s-5); }
/* Kundenkarte */
.card-teaser { padding-block: var(--section); background: linear-gradient(180deg, var(--c-papier) 0, var(--c-papier) 30%, var(--c-petrol-hell) 30%); }
.card-teaser__grid { display: grid; gap: var(--s-7); align-items: center; }
.card-teaser__visual { display: grid; justify-items: center; }
.card-teaser__txt { display: grid; gap: var(--s-4); justify-items: start; max-width: 34rem; }
@media (min-width: 60rem) {
.card-teaser { background: linear-gradient(90deg, var(--c-petrol-hell) 0, var(--c-petrol-hell) 46%, var(--c-papier) 46%); }
.card-teaser__grid { grid-template-columns: 1fr 1.1fr; gap: var(--s-9); }
}
/* Besuch */
.visit__grid { display: grid; gap: var(--s-7); }
.visit__img { margin: 0; }
.visit__txt { display: grid; gap: var(--s-5); justify-items: start; }
.visit__txt address { font-style: normal; }
@media (min-width: 60rem) {
.visit__grid { grid-template-columns: 1.1fr 1fr; gap: var(--s-9); align-items: start; }
.visit__img { margin-left: calc(var(--gutter) * -1); }
}
</style>
+97
View File
@@ -0,0 +1,97 @@
---
import Base from '../layouts/Base.astro';
import Pic from '../components/Pic.astro';
import Hours from '../components/Hours.astro';
import Icon from '../components/Icon.astro';
import Rating from '../components/Rating.astro';
import { salon } from '../data/site';
import salonInnen3 from '../assets/img/salon-innen-3.jpg';
---
<Base
title="Kontakt und Anfahrt"
description="Friseur-Team VK, Kastanienallee 1, 21255 Tostedt. Telefon 04182 2889535, WhatsApp und E-Mail. Öffnungszeiten Dienstag bis Samstag, Route planen."
crumbs={[{ name: 'Kontakt', href: '/kontakt' }]}
bar="kontakt"
>
<section class="kt">
<div class="wrap kt__grid">
<div class="kt__txt">
<p class="label">Kontakt und Anfahrt</p>
<h1>So erreichen <em>Sie uns.</em></h1>
<ul class="kt__ways">
<li>
<a href={salon.phoneHref} class="way">
<Icon name="phone" /><span><span class="way__k">Telefon</span><span class="way__v">{salon.phoneDisplay}</span></span>
</a>
</li>
<li>
<a href={salon.whatsappHref} rel="noopener" class="way">
<Icon name="whatsapp" /><span><span class="way__k">WhatsApp</span><span class="way__v">Nachricht schreiben</span></span>
</a>
</li>
<li>
<a href={`mailto:${salon.email}`} class="way">
<Icon name="mail" /><span><span class="way__k">E-Mail</span><span class="way__v">{salon.email}</span></span>
</a>
</li>
<li>
<a href="/anfrage" class="way">
<Icon name="arrow" /><span><span class="way__k">Für Braut, Farbe, Verlängerung</span><span class="way__v">Anfrage stellen</span></span>
</a>
</li>
</ul>
<p class="small leise">Termine vergeben wir am Telefon. Wenn wir gerade alle Hände voll zu tun haben, klappt das nicht immer beim ersten Klingeln. Dann schreiben Sie uns gern per WhatsApp.</p>
</div>
<div class="kt__info">
<div class="kt__block">
<h2 class="label">Adresse</h2>
<address class="kt__addr">{salon.legalName}<br />{salon.street}<br />{salon.zip} {salon.city}</address>
<div class="btn-row">
<a class="btn btn--ghost" href={salon.routeUrl} rel="noopener"><Icon name="pin" />Google Maps</a>
<a class="btn btn--ghost" href={salon.appleMapsUrl} rel="noopener"><Icon name="pin" />Apple Karten</a>
</div>
<p class="small leise">Die Knöpfe öffnen die Route in Ihrer Karten-App. Eine eingebettete Karte gibt es hier bewusst nicht, damit keine Daten an Dritte gehen.</p>
</div>
<div class="kt__block">
<h2 class="label">Öffnungszeiten</h2>
<Hours />
<p class="small leise">Stand März 2026, wird vor dem Livegang bestätigt.</p>
</div>
<div class="kt__block">
<h2 class="label">Bewertungen</h2>
<Rating />
</div>
</div>
</div>
</section>
<section class="section--tight">
<div class="wrap">
<figure class="kt__img">
<div class="frame ratio-169">
<Pic src={salonInnen3} alt="Blick durch den Salon mit Bedienplätzen und dem Aufsteller mit dem Salon-Logo" sizes="(min-width: 84rem) 80rem, 100vw" />
</div>
<figcaption>Der Salon von innen</figcaption>
</figure>
</div>
</section>
</Base>
<style>
.kt { padding-block: var(--s-7) var(--s-8); }
.kt__grid { display: grid; gap: var(--s-8); }
.kt__txt { display: grid; gap: var(--s-6); align-content: start; }
.kt__ways { list-style: none; padding: 0; border-top: 1.5px solid var(--c-anthrazit); }
.kt__ways li { border-bottom: var(--hair); }
.way { display: flex; gap: var(--s-4); align-items: center; min-height: 4.5rem; padding-block: var(--s-3); color: var(--c-anthrazit); text-decoration: none; }
.way > :global(svg) { width: 1.5rem; height: 1.5rem; color: var(--c-petrol); flex: none; }
.way > span { display: grid; }
.way__k { font-size: var(--fs-small); color: var(--c-text-leise); }
.way__v { font-family: var(--font-display); font-size: var(--fs-h4); line-height: 1.25; overflow-wrap: anywhere; }
.way:hover .way__v { color: var(--c-petrol); }
.kt__info { display: grid; gap: var(--s-7); align-content: start; }
.kt__block { display: grid; gap: var(--s-4); justify-items: start; }
.kt__addr { font-style: normal; font-size: var(--fs-lead); line-height: 1.5; }
.kt__img { margin: 0; }
@media (min-width: 60rem) { .kt__grid { grid-template-columns: 1.2fr 1fr; gap: var(--s-9); } .kt__info { padding-top: var(--s-6); } }
</style>
+142
View File
@@ -0,0 +1,142 @@
---
import Base from '../layouts/Base.astro';
import LoyaltyCard from '../components/LoyaltyCard.astro';
import Faq from '../components/Faq.astro';
import Icon from '../components/Icon.astro';
import { KARTE_START, KARTE_DEMO, salon } from '../data/site';
const wege = [
{ t: 'Apple Wallet', d: 'Auf dem iPhone landet die Karte in der Wallet-App, ohne zusätzliche App. Der Stempelstand aktualisiert sich nach jedem Besuch von selbst.' },
{ t: 'Google Wallet', d: 'Auf Android-Handys genauso: Karte einmal hinzufügen, an der Kasse zeigen, fertig.' },
{ t: 'Web-Karte', d: 'Ein persönlicher Link, der im Browser funktioniert. Auf Wunsch legen Sie ihn als Symbol auf den Startbildschirm.' },
{ t: 'Gedruckte Karte', d: 'Im Visitenkartenformat mit Ihrem persönlichen QR-Code, für alle, die lieber etwas in der Hand haben.' },
];
const faq = [
{ q: 'Was passiert mit meiner Papier-Stempelkarte?', a: 'Sie gilt weiter. Bringen Sie sie beim nächsten Besuch mit, dann übertragen wir Ihre Stempel auf das neue Konto. Verloren geht nichts.' },
{ q: 'Brauche ich dafür eine App?', a: 'Nein. Apple Wallet und Google Wallet sind auf den meisten Handys schon installiert. Ohne Wallet nutzen Sie die Web-Karte oder die gedruckte Karte.' },
{ q: 'Kann ich mehrere Wege gleichzeitig nutzen?', a: 'Ja. Wallet, Web-Karte und gedruckte Karte gehören zu einem Konto. Egal, was Sie an der Kasse zeigen, der Stempel landet am selben Ort.' },
{ q: 'Welche Daten werden gespeichert?', a: 'Für die Karte brauchen wir Ihren Namen und Ihre Stempel. Telefonnummer oder E-Mail sind freiwillig. Nachrichten von uns bekommen Sie nur, wenn Sie das ausdrücklich erlauben, und Sie können die Karte jederzeit löschen lassen.' },
{ q: 'Was bekomme ich für sechs Stempel?', a: 'Beim sechsten Stempel gibt es ein Pflege-Upgrade gratis, zum Beispiel eine intensivere Pflegebehandlung zu Ihrem nächsten Termin.' },
];
const faqSchema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faq.map((f) => ({ '@type': 'Question', name: f.q, acceptedAnswer: { '@type': 'Answer', text: f.a } })),
};
---
<Base
title="Kundenkarte: die digitale Stempelkarte"
description="Die Kundenkarte vom Friseur-Team VK: Stempel sammeln in Apple Wallet, Google Wallet, als Web-Karte oder gedruckt. Sechs Stempel, ein Pflege-Upgrade gratis. Papierkarten gelten weiter."
crumbs={[{ name: 'Kundenkarte', href: '/kundenkarte' }]}
schema={[faqSchema]}
>
<section class="khero">
<div class="wrap khero__grid">
<div class="khero__txt">
<p class="label">Kundenkarte</p>
<h1>Sechs Stempel, <em>ein Pflege-Upgrade.</em></h1>
<p class="lead">Unsere Stempelkarte gibt es jetzt auch digital. Bei jedem Besuch scannen wir Ihre Karte an der Kasse, beim sechsten Stempel ist ein Pflege-Upgrade gratis.</p>
<div class="btn-row">
<a class="btn" href={KARTE_START} rel="noopener">Karte holen<Icon name="arrow" /></a>
<a class="btn btn--ghost" href={KARTE_DEMO} rel="noopener">So sieht’s aus</a>
</div>
</div>
<div class="khero__card">
<div class="khero__panel" aria-hidden="true"></div>
<LoyaltyCard stamps={4} />
</div>
</div>
</section>
<section class="section" aria-labelledby="wege-h">
<div class="wrap">
<div class="head">
<p class="label">Ein Konto, vier Wege</p>
<h2 id="wege-h">Sie entscheiden, <em>wo die Karte liegt.</em></h2>
</div>
<ol class="wege">
{wege.map((w, i) => (
<li class="wege__item reveal">
<span class="wege__n display" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
<h3>{w.t}</h3>
<p>{w.d}</p>
</li>
))}
</ol>
</div>
</section>
<section class="section section--nebel" aria-labelledby="so-h">
<div class="wrap so">
<div class="head">
<p class="label">So geht’s</p>
<h2 id="so-h">Einmal anmelden, <em>dann nur noch zeigen.</em></h2>
</div>
<ol class="so__list">
<li><p><strong>Anmelden.</strong> Am Tresen liegt ein QR-Code. Scannen, Namen eingeben, Weg auswählen. Das geht auch vorab über den Knopf „Karte holen“.</p></li>
<li><p><strong>Stempeln.</strong> Zeigen Sie die Karte an der Kasse. Wir scannen sie, der Stempel ist sofort da.</p></li>
<li><p><strong>Einlösen.</strong> Beim sechsten Stempel bekommen Sie Ihr Pflege-Upgrade. Danach beginnt die Karte von vorn.</p></li>
</ol>
</div>
</section>
<section class="section" aria-labelledby="papier-h">
<div class="wrap papier">
<div>
<p class="label">Papierkarte</p>
<h2 id="papier-h" class="papier__h">Ihre alte Karte <em>gilt weiter.</em></h2>
</div>
<p class="lead">Bringen Sie Ihre Papier-Stempelkarte zum nächsten Termin mit. Wir übertragen die Stempel auf Ihr neues Konto, und Sie sammeln dort weiter.</p>
</div>
</section>
<section class="section--tight" aria-labelledby="ds-h">
<div class="wrap ds">
<h2 id="ds-h" class="label">Datenschutz</h2>
<p>Für die Karte speichern wir nur, was dafür nötig ist: Ihren Namen und Ihre Stempel. Telefonnummer und E-Mail sind freiwillig. Werbung oder Erinnerungen schicken wir nur mit Ihrer ausdrücklichen, getrennten Einwilligung, die Sie jederzeit widerrufen können. Auf Wunsch löschen wir Ihr Konto vollständig. Fragen dazu beantworten wir unter <a href={`mailto:${salon.email}`}>{salon.email}</a>; Details stehen in der <a href="/datenschutz">Datenschutzerklärung</a>.</p>
</div>
</section>
<Faq items={faq} />
<section class="section--tight kend">
<div class="wrap kend__in">
<h2 class="kend__h">Bereit für den ersten Stempel?</h2>
<div class="btn-row">
<a class="btn" href={KARTE_START} rel="noopener">Karte holen<Icon name="arrow" /></a>
<a class="btn btn--ghost" href={KARTE_DEMO} rel="noopener">So sieht’s aus</a>
</div>
</div>
</section>
</Base>
<style>
.khero { padding-block: var(--s-7) var(--section); overflow: hidden; }
.khero__grid { display: grid; gap: var(--s-8); align-items: center; }
.khero__txt { display: grid; gap: var(--s-5); justify-items: start; }
.khero__txt h1 { max-width: 12ch; }
.khero__card { position: relative; display: grid; justify-items: center; padding-block: var(--s-7); }
.khero__panel { position: absolute; inset: 0 10% 0 10%; background: var(--c-petrol); border-radius: var(--arch); }
.khero__card :global(.lcard) { position: relative; }
@media (min-width: 60rem) { .khero__grid { grid-template-columns: 1.1fr 0.9fr; gap: var(--s-9); } }
@media (prefers-reduced-motion: no-preference) {
.khero__card :global(.lcard) { animation: float-in 1s var(--ease-out) both 120ms; }
@keyframes float-in { from { opacity: 0; transform: translateY(28px) rotate(-2deg); } }
}
.wege { list-style: none; padding: 0; margin-top: var(--s-7); display: grid; gap: var(--s-6); }
@media (min-width: 52rem) { .wege { grid-template-columns: repeat(2, 1fr); gap: var(--s-7) var(--s-8); } }
.wege__item { border-top: 1.5px solid var(--c-anthrazit); padding-top: var(--s-4); display: grid; gap: var(--s-2); }
.wege__n { font-style: italic; color: var(--c-petrol); font-size: var(--fs-h4); }
.wege__item p { max-width: 44ch; }
.so { display: grid; gap: var(--s-7); }
@media (min-width: 60rem) { .so { grid-template-columns: 1fr 1.2fr; gap: var(--s-9); } }
.so__list { padding: 0; list-style: none; counter-reset: s; display: grid; gap: var(--s-5); }
.so__list li { counter-increment: s; display: grid; grid-template-columns: 3rem 1fr; }
.so__list li::before { content: counter(s); font-family: var(--font-display); font-size: 2rem; line-height: 1; color: var(--c-petrol); font-style: italic; }
.papier { display: grid; gap: var(--s-5); }
.papier__h { font-size: var(--fs-h2); margin-top: var(--s-3); }
@media (min-width: 60rem) { .papier { grid-template-columns: 1fr 1fr; gap: var(--s-9); align-items: end; } }
.ds { max-width: 50rem; display: grid; gap: var(--s-4); }
.kend__in { display: grid; gap: var(--s-5); justify-items: start; padding-bottom: var(--s-7); }
.kend__h { font-size: var(--fs-h3); }
</style>
+92
View File
@@ -0,0 +1,92 @@
---
import Base from '../layouts/Base.astro';
import PriceList from '../components/PriceList.astro';
import CtaBand from '../components/CtaBand.astro';
import Pic from '../components/Pic.astro';
import Icon from '../components/Icon.astro';
import { priceGroups, salon } from '../data/site';
import herren from '../assets/img/produkte-herren.jpg';
const premium = priceGroups.filter((g) => ['braut', 'farbe', 'verlaengerung'].includes(g.id));
const links: Record<string, string> = { braut: '/brautservice', farbe: '/balayage-farbe', verlaengerung: '/haarverlaengerung' };
---
<Base
title="Leistungen und Preise"
description="Leistungen und Richtpreise beim Friseur-Team VK in Tostedt: Brautservice, Balayage und Farbe, Haarverlängerung mit Great Lengths, Damen, Herren und Barbershop, Kinder, Augenbrauen und Wimpern."
crumbs={[{ name: 'Leistungen & Preise', href: '/leistungen-preise' }]}
>
<section class="intro">
<div class="wrap intro__grid">
<div>
<p class="label">Leistungen und Preise</p>
<h1>Was wir machen, <em>und was es kostet.</em></h1>
</div>
<div class="intro__side">
<p class="lead">Alle Preise sind „ab“-Werte. Wie viel es am Ende wird, hängt von Haarlänge, Dichte und Aufwand ab. Den genauen Preis nennen wir Ihnen vor der Behandlung.</p>
<nav aria-label="Leistungsbereiche" class="jump">
{priceGroups.map((g) => <a href={`#preise-${g.id}`}>{g.title}</a>)}
</nav>
</div>
</div>
</section>
<section class="section premium" aria-labelledby="prem-h">
<div class="wrap">
<h2 id="prem-h" class="label">Schwerpunkte</h2>
<div class="premium__grid">
{premium.map((g) => (
<div class="premium__col">
<PriceList ids={[g.id]} headingLevel={3} />
<a class="more" href={links[g.id]}><span>Mehr zu {g.title}</span><Icon name="arrow" /></a>
</div>
))}
</div>
</div>
</section>
<section class="section section--nebel" aria-labelledby="alltag-h">
<div class="wrap alltag">
<div class="alltag__side">
<h2 id="alltag-h" class="label">Für jeden Tag</h2>
<figure class="alltag__img">
<div class="frame ratio-32">
<Pic src={herren} alt="Regal mit Pflege- und Stylingprodukten für Herren auf einer blauen Felge" sizes="(min-width: 60rem) 30vw, 100vw" widths={[400, 640, 954]} />
</div>
<figcaption>Barbershop-Ecke im Salon</figcaption>
</figure>
</div>
<PriceList ids={['damen', 'herren', 'kinder', 'augen']} headingLevel={3} />
</div>
</section>
<section class="section--tight">
<div class="wrap note">
<p>Sie finden Ihre Leistung nicht? Rufen Sie uns an unter <a href={salon.phoneHref}>{salon.phoneDisplay}</a> oder fragen Sie über das <a href="/anfrage?anliegen=sonstiges">Anfrageformular</a>.</p>
</div>
</section>
<CtaBand />
</Base>
<style>
.intro { padding-block: var(--s-7) 0; }
.intro__grid { display: grid; gap: var(--s-6); }
.intro h1 { margin-top: var(--s-4); max-width: 13ch; }
.intro__side { display: grid; gap: var(--s-5); align-content: end; }
@media (min-width: 60rem) { .intro__grid { grid-template-columns: 1.2fr 1fr; gap: var(--s-9); align-items: end; } }
.jump { display: flex; flex-wrap: wrap; gap: var(--s-2); }
.jump a {
display: inline-flex; align-items: center; min-height: var(--tap); padding: 0 1rem;
border: 1px solid var(--c-linie); border-radius: var(--r-pill);
color: var(--c-anthrazit); text-decoration: none; font-size: var(--fs-small); font-stretch: 90%;
}
.jump a:hover { border-color: var(--c-petrol); color: var(--c-petrol); }
.premium__grid { margin-top: var(--s-6); display: grid; gap: var(--s-8); }
@media (min-width: 64rem) { .premium__grid { grid-template-columns: repeat(3, 1fr); gap: var(--s-7); } }
.premium__col { display: grid; gap: var(--s-4); align-content: start; }
.premium__col :global(.plist__foot) { display: none; }
.alltag { display: grid; gap: var(--s-7); }
.alltag__img { margin: var(--s-5) 0 0; max-width: 28rem; }
@media (min-width: 60rem) { .alltag { grid-template-columns: 1fr 1.4fr; gap: var(--s-9); } .alltag__side { position: sticky; top: 7rem; align-self: start; } }
.note { max-width: 50rem; font-size: var(--fs-lead); }
</style>
+101
View File
@@ -0,0 +1,101 @@
---
import Base from '../layouts/Base.astro';
import Pic from '../components/Pic.astro';
import Shot from '../components/Shot.astro';
import CtaBand from '../components/CtaBand.astro';
import Rating from '../components/Rating.astro';
import { team, salon, SITE_URL } from '../data/site';
import teamFoto from '../assets/img/team-gruppenfoto.jpg';
import salonInnen2 from '../assets/img/salon-innen-2.jpg';
const people = team.map((p) => ({
'@context': 'https://schema.org',
'@type': 'Person',
name: p.name,
jobTitle: p.role,
worksFor: { '@id': `${SITE_URL}/#salon` },
}));
---
<Base
title="Team: Ihre Friseurinnen und Friseure in Tostedt"
description="Das Friseur-Team VK in Tostedt: Inhaberin und Friseurmeisterin Veronika Sprangel und ihr Team. Meisterbetrieb seit November 2017."
crumbs={[{ name: 'Team', href: '/team' }]}
schema={people}
>
<section class="intro">
<div class="wrap">
<p class="label">Team</p>
<h1>Die Menschen <em>hinter dem Stuhl.</em></h1>
</div>
<figure class="intro__img">
<div class="frame intro__frame">
<Pic src={teamFoto} eager alt="Das Team des Friseur-Team VK in schwarzer Kleidung im Salon" sizes="100vw" widths={[640, 960, 1280, 1700, 2000]} />
</div>
<figcaption class="wrap">Das Team im Salon in der Kastanienallee 1</figcaption>
</figure>
</section>
<section class="section owner" aria-labelledby="owner-h">
<div class="wrap owner__grid">
<Shot arch title="Veronika Sprangel" detail="Porträt am Bedienplatz, Blick zur Kamera" ratio="4/5" class="owner__img" />
<div class="owner__txt">
<p class="label">Inhaberin</p>
<h2 id="owner-h">Veronika <em>Sprangel.</em></h2>
<p class="lead">Friseurmeisterin, seit {salon.since} mit eigenem Salon in Tostedt.</p>
<p>Unter ihrer Leitung arbeitet das Team als Meisterbetrieb, eingetragen bei der {salon.chamber}. Für Farbe und Pflege setzt der Salon auf Redken und Paul Mitchell, für Haarverlängerungen auf Great Lengths.</p>
<Rating />
</div>
</div>
</section>
<section class="section section--nebel" aria-labelledby="team-h">
<div class="wrap">
<div class="head">
<p class="label">Das Team</p>
<h2 id="team-h">Wer Sie <em>bei uns trifft.</em></h2>
<p class="leise">Die Einzelporträts entstehen beim nächsten Shooting.</p>
</div>
<ul class="people">
{team.slice(1).map((p) => (
<li class="person reveal">
<Shot title="Porträt folgt" detail={p.name} ratio="3/4" arch />
<h3 class="person__n">{p.name}</h3>
<p class="person__r">{p.role}</p>
</li>
))}
</ul>
</div>
</section>
<section class="section--tight" aria-label="Salon">
<div class="wrap salon">
<div class="frame ratio-169 salon__img">
<Pic src={salonInnen2} alt="Waschplätze im Salon, daneben ein Aufsteller mit dem Spruch: Schön von außen, glücklich von innen" sizes="(min-width: 84rem) 80rem, 100vw" />
</div>
<p class="salon__q display">„Schön von außen, glücklich von innen.“</p>
<p class="small leise">Der Satz steht im Salon auf dem Aufsteller am Waschplatz.</p>
</div>
</section>
<CtaBand />
</Base>
<style>
.intro { padding-top: var(--s-7); }
.intro h1 { margin-top: var(--s-4); max-width: 14ch; }
.intro__img { margin: var(--s-7) 0 0; }
.intro__frame { aspect-ratio: 4 / 3; }
@media (min-width: 48rem) { .intro__frame { aspect-ratio: 21 / 9; } }
.intro__frame :global(img) { object-position: 50% 8%; }
.owner__grid { display: grid; gap: var(--s-7); }
.owner__grid :global(.owner__img) { max-width: 26rem; }
.owner__txt { display: grid; gap: var(--s-4); justify-items: start; max-width: 34rem; align-content: center; }
@media (min-width: 60rem) { .owner__grid { grid-template-columns: 1fr 1.2fr; gap: var(--s-9); } }
.people { list-style: none; padding: 0; margin-top: var(--s-7); display: grid; gap: var(--s-6) var(--s-4); grid-template-columns: repeat(2, 1fr); }
@media (min-width: 52rem) { .people { grid-template-columns: repeat(4, 1fr); gap: var(--s-7) var(--s-5); } .person:nth-child(even) { margin-top: var(--s-7); } }
.person__n { font-size: var(--fs-h4); margin-top: var(--s-4); line-height: 1.2; }
.person__r { color: var(--c-text-leise); font-size: var(--fs-small); margin-top: var(--s-1); }
.salon { display: grid; gap: var(--s-4); justify-items: center; text-align: center; }
.salon__img { width: 100%; }
.salon__q { font-size: var(--fs-h2); font-style: italic; color: var(--c-petrol); margin-top: var(--s-6); max-width: 18ch; }
</style>
-64
View File
@@ -1,64 +0,0 @@
---
import Base from "../layouts/Base.astro";
---
<Base title="Digitale Treuekarte · Apple & Google Wallet · Friseur-Team VK" description="Die Stempelkarte von Friseur-Team VK – digital in Apple Wallet & Google Wallet. Jeder 10. Schnitt eine Pflege gratis. Nie wieder Karte vergessen.">
<section class="lp-hero">
<div class="container grid">
<div>
<span class="eyebrow"><span class="ln"></span>Treuekarte – digital</span>
<h1>Die Stempelkarte, die man nie vergisst.</h1>
<p class="lead">Ihre Papier-Stempelkarte – jetzt direkt im Handy. Einmal hinzufügen, beim Bezahlen zeigen, wir stempeln digital. Der zehnte Schnitt bringt eine Pflege gratis.</p>
<div class="wallet-btns">
<button class="wbtn" data-wallet> Zu Apple Wallet</button>
<button class="wbtn g" data-wallet>Zu Google Wallet</button>
</div>
<p style="margin-top:14px"><span class="demo-flag">Demo</span> &nbsp;im Live-System fügt ein Tap die echte Wallet-Karte hinzu.</p>
</div>
<div>
<div class="pass">
<div class="ptop"><span class="pbrand">Friseur-Team VK<span class="s">Tostedt</span></span><span class="chip">Treuekarte</span></div>
<span class="plabel">Ihre Stempel</span>
<div class="preward">Jeder 10. Schnitt – Pflege gratis 🎁</div>
<div class="stamps">
<div class="stamp on">✂</div><div class="stamp on">✂</div><div class="stamp on">✂</div><div class="stamp on">✂</div><div class="stamp on">✂</div>
<div class="stamp on">✂</div><div class="stamp on">✂</div><div class="stamp">✂</div><div class="stamp">✂</div><div class="stamp">🎁</div>
</div>
<div class="pfoot"><span>7 / 10 Stempel</span><span>Kastanienallee 1</span></div>
</div>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center"><span class="ln"></span>So funktioniert's<span class="ln"></span></span>
<h2>In drei Schritten zur digitalen Karte</h2>
</div>
<div class="how">
<div class="step reveal"><div class="n">1</div><h4>Hinzufügen</h4><p>Gast scannt einen QR-Code im Salon oder tippt auf der Website – die Karte landet in Apple oder Google Wallet.</p></div>
<div class="step reveal"><div class="n">2</div><h4>Stempeln</h4><p>Nach jedem Besuch stempeln wir digital. Der Stand aktualisiert sich automatisch auf dem Handy des Gasts.</p></div>
<div class="step reveal"><div class="n">3</div><h4>Belohnen</h4><p>Ist die Karte voll, gibt's die Prämie – und optional eine Push-Nachricht „Ihre Karte ist voll".</p></div>
</div>
</div>
</section>
<section class="section bg-soft">
<div class="container">
<div class="head-center reveal">
<span class="eyebrow center"><span class="ln"></span>Vernetzt & self-hosted<span class="ln"></span></span>
<h2>Karte, Website und Salon – ein System</h2>
<p>Alles läuft auf eigener Infrastruktur, DSGVO-konform. Kein teurer Drittanbieter pro Karte.</p>
</div>
<div class="how">
<div class="step reveal"><div class="n">🌐</div><h4>Website</h4><p>„Zur Wallet hinzufügen"-Button + QR-Code im Salon erzeugen die Karte für jeden Gast.</p></div>
<div class="step reveal"><div class="n">🔄</div><h4>Pass-Server</h4><p>Ein kleiner Dienst auf eurem Hetzner-Server erstellt und aktualisiert die Pässe (Apple PassKit + Google Wallet) und schickt Push bei neuem Stempel.</p></div>
<div class="step reveal"><div class="n">💇‍♀️</div><h4>Salon-Ansicht</h4><p>Veronika stempelt mit einem Tap (Tablet/Handy) – verknüpft mit der Kundenkartei. Optional an die Buchung gekoppelt.</p></div>
</div>
<p style="text-align:center;margin-top:28px;color:var(--body);max-width:60ch;margin-left:auto;margin-right:auto">Eine echte Apple-Wallet-Karte wird mit einem Apple-Developer-Zertifikat signiert (einmalige Einrichtung). Danach läuft alles automatisch auf eurer Infrastruktur.</p>
<div style="text-align:center;margin-top:24px"><a href="/#buchen" class="btn btn-dark">Klingt gut? Termin buchen</a></div>
</div>
</section>
</Base>
+225
View File
@@ -0,0 +1,225 @@
/**
* Anfrageformular: schrittweises Einblenden, Prüfung, Versand per fetch.
* Ohne JavaScript schickt das Formular normal ab (Backend antwortet mit 303 auf /anfrage/danke).
*/
const MAX_FILES = 3;
const MAX_BYTES = 6 * 1024 * 1024;
const RUECKRUF: Record<string, string> = {
vormittags: 'vormittags',
nachmittags: 'nachmittags',
abends: 'abends',
egal: '',
};
const fmtMB = (b: number) =>
b < 1024 * 1024
? Math.max(1, Math.round(b / 1024)) + ' KB'
: (b / 1024 / 1024).toLocaleString('de-DE', { maximumFractionDigits: 1 }) + ' MB';
export function initAnfrage() {
const form = document.querySelector<HTMLFormElement>('#anfrage');
if (!form) return;
form.classList.add('is-js');
form.noValidate = true; // eigene, verständliche Fehlermeldungen
const summary = document.querySelector<HTMLElement>('#f-summary')!;
const done = document.querySelector<HTMLElement>('#f-done')!;
const btn = form.querySelector<HTMLButtonElement>('#f-btn')!;
const net = form.querySelector<HTMLElement>('#f-net')!;
const fileInput = form.querySelector<HTMLInputElement>('#fotos')!;
const fileList = form.querySelector<HTMLUListElement>('#files')!;
const later = [...form.querySelectorAll<HTMLElement>('.step-later')];
const braut = form.querySelector<HTMLElement>('[data-only="braut"]')!;
const priceV = form.querySelector<HTMLElement>('#pricebox-v')!;
const priceAll = form.querySelector<HTMLElement>('#pricebox-all')!;
const datum = form.querySelector<HTMLInputElement>('#hochzeitsdatum');
if (datum) datum.min = new Date().toISOString().slice(0, 10);
// --- Schritt 1 steuert, was danach kommt ---
const update = (scroll = false) => {
const sel = form.querySelector<HTMLInputElement>('input[name="anliegen"]:checked');
if (!sel) return;
const isBraut = sel.value === 'braut';
later.forEach((el) => el.removeAttribute('data-locked'));
braut.hidden = !isBraut;
// Nummern der Schritte anpassen
let n = isBraut ? 3 : 2;
form.querySelectorAll<HTMLElement>('[data-num]').forEach((el) => (el.textContent = String(n++)));
priceV.textContent = `${sel.dataset.label}: ${sel.dataset.hint}`;
priceV.hidden = false;
priceAll.hidden = true;
if (scroll) {
const next = isBraut ? braut : later[1];
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
next?.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' });
}
};
form.querySelectorAll<HTMLInputElement>('input[name="anliegen"]').forEach((r) =>
r.addEventListener('change', () => {
const first = !later[0].hasAttribute('data-locked') ? false : true;
update(first);
clearErr('anliegen');
})
);
// Vorauswahl über ?anliegen=braut
const pre = new URLSearchParams(location.search).get('anliegen');
if (pre) {
const r = form.querySelector<HTMLInputElement>(`input[name="anliegen"][value="${CSS.escape(pre)}"]`);
if (r) { r.checked = true; update(false); }
} else if (form.querySelector('input[name="anliegen"]:checked')) {
update(false); // z. B. nach "Zurück" im Browser
}
// --- Fehleranzeige ---
const errEl = (name: string) => form.querySelector<HTMLElement>(`.f__err[data-for="${name}"]`);
const setErr = (name: string, msg: string) => {
const e = errEl(name);
if (e) e.textContent = msg;
form.querySelectorAll<HTMLInputElement>(`[name="${name}"]`).forEach((i) => i.setAttribute('aria-invalid', 'true'));
};
const clearErr = (name: string) => {
const e = errEl(name);
if (e) e.textContent = '';
form.querySelectorAll<HTMLInputElement>(`[name="${name}"]`).forEach((i) => i.removeAttribute('aria-invalid'));
};
['name', 'telefon', 'email', 'datenschutz'].forEach((n) =>
form.querySelector(`[name="${n}"]`)?.addEventListener('input', () => clearErr(n))
);
// --- Dateien prüfen ---
const checkFiles = (): string | null => {
const files = [...(fileInput.files ?? [])];
fileList.innerHTML = '';
let msg: string | null = null;
if (files.length > MAX_FILES) msg = `Bitte höchstens ${MAX_FILES} Fotos auswählen (gewählt: ${files.length}).`;
files.forEach((f) => {
const li = document.createElement('li');
const tooBig = f.size > MAX_BYTES;
const notImg = f.type !== '' && !f.type.startsWith('image/');
const a = document.createElement('span');
a.textContent = f.name;
const b = document.createElement('span');
b.textContent = tooBig ? `${fmtMB(f.size)}, zu groß` : notImg ? 'kein Bild' : fmtMB(f.size);
li.append(a, b);
if (tooBig || notImg) {
li.className = 'is-bad';
msg ??= tooBig ? `„${f.name}“ ist größer als 6 MB. Bitte ein kleineres Foto wählen.` : `„${f.name}“ ist kein Bild.`;
}
fileList.append(li);
});
return msg;
};
fileInput.addEventListener('change', () => {
const m = checkFiles();
m ? setErr('fotos', m) : clearErr('fotos');
});
// --- Prüfung beim Absenden ---
const validate = () => {
const errors: { name: string; msg: string; target: string }[] = [];
const v = (n: string) => (form.elements.namedItem(n) as HTMLInputElement | null)?.value.trim() ?? '';
if (!form.querySelector('input[name="anliegen"]:checked'))
errors.push({ name: 'anliegen', msg: 'Bitte wählen Sie aus, worum es geht.', target: 'input[name="anliegen"]' });
if (!v('name')) errors.push({ name: 'name', msg: 'Bitte geben Sie Ihren Namen an.', target: '#name' });
const tel = v('telefon');
if (!tel) errors.push({ name: 'telefon', msg: 'Bitte geben Sie eine Telefonnummer an, damit wir zurückrufen können.', target: '#telefon' });
else if (!/^[0-9 +()/\-]{6,}$/.test(tel)) errors.push({ name: 'telefon', msg: 'Die Telefonnummer sieht unvollständig aus. Bitte nur Ziffern, Leerzeichen, + oder /.', target: '#telefon' });
const mail = v('email');
if (mail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(mail)) errors.push({ name: 'email', msg: 'Die E-Mail-Adresse ist nicht vollständig.', target: '#email' });
if (!(form.elements.namedItem('datenschutz') as HTMLInputElement).checked)
errors.push({ name: 'datenschutz', msg: 'Bitte stimmen Sie der Verarbeitung Ihrer Angaben zu.', target: 'input[name="datenschutz"]' });
const fm = checkFiles();
if (fm) errors.push({ name: 'fotos', msg: fm, target: '#fotos' });
return errors;
};
const showSummary = (errors: { name: string; msg: string; target: string }[]) => {
const ul = summary.querySelector('ul')!;
ul.innerHTML = '';
errors.forEach((e) => {
setErr(e.name, e.msg);
const li = document.createElement('li');
const a = document.createElement('a');
a.href = '#';
a.textContent = e.msg;
a.addEventListener('click', (ev) => {
ev.preventDefault();
form.querySelector<HTMLElement>(e.target)?.focus();
});
li.append(a);
ul.append(li);
});
summary.hidden = false;
summary.focus();
};
// --- Absenden ---
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
net.textContent = '';
['anliegen', 'name', 'telefon', 'email', 'datenschutz', 'fotos'].forEach(clearErr);
const errors = validate();
if (errors.length) {
later.forEach((el) => el.removeAttribute('data-locked'));
showSummary(errors);
return;
}
summary.hidden = true;
const data = new FormData(form);
// Honeypot: von Bots ausgefüllt. Nicht senden, aber freundlich bestätigen.
if (String(data.get('website') ?? '') !== '') {
showDone(data, null);
return;
}
// Nicht sichtbare Braut-Felder nicht mitsenden
if (braut.hidden) ['hochzeitsdatum', 'ort', 'personen'].forEach((k) => data.delete(k));
btn.setAttribute('aria-busy', 'true');
btn.disabled = true;
const t = btn.querySelector('.f__btn-t')!;
t.textContent = 'Wird gesendet …';
try {
const res = await fetch(form.dataset.endpoint || form.action, {
method: 'POST',
body: data,
headers: { Accept: 'application/json' },
mode: 'cors',
credentials: 'omit',
});
let json: { ok?: boolean; id?: string; error?: string } = {};
try { json = await res.json(); } catch { /* keine JSON-Antwort */ }
if (res.ok && json.ok) {
showDone(data, json.id ?? null);
} else {
net.textContent = json.error
? `Das hat nicht geklappt: ${json.error}`
: 'Das hat nicht geklappt. Bitte versuchen Sie es noch einmal oder rufen Sie uns an: 04182 2889535.';
}
} catch {
net.textContent = 'Keine Verbindung. Bitte prüfen Sie Ihr Internet und versuchen Sie es noch einmal, oder rufen Sie uns an: 04182 2889535.';
} finally {
btn.removeAttribute('aria-busy');
btn.disabled = false;
t.textContent = 'Anfrage senden';
}
});
function showDone(data: FormData, id: string | null) {
const name = String(data.get('name') ?? '').trim().split(' ')[0];
const tel = String(data.get('telefon') ?? '').trim();
const rr = RUECKRUF[String(data.get('rueckruf') ?? 'egal')] ?? '';
const wl = data.get('warteliste') === 'on';
document.querySelector('#f-done-h')!.textContent = name ? `Danke, ${name}. Ihre Anfrage ist da.` : 'Danke, Ihre Anfrage ist da.';
document.querySelector('#f-done-p')!.textContent =
`Wir rufen Sie ${rr ? rr + ' ' : ''}unter ${tel} zurück und besprechen alles Weitere.` +
(wl ? ' Wenn kurzfristig ein Termin frei wird, melden wir uns auch früher.' : '');
document.querySelector('#f-done-id')!.textContent = id ? `Ihre Anfragenummer: ${id}` : '';
form!.hidden = true;
summary.hidden = true;
done.hidden = false;
done.focus();
done.scrollIntoView({ block: 'start' });
}
}
+137
View File
@@ -0,0 +1,137 @@
/* Formular-Bausteine (Anfrage) */
.f { display: grid; gap: var(--s-7); }
.f fieldset { border: 0; padding: 0; min-width: 0; display: grid; gap: var(--s-5); }
.f legend {
padding: 0; margin-bottom: var(--s-5);
font-family: var(--font-display); font-size: var(--fs-h3); line-height: 1.15;
display: flex; gap: var(--s-3); align-items: baseline;
}
.f legend .f__n { font-style: italic; color: var(--c-petrol); font-size: 0.75em; }
.f .f__group > legend.f__sub { display: block; font-family: var(--font-text); font-size: 1rem; font-weight: 600; line-height: 1.4; margin-bottom: var(--s-3); padding: 0; }
.f__group { display: grid; gap: var(--s-3); border: 0; padding: 0; margin: 0; min-width: 0; }
.f__field { display: grid; gap: var(--s-2); }
.f__label { font-weight: 600; font-size: 1rem; }
.f__opt { font-weight: 400; color: var(--c-text-leise); font-size: var(--fs-small); }
.f__req { color: var(--c-petrol); }
.f__help { font-size: var(--fs-small); color: var(--c-text-leise); }
.f__err { font-size: var(--fs-small); color: var(--c-fehler); font-weight: 600; }
.f__err:empty { display: none; }
.f input[type='text'], .f input[type='tel'], .f input[type='email'], .f input[type='date'],
.f input[type='number'], .f select, .f textarea {
width: 100%;
min-height: 3.25rem;
padding: 0.8rem 0.95rem;
border: 1.5px solid var(--c-rand);
border-radius: var(--r-m);
background: oklch(99.5% 0.002 220);
font-size: 1.0625rem; /* ≥16px verhindert Zoom auf iOS */
transition: border-color var(--t-fast), box-shadow var(--t-fast);
}
.f textarea { min-height: 8rem; resize: vertical; line-height: 1.5; }
.f select { appearance: none; background-image: url('/img/chevron.svg'); background-repeat: no-repeat; background-position: right 0.9rem center; background-size: 1rem; padding-right: 2.5rem; }
.f input:hover, .f select:hover, .f textarea:hover { border-color: var(--c-anthrazit); }
.f input:focus-visible, .f select:focus-visible, .f textarea:focus-visible {
outline: none; border-color: var(--c-petrol);
box-shadow: 0 0 0 3px oklch(50.7% 0.105 234 / 0.22);
}
.f [aria-invalid='true'] { border-color: var(--c-fehler) !important; }
/* Auswahlkarten (Radio) */
.choices { display: grid; gap: var(--s-2); }
.choices--2 { grid-template-columns: 1fr; }
@media (min-width: 36rem) { .choices--2 { grid-template-columns: 1fr 1fr; } }
.choices--inline { grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr)); }
.choice {
position: relative;
display: grid; grid-template-columns: auto 1fr; gap: 0.1rem 0.8rem; align-items: center;
min-height: 3.25rem; padding: 0.8rem 1rem;
border: 1.5px solid var(--c-linie); border-radius: var(--r-m);
background: oklch(99.5% 0.002 220);
cursor: pointer;
transition: border-color var(--t-fast), background-color var(--t-fast);
}
.choice:hover { border-color: var(--c-silber); }
.choice:has(input:checked) { border-color: var(--c-petrol); background: var(--c-petrol-hell); }
.choice:has(input:focus-visible) { outline: 2px solid var(--c-petrol); outline-offset: 2px; }
.choice input {
appearance: none; margin: 0;
width: 1.25rem; height: 1.25rem; border-radius: 50%;
border: 1.5px solid var(--c-rand); background: var(--c-papier);
display: grid; place-items: center;
grid-row: span 2;
}
.choice input:checked { border-color: var(--c-petrol); background: radial-gradient(circle, var(--c-petrol) 0 42%, var(--c-papier) 46%); }
.choice input:focus-visible { outline: none; }
.choice__t { font-weight: 600; line-height: 1.3; }
.choice__h { grid-column: 2; font-size: var(--fs-small); color: var(--c-text-leise); line-height: 1.35; }
.choice:has(input:checked) .choice__h { color: var(--c-petrol-tief); }
/* Checkboxen */
.check { display: grid; grid-template-columns: auto 1fr; gap: 0.8rem; align-items: start; cursor: pointer; min-height: var(--tap); padding-block: 0.25rem; }
.check input {
appearance: none; margin: 0.1rem 0 0;
width: 1.375rem; height: 1.375rem; border-radius: 5px;
border: 1.5px solid var(--c-rand); background: var(--c-papier);
display: grid; place-items: center;
}
.check input::after { content: ''; width: 0.4rem; height: 0.75rem; border: solid var(--c-papier); border-width: 0 2px 2px 0; transform: rotate(45deg) translate(-1px, -1px); opacity: 0; }
.check input:checked { background: var(--c-petrol); border-color: var(--c-petrol); }
.check input:checked::after { opacity: 1; }
.check input:focus-visible { outline: 2px solid var(--c-petrol); outline-offset: 2px; }
.check--box { padding: var(--s-4); border: 1.5px solid var(--c-linie); border-radius: var(--r-m); background: oklch(99.5% 0.002 220); }
.check--box:has(input:checked) { border-color: var(--c-petrol); background: var(--c-petrol-hell); }
/* Datei-Upload */
.drop {
position: relative; display: grid; gap: var(--s-2); justify-items: start;
padding: var(--s-5); border: 1.5px dashed var(--c-rand); border-radius: var(--r-m);
background: oklch(99.5% 0.002 220);
}
.drop:hover, .drop:focus-within { border-color: var(--c-petrol); }
.drop input[type='file'] { font-size: var(--fs-small); max-width: 100%; }
.drop input[type='file']::file-selector-button {
min-height: var(--tap); margin-right: 0.8rem; padding: 0 1.1rem;
border: 1.5px solid var(--c-petrol); border-radius: var(--r-pill);
background: var(--c-papier); color: var(--c-petrol); font-weight: 600; cursor: pointer;
}
.files { list-style: none; padding: 0; display: grid; gap: var(--s-2); }
.files li { display: flex; justify-content: space-between; gap: var(--s-3); font-size: var(--fs-small); padding: 0.5rem 0.75rem; background: var(--c-nebel-hell); border-radius: var(--r-s); }
.files li.is-bad { background: oklch(95% 0.03 27); color: var(--c-fehler); }
/* Preisrahmen */
.pricebox { padding: var(--s-5); border-radius: var(--r-m); background: var(--c-anthrazit); color: var(--c-papier); display: grid; gap: var(--s-2); }
.pricebox .label { color: oklch(85% 0.05 225); }
.pricebox__v { font-family: var(--font-display); font-size: var(--fs-h4); line-height: 1.3; }
.pricebox__all { list-style: none; padding: 0; font-size: var(--fs-small); display: grid; gap: 0.2rem; }
.pricebox a { color: var(--c-papier); }
/* Fehlerübersicht */
.f__summary { padding: var(--s-5); border: 2px solid var(--c-fehler); border-radius: var(--r-m); background: oklch(97% 0.015 27); }
.f__summary h2 { font-family: var(--font-text); font-size: 1.0625rem; font-weight: 700; color: var(--c-fehler); }
.f__summary ul { margin-top: var(--s-2); padding-left: 1.2em; }
.f__summary a { color: var(--c-fehler); }
/* Honeypot: für Menschen unsichtbar, für Screenreader ausgeblendet */
.hp { position: absolute !important; left: -10000px !important; width: 1px; height: 1px; overflow: hidden; }
/* Schrittweises Einblenden (nur mit JS) */
.f.is-js .step-later[data-locked] { display: none; }
.f [hidden] { display: none !important; }
@media (prefers-reduced-motion: no-preference) {
.f.is-js .step-later:not([data-locked]) { animation: f-in var(--t-med) var(--ease-out); }
@keyframes f-in { from { opacity: 0; transform: translateY(10px); } }
}
.f__submit { display: grid; gap: var(--s-3); justify-items: start; }
.f__submit .btn { min-height: 3.25rem; font-size: 1.0625rem; padding-inline: 1.8rem; }
.f__submit .btn[aria-busy='true'] { opacity: 0.7; pointer-events: none; }
.f__spin { width: 1.1em; height: 1.1em; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; display: none; }
.btn[aria-busy='true'] .f__spin { display: inline-block; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Bestätigung */
.done { padding: var(--s-7) var(--s-6); border-radius: var(--r-m); background: var(--c-petrol-hell); display: grid; gap: var(--s-4); justify-items: start; }
.done__ico { width: 3rem; height: 3rem; border-radius: 50%; background: var(--c-petrol); color: var(--c-papier); display: grid; place-items: center; }
.done__ico svg { width: 1.6rem; height: 1.6rem; }
.done h2 { font-size: var(--fs-h3); }
+204 -251
View File
@@ -1,268 +1,221 @@
:root {
--cream: #faf7f4;
--cream-2: #f3ece5;
--nude: #f0e8e0;
--terracotta: #c4896a;
--terracotta-deep: #a96b4d;
--ink: #1a1a1a;
--dark: #2c2420;
--soft: #5a5045;
--border: #e5ddd6;
--white: #ffffff;
--shadow-sm: 0 1px 3px rgba(44, 36, 32, 0.06);
--shadow-md: 0 12px 32px -12px rgba(44, 36, 32, 0.22);
--shadow-lg: 0 30px 60px -24px rgba(44, 36, 32, 0.35);
--radius: 18px;
--radius-lg: 28px;
--maxw: 1180px;
--serif: "Fraunces", Georgia, serif;
--sans: "Public Sans", system-ui, -apple-system, sans-serif;
@import './tokens.css';
/* ---------- Grundlage ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html {
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
scroll-padding-top: 6rem;
}
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 92px; }
body {
font-family: var(--sans);
background: var(--cream);
color: var(--ink);
line-height: 1.65;
background: var(--bg);
color: var(--fg);
font-family: var(--font-text);
font-size: var(--fs-body);
line-height: var(--lh-text);
font-weight: 400;
font-stretch: 100%;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
overflow-x: hidden;
min-height: 100dvh;
display: flex;
flex-direction: column;
}
main { flex: 1; }
[hidden] { display: none !important; }
img, picture, svg, video { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; }
p, li, dd { text-wrap: pretty; }
h1, h2, h3, h4 { text-wrap: balance; }
/* ---------- Typografie ---------- */
.display, h1, h2, h3 {
font-family: var(--font-display);
font-weight: 400;
font-optical-sizing: auto;
line-height: var(--lh-head);
letter-spacing: -0.012em;
color: var(--c-anthrazit);
}
h1 { font-size: var(--fs-h1); line-height: var(--lh-tight); letter-spacing: -0.02em; }
h2 { font-size: var(--fs-h2); line-height: 1.06; letter-spacing: -0.018em; }
h3 { font-size: var(--fs-h3); }
h4 {
font-family: var(--font-text);
font-size: var(--fs-h4);
font-weight: 600;
line-height: 1.3;
letter-spacing: -0.005em;
}
em, i { font-style: italic; }
.display em, h1 em, h2 em, h3 em {
font-style: italic;
color: var(--c-petrol);
}
strong, b { font-weight: 600; }
.label {
font-family: var(--font-text);
font-size: var(--fs-label);
font-weight: 600;
font-stretch: 72%;
text-transform: uppercase;
letter-spacing: 0.12em;
line-height: 1.3;
color: var(--c-petrol);
}
.label--leise { color: var(--c-text-leise); }
.lead {
font-size: var(--fs-lead);
line-height: 1.5;
color: var(--c-anthrazit);
max-width: var(--measure);
}
.prose { max-width: var(--measure); }
.prose > * + * { margin-top: var(--s-4); }
.prose h2 { font-size: var(--fs-h3); margin-top: var(--s-7); }
.prose h3 { font-size: var(--fs-h4); font-family: var(--font-text); font-weight: 600; margin-top: var(--s-6); }
.prose ul { padding-left: 1.2em; }
.prose li + li { margin-top: var(--s-2); }
.leise { color: var(--c-text-leise); }
.small { font-size: var(--fs-small); }
.num { font-variant-numeric: tabular-nums lining-nums; }
a {
color: var(--c-petrol);
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
transition: color var(--t-fast) var(--ease-out), text-decoration-color var(--t-fast);
}
a:hover { color: var(--c-petrol-tief); }
:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 3px;
border-radius: 2px;
}
.on-dark :focus-visible { outline-color: var(--c-papier); }
::selection { background: var(--c-petrol); color: var(--c-papier); }
/* ---------- Layout ---------- */
.wrap {
width: 100%;
max-width: var(--maxw);
margin-inline: auto;
padding-inline: var(--gutter);
}
.section { padding-block: var(--section); }
.section--tight { padding-block: calc(var(--section) * 0.6); }
.section--nebel { background: var(--c-nebel-hell); }
.stack > * + * { margin-top: var(--s-4); }
.stack-l > * + * { margin-top: var(--s-6); }
.visually-hidden {
position: absolute !important;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
h1, h2, h3 { font-family: var(--serif); font-weight: 500; line-height: 1.08; letter-spacing: -0.015em; }
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
.wrap { width: 100%; max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }
.eyebrow {
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--terracotta-deep);
.skip {
position: absolute; left: var(--s-4); top: -4rem; z-index: 100;
background: var(--c-anthrazit); color: var(--c-papier);
padding: var(--s-3) var(--s-4); border-radius: var(--r-s);
font-weight: 600; text-decoration: none;
}
.skip:focus { top: var(--s-4); color: var(--c-papier); }
/* Abschnittskopf: Label + Überschrift, links bündig */
.head { max-width: 46rem; }
.head > .label { display: block; margin-bottom: var(--s-4); }
.head > p { margin-top: var(--s-5); }
/* ---------- Knöpfe ---------- */
.btn {
display: inline-flex; align-items: center; gap: 0.55rem;
font-weight: 600; font-size: 0.95rem; letter-spacing: 0.01em;
padding: 0.95rem 1.6rem; border-radius: 100px; cursor: pointer; border: 1px solid transparent;
transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
--btn-bg: var(--c-petrol);
--btn-fg: var(--c-papier);
--btn-bd: var(--c-petrol);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.6em;
min-height: var(--tap);
padding: 0.7em 1.45em;
border-radius: var(--r-pill);
border: 1.5px solid var(--btn-bd);
background: var(--btn-bg);
color: var(--btn-fg);
font-family: var(--font-text);
font-size: 1rem;
font-weight: 600;
font-stretch: 90%;
letter-spacing: 0.01em;
line-height: 1.2;
text-decoration: none;
white-space: nowrap;
transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out), border-color var(--t-fast) var(--ease-out), transform var(--t-fast) var(--ease-out);
}
.btn-primary { background: var(--ink); color: var(--cream); }
.btn-primary:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); background: var(--dark); }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--border); }
.btn-ghost:hover { border-color: var(--ink); transform: translateY(-2px); }
.btn-accent { background: var(--terracotta); color: #fff; }
.btn-accent:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); background: var(--terracotta-deep); }
/* NAV */
header.nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
transition: background 0.3s ease, box-shadow 0.3s ease, padding 0.3s ease;
padding: 18px 0;
.btn:hover { --btn-bg: var(--c-petrol-tief); --btn-bd: var(--c-petrol-tief); color: var(--btn-fg); }
.btn:active { transform: translateY(1px); }
.btn svg { width: 1.1em; height: 1.1em; flex: none; }
.btn--ghost {
--btn-bg: transparent;
--btn-fg: var(--c-anthrazit);
--btn-bd: var(--c-silber);
}
header.nav.scrolled { background: rgba(250, 247, 244, 0.88); backdrop-filter: blur(14px); box-shadow: var(--shadow-sm); padding: 10px 0; }
.nav-inner { display: flex; align-items: center; justify-content: space-between; }
.logo { font-family: var(--serif); font-size: 1.35rem; font-weight: 600; letter-spacing: -0.02em; }
.logo b { color: var(--terracotta); }
.logo .u { color: var(--terracotta); }
.nav-links { display: flex; gap: 2rem; align-items: center; }
.nav-links a { font-size: 0.9rem; font-weight: 500; color: var(--soft); transition: color 0.2s; }
.nav-links a:hover { color: var(--ink); }
.nav-cta { display: flex; align-items: center; gap: 1rem; }
.burger { display: none; }
.btn--ghost:hover { --btn-bg: transparent; --btn-fg: var(--c-petrol); --btn-bd: var(--c-petrol); }
.btn--light { --btn-bg: var(--c-papier); --btn-fg: var(--c-petrol-tief); --btn-bd: var(--c-papier); }
.btn--light:hover { --btn-bg: var(--c-nebel); --btn-bd: var(--c-nebel); --btn-fg: var(--c-petrol-tief); }
.btn--outline-light { --btn-bg: transparent; --btn-fg: var(--c-papier); --btn-bd: oklch(98% 0.003 220 / 0.55); }
.btn--outline-light:hover { --btn-bg: oklch(98% 0.003 220 / 0.1); --btn-fg: var(--c-papier); --btn-bd: var(--c-papier); }
.btn-row { display: flex; flex-wrap: wrap; gap: var(--s-3); align-items: center; }
/* HERO */
.hero { position: relative; min-height: 100svh; display: flex; align-items: flex-end; padding-bottom: 64px; }
.hero-bg { position: absolute; inset: 0; z-index: -2; }
.hero-bg img { width: 100%; height: 100%; object-fit: cover; }
.hero-bg::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(26,26,26,0.35) 0%, rgba(26,26,26,0.15) 40%, rgba(26,26,26,0.78) 100%);
/* Textlink mit Pfeil */
.more {
display: inline-flex; align-items: center; gap: 0.5em;
min-height: var(--tap);
font-weight: 600;
font-stretch: 90%;
text-decoration: none;
color: var(--c-petrol);
}
.hero-inner { color: var(--cream); max-width: 760px; }
.hero .eyebrow { color: #f0d9cc; }
.hero h1 { font-size: clamp(2.8rem, 7vw, 5.4rem); color: #fff; margin: 1rem 0 1.2rem; }
.hero h1 em { font-style: italic; color: var(--terracotta); }
.hero p.lead { font-size: clamp(1.05rem, 2vw, 1.3rem); color: rgba(255,255,255,0.9); max-width: 560px; margin-bottom: 2rem; }
.hero-actions { display: flex; gap: 1rem; flex-wrap: wrap; }
.hero-trust { margin-top: 2.4rem; display: flex; flex-wrap: wrap; gap: 1.4rem; font-size: 0.85rem; color: rgba(255,255,255,0.82); }
.hero-trust span { display: inline-flex; align-items: center; gap: 0.45rem; }
.more svg { width: 1.05em; height: 1.05em; transition: transform var(--t-fast) var(--ease-out); }
.more:hover svg { transform: translateX(3px); }
.more span { text-decoration: underline; text-decoration-color: color-mix(in oklch, currentColor 35%, transparent); text-underline-offset: 0.25em; }
/* SECTION */
section { padding: clamp(72px, 10vw, 130px) 0; }
.sec-head { max-width: 680px; margin-bottom: 3.2rem; }
.sec-head.center { margin-left: auto; margin-right: auto; text-align: center; }
.sec-head h2 { font-size: clamp(2rem, 4.5vw, 3.2rem); margin: 0.8rem 0 1rem; }
.sec-head p { color: var(--soft); font-size: 1.05rem; }
/* ---------- Bilder ---------- */
.frame { overflow: hidden; background: var(--c-nebel); }
.frame picture { width: 100%; height: 100%; }
.frame img { width: 100%; height: 100%; object-fit: cover; }
.arch { border-radius: var(--arch); }
.ratio-45 { aspect-ratio: 4 / 5; }
.ratio-34 { aspect-ratio: 3 / 4; }
.ratio-32 { aspect-ratio: 3 / 2; }
.ratio-169 { aspect-ratio: 16 / 9; }
.ratio-11 { aspect-ratio: 1; }
figcaption { font-size: var(--fs-small); color: var(--c-text-leise); margin-top: var(--s-3); }
/* TRUST STRIP */
.strip { background: var(--dark); color: var(--cream); padding: 28px 0; }
.strip-inner { display: flex; flex-wrap: wrap; justify-content: space-around; gap: 1.5rem; text-align: center; }
.strip .num { font-family: var(--serif); font-size: 2rem; color: var(--terracotta); display: block; }
.strip .lbl { font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; color: rgba(255,255,255,0.7); }
/* ---------- Haarlinie ---------- */
.rule { border: 0; border-top: var(--hair); }
/* LEISTUNGEN BENTO */
.bento { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.card-svc {
background: var(--white); border: 1px solid var(--border); border-radius: var(--radius);
padding: 2rem 1.8rem; transition: transform 0.3s ease, box-shadow 0.3s ease; position: relative; overflow: hidden;
}
.card-svc:hover { transform: translateY(-6px); box-shadow: var(--shadow-md); }
.card-svc.feature { grid-row: span 2; background: var(--dark); color: var(--cream); display: flex; flex-direction: column; justify-content: space-between; }
.card-svc.feature .ico { background: rgba(196,137,106,0.18); }
.card-svc .ico { width: 48px; height: 48px; border-radius: 12px; background: var(--nude); display: grid; place-items: center; font-size: 1.4rem; margin-bottom: 1.2rem; }
.card-svc h3 { font-size: 1.35rem; margin-bottom: 0.5rem; }
.card-svc.feature h3 { font-size: 1.8rem; }
.card-svc p { color: var(--soft); font-size: 0.92rem; }
.card-svc.feature p { color: rgba(255,255,255,0.75); }
.card-svc .tag { font-size: 0.78rem; color: var(--terracotta-deep); font-weight: 600; margin-top: 1rem; display: inline-block; }
.card-svc.feature .tag { color: var(--terracotta); }
/* PREISE */
.price-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.price-col { background: var(--white); border: 1px solid var(--border); border-radius: var(--radius); padding: 2rem; }
.price-col h3 { font-size: 1.5rem; margin-bottom: 1.2rem; display: flex; align-items: center; gap: 0.6rem; }
.price-row { display: flex; justify-content: space-between; align-items: baseline; padding: 0.7rem 0; border-bottom: 1px dashed var(--border); }
.price-row:last-child { border: none; }
.price-row .name { font-weight: 500; }
.price-row .name small { display: block; color: var(--soft); font-size: 0.78rem; font-weight: 400; }
.price-row .val { font-weight: 700; white-space: nowrap; color: var(--dark); }
.price-row .val span { color: var(--soft); font-weight: 400; font-size: 0.8rem; }
.price-note { text-align: center; color: var(--soft); font-size: 0.85rem; margin-top: 1.6rem; }
/* TEAM */
.team-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }
.member { text-align: center; }
.avatar {
width: 100%; aspect-ratio: 3/4; border-radius: var(--radius); background: var(--nude);
display: grid; place-items: center; margin-bottom: 0.9rem; position: relative; overflow: hidden;
border: 1px solid var(--border);
}
.avatar .ini { font-family: var(--serif); font-size: 2.4rem; color: var(--terracotta-deep); }
.avatar .ph { position: absolute; bottom: 8px; left: 8px; right: 8px; font-size: 0.62rem; color: var(--soft); background: rgba(255,255,255,0.78); border-radius: 8px; padding: 3px 6px; }
.member .mname { font-weight: 600; font-size: 1.02rem; }
.member .mrole { font-size: 0.82rem; color: var(--soft); }
/* GALERIE */
.gallery { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 200px; gap: 14px; }
.gallery figure { overflow: hidden; border-radius: var(--radius); position: relative; }
.gallery img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s ease; }
.gallery figure:hover img { transform: scale(1.06); }
.gallery .big { grid-column: span 2; grid-row: span 2; }
.gallery .tall { grid-row: span 2; }
.insta-cta { text-align: center; margin-top: 2rem; }
/* REELS 9:16 */
.reels-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin: 3rem 0 1.2rem; flex-wrap: wrap; }
.reels-head h3 { font-size: 1.5rem; }
.reels-head span { font-size: 0.9rem; color: var(--soft); }
.reels { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; }
.reel { aspect-ratio: 9/16; border-radius: var(--radius); overflow: hidden; position: relative; border: 1px solid var(--border); }
.reel img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s ease; }
.reel:hover img { transform: scale(1.06); }
.reel::after { content: "▶"; position: absolute; inset: 0; display: grid; place-items: center; color: #fff; font-size: 1.6rem; background: linear-gradient(180deg, rgba(26,26,26,.05), rgba(26,26,26,.45)); text-shadow: 0 2px 12px rgba(0,0,0,.4); transition: transform 0.3s; }
.reel:hover::after { transform: scale(1.15); }
.reel .rlabel { position: absolute; left: 10px; bottom: 10px; z-index: 2; font-size: 0.7rem; font-weight: 600; color: #fff; background: rgba(26,26,26,.55); backdrop-filter: blur(4px); padding: 3px 9px; border-radius: 20px; }
/* WALLET / STEMPELKARTE */
.wallet-sec { background: linear-gradient(160deg, var(--nude), var(--cream-2)); }
.wallet-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 3.5rem; align-items: center; }
.pass {
background: linear-gradient(155deg, #2c2420, #443a32); color: #fff; border-radius: 24px;
padding: 1.8rem; box-shadow: var(--shadow-lg); max-width: 360px; margin: 0 auto; position: relative;
}
.pass-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.4rem; }
.pass-brand { font-family: var(--serif); font-size: 1.15rem; }
.pass-brand b { color: var(--terracotta); }
.pass-chip { font-size: 0.62rem; letter-spacing: 0.12em; text-transform: uppercase; background: rgba(255,255,255,0.12); padding: 4px 10px; border-radius: 20px; }
.pass-label { font-size: 0.72rem; color: rgba(255,255,255,0.55); letter-spacing: 0.05em; text-transform: uppercase; }
.pass-reward { font-family: var(--serif); font-size: 1.45rem; margin: 0.2rem 0 1.4rem; }
.stamps { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 1.2rem; }
.stamp { aspect-ratio: 1; border-radius: 50%; display: grid; place-items: center; font-size: 1.1rem; border: 1.5px dashed rgba(255,255,255,0.25); color: rgba(255,255,255,0.3); }
.stamp.on { background: var(--terracotta); border-style: solid; border-color: var(--terracotta); color: #fff; }
.pass-foot { display: flex; justify-content: space-between; font-size: 0.72rem; color: rgba(255,255,255,0.5); border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1rem; }
.wallet-btns { display: flex; gap: 0.8rem; margin-top: 1.6rem; flex-wrap: wrap; }
.wallet-btn { display: inline-flex; align-items: center; gap: 0.5rem; background: #000; color: #fff; padding: 0.7rem 1.1rem; border-radius: 12px; font-size: 0.85rem; font-weight: 600; cursor: pointer; border: none; transition: transform 0.2s; }
.wallet-btn:hover { transform: translateY(-2px); }
.wallet-btn.g { background: #fff; color: #1a1a1a; border: 1px solid var(--border); }
/* BOOKING */
.book-sec { background: var(--dark); color: var(--cream); }
.book-sec .sec-head h2 { color: #fff; }
.book-sec .sec-head p { color: rgba(255,255,255,0.7); }
.booker { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1); border-radius: var(--radius-lg); padding: 2.4rem; max-width: 720px; margin: 0 auto; }
.steps-nav { display: flex; gap: 0.5rem; margin-bottom: 2rem; }
.step-dot { flex: 1; height: 4px; border-radius: 4px; background: rgba(255,255,255,0.12); transition: background 0.3s; }
.step-dot.active { background: var(--terracotta); }
.step { display: none; }
.step.active { display: block; animation: fade 0.4s ease; }
@keyframes fade { from { opacity: 0; transform: translateY(8px);} to {opacity:1; transform:none;} }
.step h3 { color: #fff; font-size: 1.4rem; margin-bottom: 1.2rem; }
.opt-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.8rem; }
.opt { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; padding: 1rem 1.2rem; cursor: pointer; transition: all 0.2s; display: flex; justify-content: space-between; align-items: center; }
.opt:hover, .opt.sel { border-color: var(--terracotta); background: rgba(196,137,106,0.15); }
.opt .o-name { font-weight: 600; color: #fff; }
.opt .o-meta { font-size: 0.8rem; color: rgba(255,255,255,0.6); }
.slots { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.6rem; }
.slot { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; padding: 0.7rem; text-align: center; cursor: pointer; font-size: 0.9rem; color: #fff; transition: all 0.2s; }
.slot:hover, .slot.sel { border-color: var(--terracotta); background: rgba(196,137,106,0.15); }
.book-foot { display: flex; justify-content: space-between; margin-top: 2rem; align-items: center; flex-wrap: wrap; gap: 1rem; }
.book-summary { font-size: 0.88rem; color: rgba(255,255,255,0.7); }
.book-summary b { color: var(--terracotta); }
.confirm-box { text-align: center; padding: 1rem 0; }
.confirm-box .ok { width: 64px; height: 64px; border-radius: 50%; background: var(--terracotta); display: grid; place-items: center; margin: 0 auto 1.2rem; font-size: 1.8rem; }
.note-line { font-size: 0.8rem; color: rgba(255,255,255,0.55); margin-top: 1.2rem; text-align: center; }
/* REVIEWS */
.reviews { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.review { background: var(--white); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.8rem; }
.review .stars { color: var(--terracotta); letter-spacing: 2px; margin-bottom: 0.8rem; }
.review p { font-size: 0.95rem; color: var(--soft); font-style: italic; }
.review .who { margin-top: 1.2rem; font-weight: 600; font-size: 0.9rem; }
/* FAQ */
.faq { max-width: 800px; margin: 0 auto; }
.faq details { background: var(--white); border: 1px solid var(--border); border-radius: 14px; margin-bottom: 0.8rem; padding: 0 1.4rem; transition: box-shadow 0.2s; }
.faq details[open] { box-shadow: var(--shadow-sm); }
.faq summary { cursor: pointer; padding: 1.2rem 0; font-weight: 600; list-style: none; display: flex; justify-content: space-between; align-items: center; gap: 1rem; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; font-size: 1.4rem; color: var(--terracotta); transition: transform 0.2s; }
.faq details[open] summary::after { transform: rotate(45deg); }
.faq .ans { padding: 0 0 1.3rem; color: var(--soft); font-size: 0.95rem; }
/* CONTACT / FOOTER */
footer { background: var(--ink); color: var(--cream); padding: 80px 0 32px; }
.foot-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 3rem; margin-bottom: 3rem; }
.foot-grid h4 { font-family: var(--serif); font-size: 1.3rem; margin-bottom: 1rem; }
.foot-grid .logo { font-size: 1.6rem; margin-bottom: 1rem; }
.foot-col p, .foot-col a { color: rgba(255,255,255,0.7); font-size: 0.92rem; display: block; margin-bottom: 0.5rem; transition: color 0.2s; }
.foot-col a:hover { color: var(--terracotta); }
.hours-row { display: flex; justify-content: space-between; font-size: 0.9rem; color: rgba(255,255,255,0.7); padding: 0.3rem 0; border-bottom: 1px solid rgba(255,255,255,0.08); }
.hours-row.closed { color: rgba(255,255,255,0.4); }
.wa-btn { display: inline-flex; align-items: center; gap: 0.5rem; background: #25d366; color: #fff !important; padding: 0.7rem 1.2rem; border-radius: 100px; font-weight: 600; font-size: 0.9rem; margin-top: 0.6rem; }
.foot-bottom { border-top: 1px solid rgba(255,255,255,0.1); padding-top: 1.5rem; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 1rem; font-size: 0.8rem; color: rgba(255,255,255,0.5); }
.foot-bottom a { color: rgba(255,255,255,0.5); }
/* REVEAL */
.reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.7s ease, transform 0.7s ease; }
.reveal.in { opacity: 1; transform: none; }
/* TOAST */
.toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(120%); background: var(--ink); color: var(--cream); padding: 0.9rem 1.4rem; border-radius: 100px; box-shadow: var(--shadow-lg); z-index: 200; transition: transform 0.4s ease; font-size: 0.9rem; }
.toast.show { transform: translateX(-50%) translateY(0); }
.demo-flag { display: inline-block; font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase; background: var(--nude); color: var(--terracotta-deep); padding: 3px 10px; border-radius: 20px; font-weight: 700; }
@media (max-width: 900px) {
.nav-links { display: none; }
.bento, .price-grid, .team-grid, .gallery, .reviews, .wallet-layout, .foot-grid { grid-template-columns: 1fr; }
.gallery { grid-auto-rows: 220px; }
.gallery .big, .gallery .tall { grid-column: auto; grid-row: auto; }
.card-svc.feature { grid-row: auto; }
.team-grid { grid-template-columns: 1fr 1fr; }
.opt-grid, .slots { grid-template-columns: 1fr 1fr; }
/* ---------- Bewegung: dezentes Einblenden beim Scrollen ---------- */
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
.reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 55%;
}
}
@keyframes reveal {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: none; }
}
}
-232
View File
@@ -1,232 +0,0 @@
:root{
--ink:#14142a; --ink-2:#2a2a48; --body:#5c5d6e; --muted:#8a8b99; --text:#3b3c4a;
--accent:#a6826a; --accent-2:#cf9a73; --bg:#f5f5f7; --white:#fff;
--line:#ededf1; --line-2:#ececf1; --footer:#0f0f20;
--maxw:1180px; --radius:24px; --radius-img:26px; --pill:40px;
--shadow-card:0 22px 50px rgba(20,20,42,.10);
--shadow-float:0 18px 50px rgba(20,20,42,.16);
--sans:"DM Sans",system-ui,-apple-system,sans-serif;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;scroll-padding-top:120px}
body{margin:0;background:var(--white);color:var(--body);font-family:var(--sans);-webkit-font-smoothing:antialiased;line-height:1.6}
h1,h2,h3,h4{font-family:var(--sans);color:var(--ink);margin:0;letter-spacing:-0.02em;line-height:1.08;font-weight:700}
p{margin:0}
a{color:inherit;text-decoration:none}
img{display:block;max-width:100%}
.container{max-width:var(--maxw);margin:0 auto;padding:0 24px}
.section{padding:clamp(56px,9vw,112px) 0}
.bg-soft{background:var(--bg)}
.bg-dark{background:var(--ink);color:#fff}
/* eyebrow */
.eyebrow{display:inline-flex;align-items:center;gap:9px;font-size:13px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--accent)}
.eyebrow .ln{width:26px;height:1px;background:var(--accent-2);display:inline-block}
.eyebrow.center{justify-content:center}
/* buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;height:54px;padding:0 30px;border-radius:var(--pill);font-weight:600;font-size:16px;border:1px solid transparent;cursor:pointer;transition:background .2s,border-color .2s,transform .2s,box-shadow .2s;white-space:nowrap}
.btn-dark{background:var(--ink);color:#fff}
.btn-dark:hover{background:var(--ink-2);transform:translateY(-2px)}
.btn-outline{background:transparent;border-color:#c9cad3;color:var(--ink)}
.btn-outline:hover{border-color:var(--ink)}
.btn-light{background:#fff;color:var(--ink);font-weight:700}
.btn-light:hover{background:#f0e7df}
.btn-accent{background:var(--accent);color:#fff}
.btn-accent:hover{background:var(--accent-2);transform:translateY(-2px)}
.btn-sm{height:46px;padding:0 22px;font-size:15px}
/* topbar */
.topbar{background:var(--ink);color:#cfd0db;font-size:13.5px}
.topbar .inner{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:9px 24px;max-width:var(--maxw);margin:0 auto}
.topbar a{color:#fff;font-weight:500}
.topbar .dot{width:6px;height:6px;border-radius:50%;background:var(--accent-2);display:inline-block;margin-right:8px}
.topbar .right{display:flex;align-items:center;gap:22px}
.topbar .muted{color:#8587a0}
/* header */
.header{position:sticky;top:0;z-index:50;background:rgba(255,255,255,.92);backdrop-filter:saturate(180%) blur(10px);border-bottom:1px solid var(--line)}
.header .inner{display:flex;align-items:center;justify-content:space-between;gap:24px;padding:16px 24px;max-width:var(--maxw);margin:0 auto}
.brand{display:flex;align-items:center;gap:12px}
.brand .mark{width:42px;height:42px;border-radius:50%;background:var(--ink);display:flex;align-items:center;justify-content:center;color:#fff;flex:none}
.brand .name{font-weight:700;font-size:18px;color:var(--ink);line-height:1.05}
.brand .sub{font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);font-weight:600}
.nav{display:flex;align-items:center;gap:30px;font-size:15px;font-weight:500;color:var(--text)}
.nav a:hover{color:var(--ink)}
.header-cta{display:flex;align-items:center;gap:18px}
.header-cta .tel{font-weight:600;color:var(--ink);font-size:15px}
/* hero */
.hero{background:var(--bg)}
.hero .grid{display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(32px,5vw,64px);align-items:center;padding:clamp(48px,7vw,92px) 0}
.hero h1{margin:22px 0 0;font-size:clamp(40px,5.2vw,66px)}
.hero .lead{margin:24px 0 0;font-size:18px;line-height:1.65;max-width:50ch}
.hero .actions{margin-top:34px;display:flex;flex-wrap:wrap;gap:14px}
.hero .stats{margin-top:34px;display:flex;align-items:center;gap:20px;color:var(--muted);font-size:14px;flex-wrap:wrap}
.hero .stats .sep{width:1px;height:18px;background:#dcdce2}
.hero-media{position:relative}
.hero-media>img{width:100%;height:clamp(360px,46vw,540px);object-fit:cover;border-radius:28px}
.float-card{position:absolute;left:-18px;bottom:26px;background:#fff;border-radius:18px;padding:16px 20px;box-shadow:var(--shadow-float);display:flex;align-items:center;gap:14px}
.float-card .ic{width:44px;height:44px;border-radius:50%;background:var(--ink);display:flex;align-items:center;justify-content:center;color:#fff;flex:none}
/* info strip */
.infostrip{background:#fff;border-bottom:1px solid var(--line)}
.infostrip .grid{display:grid;grid-template-columns:repeat(3,1fr);text-align:center}
.infostrip .col{padding:42px 20px;border-right:1px solid var(--line)}
.infostrip .col:last-child{border-right:none}
.infostrip h4,.eyebrow-label{font-size:13px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent);font-weight:600;margin:0}
.infostrip p{margin-top:12px;font-size:16px;line-height:1.7;color:var(--text)}
/* about / split */
.split{display:grid;grid-template-columns:.9fr 1.1fr;gap:clamp(36px,6vw,80px);align-items:center}
.split.rev{grid-template-columns:1.05fr .95fr}
.split img.media{width:100%;height:clamp(380px,40vw,560px);object-fit:cover;border-radius:var(--radius-img)}
.split h2{margin:20px 0 0;font-size:clamp(30px,3.6vw,46px);max-width:18ch}
.split p.bodytext{margin:22px 0 0;font-size:17px;line-height:1.7;max-width:60ch}
.checks{margin:30px 0 0;display:grid;grid-template-columns:1fr 1fr;gap:18px 28px;max-width:520px}
.check{display:flex;gap:12px;align-items:flex-start;font-size:15.5px;color:var(--text)}
.check .tick{color:var(--accent);font-weight:700;margin-top:1px}
/* section head center */
.head-center{text-align:center;max-width:660px;margin:0 auto}
.head-center h2{margin:18px 0 0;font-size:clamp(30px,3.8vw,48px)}
.head-center p{margin:18px 0 0;font-size:17px;line-height:1.7;color:var(--body)}
/* services */
.svc-grid{margin-top:54px;display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:26px}
.scard{background:#fff;border:1px solid var(--line-2);border-radius:var(--radius);overflow:hidden;transition:transform .25s,box-shadow .25s}
.scard:hover{transform:translateY(-6px);box-shadow:var(--shadow-card)}
.scard .imgwrap{position:relative}
.scard .imgwrap img{width:100%;height:218px;object-fit:cover}
.scard .badge{position:absolute;left:20px;bottom:-22px;width:46px;height:46px;border-radius:50%;background:#fff;box-shadow:0 8px 22px rgba(20,20,42,.14);display:flex;align-items:center;justify-content:center;color:var(--ink)}
.scard .body{padding:34px 26px 28px}
.scard h3{font-size:21px}
.scard .body p{margin:12px 0 0;font-size:15px;line-height:1.65;color:#6b6c7a}
.scard .row{margin-top:22px;display:flex;align-items:center;justify-content:space-between}
.scard .price{font-weight:700;color:var(--ink);font-size:16px}
.scard .more{font-weight:600;color:var(--accent);font-size:15px}
.center-actions{margin-top:46px;display:flex;flex-wrap:wrap;justify-content:center;gap:14px}
/* dark CTA band */
.cta-band{background:var(--ink);color:#fff}
.cta-band .inner{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:30px;padding:clamp(52px,7vw,84px) 0}
.cta-band h2{margin:14px 0 0;color:#fff;font-size:clamp(28px,3.4vw,42px);max-width:18ch}
.cta-band .eyebrow{color:var(--accent-2)}
/* gallery */
.gal{margin-top:42px;display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
.gal img{width:100%;aspect-ratio:1/1;object-fit:cover;border-radius:16px;transition:transform .5s}
.gal a:hover img{transform:scale(1.05)}
.reels-head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin:46px 0 18px}
.reels-head h3{font-size:22px}
.reels-head span{font-size:15px;color:var(--body)}
.reels{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
.reel{position:relative;aspect-ratio:9/16;border-radius:18px;overflow:hidden;border:1px solid var(--line)}
.reel img{width:100%;height:100%;object-fit:cover;transition:transform .5s}
.reel:hover img{transform:scale(1.06)}
.reel::after{content:"▶";position:absolute;inset:0;display:grid;place-items:center;color:#fff;font-size:1.5rem;background:linear-gradient(180deg,rgba(20,20,42,.05),rgba(20,20,42,.45));text-shadow:0 2px 12px rgba(0,0,0,.4)}
.reel .lab{position:absolute;left:10px;bottom:10px;z-index:2;font-size:.7rem;font-weight:600;color:#fff;background:rgba(20,20,42,.55);padding:3px 9px;border-radius:20px}
/* wallet / treuekarte */
.wallet-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(36px,6vw,72px);align-items:center}
.pass{background:linear-gradient(155deg,#14142a,#2a2a48);color:#fff;border-radius:24px;padding:24px;box-shadow:0 30px 60px -24px rgba(20,20,42,.6);max-width:360px;margin:0 auto;width:100%}
.pass .ptop{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
.pass .pbrand{font-weight:700;font-size:18px}
.pass .pbrand .s{font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent-2);display:block;font-weight:600}
.pass .chip{font-size:10px;letter-spacing:.12em;text-transform:uppercase;background:rgba(255,255,255,.12);padding:5px 11px;border-radius:20px}
.pass .plabel{font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:rgba(255,255,255,.55)}
.pass .preward{font-size:20px;font-weight:700;color:#fff;margin:4px 0 18px}
.stamps{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:16px}
.stamp{aspect-ratio:1;border-radius:50%;display:grid;place-items:center;font-size:1.05rem;border:1.5px dashed rgba(255,255,255,.25);color:rgba(255,255,255,.3)}
.stamp.on{background:var(--accent);border-style:solid;border-color:var(--accent);color:#fff}
.pass .pfoot{display:flex;justify-content:space-between;font-size:12px;color:rgba(255,255,255,.5);border-top:1px solid rgba(255,255,255,.12);padding-top:14px}
.wallet-btns{display:flex;gap:.8rem;margin-top:24px;flex-wrap:wrap}
.wbtn{display:inline-flex;align-items:center;gap:.5rem;background:#000;color:#fff;padding:.8rem 1.2rem;border-radius:12px;font-size:.9rem;font-weight:600;cursor:pointer;border:none;transition:transform .2s}
.wbtn:hover{transform:translateY(-2px)}
.wbtn.g{background:#fff;color:#14142a;border:1px solid var(--line)}
.how{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:42px}
.how .step{background:#fff;border:1px solid var(--line-2);border-radius:18px;padding:24px}
.how .n{width:34px;height:34px;border-radius:50%;background:var(--ink);color:#fff;display:grid;place-items:center;font-weight:700;font-size:15px;margin-bottom:14px}
.how h4{font-size:17px;color:var(--ink)}
.how p{margin-top:8px;font-size:14.5px;color:#6b6c7a;line-height:1.6}
/* booking */
.booker{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.12);border-radius:28px;padding:clamp(24px,4vw,40px);max-width:720px;margin:0 auto}
.steps-nav{display:flex;gap:.5rem;margin-bottom:2rem}
.step-dot{flex:1;height:4px;border-radius:4px;background:rgba(255,255,255,.12);transition:background .3s}
.step-dot.active{background:var(--accent-2)}
.step{display:none}.step.active{display:block;animation:fade .4s ease}
@keyframes fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.step h3{color:#fff;font-size:1.3rem;margin-bottom:1.2rem}
.opt-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:.8rem}
.opt{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);border-radius:12px;padding:1rem 1.2rem;cursor:pointer;transition:all .2s;display:flex;justify-content:space-between;align-items:center}
.opt:hover,.opt.sel{border-color:var(--accent-2);background:rgba(207,154,115,.15)}
.opt .o-name{font-weight:600;color:#fff}
.opt .o-meta{font-size:.8rem;color:rgba(255,255,255,.6)}
.slots{display:grid;grid-template-columns:repeat(4,1fr);gap:.6rem}
.slot{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);border-radius:10px;padding:.7rem;text-align:center;cursor:pointer;font-size:.9rem;color:#fff;transition:all .2s}
.slot:hover,.slot.sel{border-color:var(--accent-2);background:rgba(207,154,115,.15)}
.book-foot{display:flex;justify-content:space-between;margin-top:2rem;align-items:center;flex-wrap:wrap;gap:1rem}
.book-summary{font-size:.88rem;color:rgba(255,255,255,.7)}
.book-summary b{color:var(--accent-2)}
.confirm .ok{width:64px;height:64px;border-radius:50%;background:var(--accent);display:grid;place-items:center;margin:0 auto 1.2rem;font-size:1.8rem}
.note-line{font-size:.8rem;color:rgba(255,255,255,.5);margin-top:1.2rem;text-align:center}
/* faq */
.faq{max-width:800px;margin:42px auto 0}
.faq details{background:#fff;border:1px solid var(--line-2);border-radius:14px;margin-bottom:.8rem;padding:0 1.4rem}
.faq summary{cursor:pointer;padding:1.2rem 0;font-weight:600;color:var(--ink);list-style:none;display:flex;justify-content:space-between;align-items:center;gap:1rem}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"+";font-size:1.4rem;color:var(--accent);transition:transform .2s}
.faq details[open] summary::after{transform:rotate(45deg)}
.faq .ans{padding:0 0 1.3rem;color:var(--body);font-size:15px;line-height:1.7}
/* footer */
.footer{background:var(--footer);color:#a9aabb}
.footer .grid{display:grid;grid-template-columns:1.4fr 1fr 1fr 1.3fr;gap:40px}
.footer h4{color:#fff;font-size:14px;font-weight:600;letter-spacing:.04em}
.footer .col{display:flex;flex-direction:column;gap:11px;font-size:14.5px;margin-top:16px}
.footer .col a:hover{color:#fff}
.footer .lead{margin:18px 0 0;font-size:14.5px;line-height:1.7;max-width:34ch}
.footer .bottom{margin-top:48px;padding-top:26px;border-top:1px solid rgba(255,255,255,.10);display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:18px;font-size:13.5px}
.hours-row{display:flex;justify-content:space-between;gap:14px}
.hours-row.closed{color:#74758a}
.wa{display:inline-flex;align-items:center;gap:.5rem;background:#25d366;color:#fff !important;padding:.7rem 1.1rem;border-radius:var(--pill);font-weight:600;font-size:14px;margin-top:4px;width:max-content}
/* lp hero */
.lp-hero{background:var(--bg)}
.lp-hero .grid{display:grid;grid-template-columns:1.05fr .95fr;gap:clamp(32px,5vw,64px);align-items:center;padding:clamp(44px,6vw,84px) 0}
.lp-hero h1{margin:20px 0 0;font-size:clamp(34px,4.6vw,56px)}
.lp-hero .lead{margin:22px 0 0;font-size:18px;line-height:1.65;max-width:52ch}
.lp-hero img{width:100%;height:clamp(340px,42vw,500px);object-fit:cover;border-radius:28px}
.pill-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:26px}
.pill{display:inline-flex;align-items:center;gap:8px;background:#fff;border:1px solid var(--line);border-radius:var(--pill);padding:9px 16px;font-size:14px;font-weight:600;color:var(--ink)}
.feature-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;margin-top:50px}
.fcard{background:#fff;border:1px solid var(--line-2);border-radius:var(--radius);padding:30px 26px}
.fcard .ic{width:48px;height:48px;border-radius:14px;background:#f3ece6;display:grid;place-items:center;font-size:1.4rem;margin-bottom:16px}
.fcard h3{font-size:19px}
.fcard p{margin-top:10px;font-size:14.5px;color:#6b6c7a;line-height:1.6}
.banner{background:#f3ece6;border:1px solid #e7d8cb;border-radius:var(--radius);padding:clamp(24px,4vw,40px);display:flex;gap:24px;align-items:center;flex-wrap:wrap;justify-content:space-between}
.banner h3{font-size:clamp(22px,2.6vw,30px);max-width:22ch}
.demo-flag{display:inline-block;font-size:.66rem;letter-spacing:.1em;text-transform:uppercase;background:#f3ece6;color:var(--accent);padding:3px 10px;border-radius:20px;font-weight:700}
/* reveal */
.reveal{opacity:0;transform:translateY(24px);transition:opacity .7s,transform .7s}
.reveal.in{opacity:1;transform:none}
/* toast */
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(160%);background:var(--ink);color:#fff;padding:.9rem 1.4rem;border-radius:var(--pill);box-shadow:var(--shadow-float);z-index:200;transition:transform .4s;font-size:.9rem}
.toast.show{transform:translateX(-50%) translateY(0)}
@media(max-width:900px){
.nav{display:none}
.hero .grid,.split,.split.rev,.wallet-grid,.lp-hero .grid{grid-template-columns:1fr}
.infostrip .grid{grid-template-columns:1fr}
.infostrip .col{border-right:none;border-bottom:1px solid var(--line)}
.checks{grid-template-columns:1fr}
.gal,.reels{grid-template-columns:repeat(2,1fr)}
.how,.feature-grid{grid-template-columns:1fr}
.footer .grid{grid-template-columns:1fr 1fr}
.float-card{left:12px;right:12px;bottom:12px}
.opt-grid,.slots{grid-template-columns:1fr 1fr}
.header-cta .tel{display:none}
}
+121
View File
@@ -0,0 +1,121 @@
/* ==========================================================================
Friseur-Team VK · Design-Tokens
Herleitung und Regeln: siehe DESIGN.md im Repo-Wurzelverzeichnis.
Alle Farben in OKLCH, abgeleitet aus Logo und Salon.
========================================================================== */
@font-face {
font-family: 'Bodoni Moda';
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url('/fonts/bodoni-moda-var.woff2') format('woff2-variations');
}
@font-face {
font-family: 'Bodoni Moda';
font-style: italic;
font-weight: 400 500;
font-display: swap;
src: url('/fonts/bodoni-moda-italic-var.woff2') format('woff2-variations');
}
@font-face {
font-family: 'Archivo';
font-style: normal;
font-weight: 350 700;
font-stretch: 68% 100%;
font-display: swap;
src: url('/fonts/archivo-var.woff2') format('woff2-variations');
}
/* Metrik-angepasste Ersatzschriften gegen Layoutsprung beim Nachladen */
@font-face {
font-family: 'Archivo Fallback';
src: local('Arial'), local('Helvetica Neue'), local('Helvetica');
size-adjust: 104%;
ascent-override: 88%;
descent-override: 21%;
}
@font-face {
font-family: 'Bodoni Fallback';
src: local('Didot'), local('Bodoni 72'), local('Georgia'), local('Times New Roman');
size-adjust: 98%;
ascent-override: 98%;
descent-override: 26%;
}
:root {
/* --- Markenfarben (Quelle: Logo, Wandpaneele im Salon) --- */
--c-anthrazit: oklch(33.8% 0.012 140.5); /* #343933 */
--c-petrol: oklch(50.7% 0.105 234); /* #036E98 */
--c-hellblau: oklch(64.9% 0.076 224.9); /* #5899B2 */
--c-silber: oklch(71.1% 0.009 225.1); /* #9CA3A6 */
--c-nebel: oklch(88.4% 0.008 216.6); /* #D3DADC */
/* --- Abgeleitete Stufen --- */
--c-petrol-tief: oklch(36% 0.075 236); /* Hover, dunkle Fläche */
--c-petrol-hell: oklch(94.5% 0.018 230); /* zarter Petrol-Hauch */
--c-papier: oklch(98.7% 0.003 220); /* Seitengrund, kühles Weiß */
--c-nebel-hell: oklch(95.2% 0.006 218); /* ruhige Flächen */
--c-linie: oklch(86% 0.008 220); /* Haarlinien */
--c-rand: oklch(58% 0.012 220); /* Formularränder, ≥ 3:1 (WCAG 1.4.11) */
--c-text: var(--c-anthrazit);
--c-text-leise: oklch(46% 0.012 200); /* 7:1 auf Papier */
--c-fehler: oklch(48% 0.16 27);
--c-ok: oklch(46% 0.09 160);
/* --- Rollen --- */
--bg: var(--c-papier);
--fg: var(--c-text);
--accent: var(--c-petrol);
--focus: var(--c-petrol);
/* --- Schrift --- */
--font-display: 'Bodoni Moda', 'Bodoni Fallback', 'Didot', Georgia, serif;
--font-text: 'Archivo', 'Archivo Fallback', 'Helvetica Neue', Arial, sans-serif;
/* Typoskala, Verhältnis ca. 1,25 bis 1,33, fließend */
--fs-label: 0.8125rem; /* 13px, Untergrenze 12px */
--fs-small: 0.9375rem; /* 15px */
--fs-body: 1.0625rem; /* 17px */
--fs-lead: clamp(1.1875rem, 1.1rem + 0.45vw, 1.4375rem);
--fs-h4: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem);
--fs-h3: clamp(1.5rem, 1.3rem + 1vw, 2.125rem);
--fs-h2: clamp(2.125rem, 1.6rem + 2.6vw, 3.75rem);
--fs-h1: clamp(2.75rem, 1.8rem + 4.6vw, 6rem);
--fs-mega: clamp(4.5rem, 2.5rem + 10vw, 11rem);
--lh-tight: 1.04;
--lh-head: 1.12;
--lh-text: 1.6;
/* --- Abstände (4px-Raster) --- */
--s-1: 0.25rem;
--s-2: 0.5rem;
--s-3: 0.75rem;
--s-4: 1rem;
--s-5: 1.5rem;
--s-6: 2rem;
--s-7: 3rem;
--s-8: 4rem;
--s-9: 6rem;
--section: clamp(4.5rem, 3rem + 6vw, 9.5rem);
--gutter: clamp(1.25rem, 0.6rem + 3vw, 3.5rem);
--maxw: 84rem;
--measure: 38rem;
/* --- Form --- */
--r-s: 4px;
--r-m: 10px;
--r-pill: 999px;
--arch: 100vmax 100vmax 0 0; /* Bogen wie der Durchgang im Salon */
--hair: 1px solid var(--c-linie);
/* --- Bewegung --- */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* ease-out-quint */
--t-fast: 160ms;
--t-med: 420ms;
--t-slow: 900ms;
/* --- Ziele --- */
--tap: 2.75rem; /* 44px Mindestgröße für Touch-Ziele */
--bar-h: 4.25rem; /* Mobile Leiste unten */
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}