51 Commits
Author SHA1 Message Date
rootandClaude Sonnet 5.5 3df23fabaf Datenschutz-Weiterleitung nimmt den Anker mit
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 10:06:15 +00:00
ClaudeandClaude Opus 5.5 09e1832041 Website-Analyse auf webklar.com: Messskript (Banner in der Sie-Fassung) und Datenschutz-Zusatzseite
Das Skript des eigenen Messdienstes (project.webklar.com/a/w.js) mit data-variant="eigen":
Banner in der Sie-Fassung ohne Auftragsverarbeiter-Satz. Die Zusatzseite /datenschutz-analyse.html
beschreibt die Messung mit WEBklar als Verantwortlichem (Abschnitt 4a der Datenschutzerklärung).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:32:50 +00:00
rootandClaude Opus 5.5 cbc149f9f8 Kontaktformular: Anfragen über die Kontakt-API, damit WEBklar per Mail davon erfährt
Bisher schrieb das Formular als Gast direkt in Appwrite, und niemand las die
Einträge. Jetzt geht es an POST /api/contact (webklar-webclaw-contact-api):
die API speichert in Appwrite und meldet jede Anfrage sofort an
support@webklar.com, Antworten gehen an die Absenderadresse.

- src/lib/contact.ts ersetzt src/lib/appwrite.ts, kein Appwrite-SDK mehr im Bundle
- Honeypot-Feld "website" (unsichtbar, nicht per Tab erreichbar)
- maxLength passend zu den Grenzen der API (120/200/200/4000)
- eigener Hinweis bei 429 (zu viele Anfragen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:53:02 +00:00
knsoandClaude Opus 5.5 795c79d393 Signatur: animiertes Logo (Sternblitz) für E-Mail-Signaturen
public/signatur/webklar-logo-v1.gif   Zeichen + Schriftzug, 573×168 (191×56 @3x)
public/signatur/webklar-zeichen-v1.gif Zeichen allein, 192×192 (64×64 @3x)
GIFs werden 1 Jahr gecacht (immutable) – bei Änderungen neuen Dateinamen (-v2) nehmen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:14:27 +00:00
rootandClaude Opus 5.5 8f5d522682 Relaunch: Portal-Bild für Öffnungszeiten live
Echte Aufnahme des Portal-Editors (Handy 402x874, Hinweis-Leiste mit
Betriebsurlaub, Knopf "Hinweis-Leiste anlegen") ersetzt den Platzhalter
(Kopie von phone-9-termine). Tippunkt aus der gemessenen Knopfmitte
(135 / 488) in HINWEIS_PUNKT und HINWEIS_ANLEGEN_PUNKT; Test pinnt, dass
beide gleich sind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:17:48 +00:00
rootandClaude Opus 5.5 a23f2e52c2 Relaunch: Station Öffnungszeiten live
Vierter Eintrag der Funktionen-Gruppe (nach der News-Pinnwand, „Bald verfügbar“, ohne Preis):
- Website: Bäckerei Sonnenschein mit großer Status-Pille und Hinweis-Leiste „Betriebsurlaub 12.–26. Oktober“
  (gleitet per transform ein, der Inhalt rückt nach), Desktop und Handy.
- Handy-Ebene: Finger tippt „Hinweis-Leiste anlegen“ (HINWEIS_PUNKT, Platzhalter bis zur Aufnahme).
- Uhr-Regler: echter Schieberegler unter dem MacBook (inaktiv: aria-hidden + inert) und in der Handy-Karte.
- ZeitenSteuerung (setzeUhr(null) beim Verlassen), lazy-Chunks, Kundenportal importiert aus ablauf.ts nur Typen.
- Pins in relaunch-kundenportal.test.tsx auf neun Stationen angepasst (Als App = Marke 8), Spezifikations-Zähler mitgezogen.

ACHTUNG: public/relaunch/portal/phone-12-zeiten-{390,780}.webp sind vorläufig Kopien von phone-9-termine
(damit Build und Tests laufen); Task 36 ersetzt sie durch die echte Aufnahme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:05:49 +00:00
rootandClaude Opus 5.5 f7147fcd30 Relaunch: Ablauf der Station Öffnungszeiten live
Reine Logik (zeiten/ablauf.ts): statusFuer/formatUhr für die Bäckerei am
Montag (6 bis 18 Uhr), useZeitenAblauf mit Uhr 05:50 bis 18:10, Tipp auf
„Hinweis-Leiste anlegen“ und Hinweis-Leiste; Bedienen stoppt den Ablauf,
ohne Bewegung steht der Endzustand. Logo, Stempel und Pfeil der
Bäckerei-Seite unverändert nach baeckerei/teile.tsx verschoben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:48:16 +00:00
knsoandClaude Opus 5.5 4139f31925 Terminplaner: Marke „Bald verfügbar“ (noch nicht für Kunden buchbar)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:09:28 +00:00
rootandClaude Opus 5.5 c1d775e090 Bühne: Live-Vorschau mit echter Bäckerei-Website, Chat als Live-Demo mit Zoom, weiches Zuklappen
- Live-Vorschau: Im MacBook steht die Beispiel-Website der Bäckerei Sonnenschein selbst
  (Startbereich, Sortiment, Backstube, Torten, Öffnungszeiten, Fuß; Instrument Serif,
  CC0-Fotos von Wikimedia Commons, nur Backwaren). Beim Scrollen durch die Station läuft
  sie einmal durch (--kp-v, nur transform). Portal-Aufnahmen (Projekte, Als App) neu mit
  derselben Website im Vorschaukasten.
- Chat: Portal-Chat als Live-Demo. Die Kamera zoomt aufs Gespräch, Foto anhängen, tippen,
  Sprachnachricht, Antworten von Kenso ploppen auf (spielerische Texte, Sie-Form). Das iPhone
  gleitet in dieser Station weg. Hinweis auf die E-Mail bei Team-Antworten (K17).
- „Funktionen“: Unterpunkte falten sich beim Verlassen ein, danach gleitet „Als App“ per FLIP
  nach oben (auch beim Aufklappen nach unten), Pfeil zeigt den Zustand.
- Schrittlängen je Station (normal, Demo, Vorschau, Chat), bei reduzierter Bewegung kurz.
- Tests: Bühne angepasst, neue Tests für Chat-Ablauf und Bäckerei-Website (287 grün).

Spezifikation: docs/superpowers/specs/2026-10-04-vorschau-chat.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:16:15 +00:00
rootandClaude Opus 5.5 58ac7745e6 Heatmap-Demo: ein Klick = ein Punkt an der Zeigerstelle, weich eingeblendet
Wunsch des Inhabers: ein simulierter Klick soll nicht mehrere Heatpunkte auf einmal
bringen. Die Heatmap der letzten 30 Tage ist jetzt von Anfang an ganz zu sehen; jeder
Demo-Klick legt genau einen Punkt an die Stelle des Zeigers, der in gut 0,4 s weich
hereinwächst. Beim Neustart blenden nur die Demo-Klicks aus, nicht die ganze Heatmap.

heat.ts: heatEbene() zeichnet den Grundbestand einmal und färbt je Bild nur den kleinen
Ausschnitt um die sich ändernden Punkte neu ein (gemessen ca. 2.000 Pixel je Klick),
nach der Überblendung läuft nichts mehr. Ohne Bewegung: alles sofort im Endzustand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:58:50 +00:00
rootandClaude Opus 5.5 adb1587a7b Heatmap-Demo: sofort sichtbar, je Klick nur wenige neue Punkte, etwas weniger Streuung
- Die Heatmap beginnt mit 70 % ihrer Punkte (Grundbestand) statt leer; jeder simulierte
  Klick gibt nur einen kleinen Teil des Rests seiner Zone frei (gemessen 1–4 Punkte je
  Klick statt rund 18), keine Streuung mehr in fremde Zonen.
- Dichte der übrigen Zonen etwas zurückgenommen (Exponent 1,8, Mindestens 1 Punkt je
  18 Klicks); „Tageskarte kaufen“ bleibt klar am heißesten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:48:47 +00:00
rootandClaude Opus 5.5 2f4ef5309b Wisch-Reihe: Funktions-Karten nur mit Titel, Stichpunkte entfernt
Wunsch des Inhabers: auch auf dem Handy zeigen Terminplaner, Website-Analyse und
News-Pinnwand nur ihren Titel. Stichpunkte samt Daten und Styles entfernt; der Hinweis
„Erfundenes Beispiel“ und der Tipp-Hinweis der Heatmap bleiben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:26:11 +00:00
rootandClaude Opus 5.5 9914002e74 Bühne: „Funktionen“ als aufklappender Überpunkt, Heatmap mit Klicks auch außerhalb der Tageskarte
- Liste: Überpunkt „Funktionen“ (Bausteine für Ihre Website). Er klappt auf, sobald
  Terminplaner, Website-Analyse oder News-Pinnwand erreicht ist, und danach wieder zu.
  Die drei Unterpunkte zeigen nur ihren Titel (keine Stichpunkte); die Liste steht oben
  in der Bühne, damit beim Aufklappen nichts darüber springt.
- Heatmap der Boulderhalle: jede Zone bekommt eine Mindestzahl Klickpunkte (Telefon,
  Menü, Routenbild, Karten sind sichtbar), die Tageskarte bleibt klar am heißesten.
  Telefonnummer 96 statt 61 Klicks (getauscht mit Öffnungszeiten, Summe bleibt 1.284).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:15:47 +00:00
rootandClaude Opus 5.5 f0bced164b Kundenportal-Bühne: Gruppe „Weitere Funktionen“, Unterpunkte als Stichpunkte
Wunsch des Inhabers: Die drei Funktionen heißen als Gruppe „Weitere Funktionen“ und
beschreiben sich in drei Stichpunkten statt in einem Satz (Liste der Bühne und Karten
der Wisch-Reihe). Der Bedienhinweis der Heatmap („Zeigen Sie mit der Maus …“ bzw.
„Tippen Sie …“) steht jetzt im Hinweis unter dem MacBook und in der Karte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 15:02:04 +00:00
rootandClaude Opus 5.5 72275db9fa Kundenportal-Bühne: Terminplaner, Heatmap und Pinnwand als Live-Demos statt „Funktionen“
Die Station „Funktionen“ ist durch drei Stationen ersetzt (Gruppe „Funktionen zum
Dazubuchen“). Auf dem MacBook läuft je eine erfundene Kunden-Website als Live-Komponente,
auf dem iPhone die Portal-Ansicht der Funktion (echter Portal-Code, Fake-Daten):

- Terminplaner (Salon Schnittstelle): Buchung im Widget, Bestätigung per Link,
  Eintrag im Kalender-Abo, Mitteilung „Neue Buchung …“ auf dem Handy
- Website-Analyse (Boulderhalle Überhang): simulierte Klicks bauen die Heatmap auf
  (Algorithmus wie analytics/client/h.js); Maus über der Seite zeigt Zone + Klickzahl,
  darunter die Diagnose mit Rangliste und Satz (Beispieldaten)
- News-Pinnwand (Ortsgemeinde Musterbach): „Aufhängen“ im Portal, Zettel fällt auf die Pinnwand

Demos laden als eigene Chunks, laufen nur bei aktiver, sichtbarer Station und ohne
reduzierte Bewegung. Unter 900 px zeigen die Karten die Website in der Handy-Fassung.
Spezifikation: docs/superpowers/specs/2026-10-04-funktionen-demos.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:24:59 +00:00
rootandClaude Opus 5.5 c9ad9ba2b7 Portal-Screenshots im Geräteformat neu aufgenommen
iPhone 16 Pro 402 x 874 (Safe Area 59/34) und MacBook-Safari-Fläche 1512 x 893,
je @2x, weiter nur mit den Demodaten der Bäckerei Sonnenschein. Kein Versatz
mehr im iPhone; im letzten Schritt bleibt die Statusleiste deckend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:54:23 +00:00
rootandClaude Opus 5.5 e0e81b65a8 Kundenportal: realistische MacBook- und iPhone-Rahmen aus KNSONWS/homepage (device-mockups)
DeviceFrames.tsx und device-frames.css unverändert übernommen (Commit ae123f0).
DesktopFenster/HandyRahmen nutzen sie jetzt; Anordnung wie in der Vorlage
(iPhone über der rechten unteren Ecke), Screenshots füllen die Bildschirmfläche.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:30:10 +00:00
rootandClaude Opus 5.5 6e8ba2cddb Relaunch Phase 1: Feinschliff nach Browser- und Code-Prüfung
- Kopfleiste voll deckend, nichts scheint beim Scrollen durch
- Leistungen: Überschrift bleibt links stehen, während die Liste vorbeizieht
- langer Handy-Screenshot unten beschnitten (kein schwarzer Streifen am Ende)
- Titel, Beschreibung, og/twitter nach Spezifikation 6.5; canonical je Route
- Über uns ohne alte Versprechen, nur belegte Aussagen
- Schrittzeile neutral benannt, Tippflächen 44 px, Navigation als eigenes Modul

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:33:27 +00:00
rootandClaude Opus 5.5 1c8cfa5c1c Relaunch Phase 1: Hero mit Deutschland-Karte, Kundenportal-Bühne, Leistungen, Preise, Unterseiten im neuen Layout
- Hero: wechselnde Überschrift (ein Durchlauf, Pause-Knopf), Karte zeichnet
  sich, roter Punkt auf Falkenstein
- Kundenportal: Bühne mit Browserfenster und Handy-Rahmen (Desktop),
  Wisch-Reihe (Handy), Schrittzeile statt „So arbeiten wir“
- Leistungen als nummerierte Zeilen, Preise aus src/data/abos.ts
- Unterseiten ohne eigenen Kopfbereich, Wortlaut der Rechtstexte unverändert
- Tests für Hero, Kundenportal, Leistungen/Preise und Layout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 15:28:38 +00:00
rootandClaude Opus 5.5 69d006334f Wisch-Reihe für Handys: Scroll-Snap mit angeschnittener Folgekarte und Zähler
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:20:43 +00:00
rootandClaude Opus 5.5 822481f2e4 Relaunch Phase 1: Gestaltungs-Grundlage, Kopfleiste, Footer, neue Startseiten-Struktur
Token für Hell und Dunkel, eine Schriftfamilie (Instrument Sans selbst gehostet,
?schrift=a für die Systemschrift), gemeinsames Layout für alle Seiten,
Abschnitte Über uns und Kontakt, Platzhalter für Hero, Kundenportal,
Leistungen und Preise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:20:18 +00:00
rootandClaude Opus 5.5 11fd118aa7 README: kurze Projektbeschreibung statt Lovable-Vorlage
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:09:17 +00:00
rootandClaude Opus 5.5 6eac6ef8fb Relaunch: Spezifikation, Material (Deutschlandkarte, Portal-Screenshots) und CLAUDE.md
Spec docs/superpowers/specs/2026-10-01-relaunch-design.md (wartet auf Freigabe),
Karte aus Natural Earth (gemeinfrei) mit Punkt Falkenstein, 19 Portal-Aufnahmen
mit erfundenen Demodaten, Regeln für Cloud-Sitzungen in CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:09:02 +00:00
rootandClaude Opus 5.5 80925c2a43 Kaltakquise-Vorlage: gültige Telefonnummer 0176 23726355
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:20:06 +00:00
rootandClaude Opus 5.5 b46527e65d Markenname WEBklar mit normaler Laufweite (B und k berührten sich)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:20:06 +00:00
rootandClaude Opus 5.5 d78338a969 Footer-Anker von Unterseiten, 404 ohne Konsolenfehler, Datenschutz-Link im neuen Tab
ScrollToTop springt bei /#services nach dem Laden der Schriften zum Abschnitt
(vorher landete man oben auf der Startseite bzw. 220 px zu weit unten).
Dazu Quelltext-Test gegen 99,9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:20:06 +00:00
rootandClaude Opus 5.5 e6d603cd0c Kontaktkarte: CTA-Button mit 4,5:1 Kontrast (Weiß auf dunklerem Blau)
Weiß auf #0793CF kam nur auf 3,45:1 (hell und dunkel). Der Button auf der
Kontaktkarte nutzt jetzt hsl(198 93% 35%) = 4,76:1, beim Hovern
hsl(198 93% 30%) = 6,08:1. Der Test nimmt den Button nicht mehr aus,
sondern prüft Ruhe- und Hover-Farbe aller Buttons auf den Karten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:10:27 +00:00
rootandClaude Sonnet 5.5 003f9e67aa Tests: 404-Konsolenrauschen unterdrückt, Glow-Beschreibung präzisiert
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:56:02 +00:00
rootandClaude Sonnet 5.5 897d594dde Karten im hellen Modus lesbar: Kartenfarbe nach Theme
Die BorderGlow-Karten in Values, Kontaktabschnitt und Über uns hatten
dunkle Hintergründe fest verdrahtet, ihr Text nutzt aber Theme-Farben
(Kontrast 1,07:1 im hellen Modus). BorderGlow kennt jetzt
lightBackgroundColor und wählt die Kartenfarbe per CSS nach Theme
(--card-bg-dark / --card-bg-light, html:not(.dark)), ohne Flackern beim
Laden. Dunkel behält exakt die bisherigen Farben (hsl(0 0% 6%), #120F17,
hsl(0 0% 4%)); hell nutzt hsl(0 0% 96%) mit sichtbarem Rand. Der
Kantenschein wird im hellen Modus normal statt mit plus-lighter gemischt
und in Violett gezeichnet, sonst wäre er auf Weiß unsichtbar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:48:36 +00:00
rootandClaude Sonnet 5.5 194301c6af Glow-Überschriften: Fix A aus der Hover-Analyse
Der Glow der Überschriften (Leistungen, Ablauf) wacht jetzt etwa 180 px
vor der SVG-Box auf (Zeiger-Tracking am window, nur feine Zeiger), bildet
den Mauszeiger über getScreenCTM korrekt auf die viewBox ab, blendet
weich per Opacity ein und aus und klebt auf Touch-Geräten nicht mehr.
Screenreader hören das Wort einmal (role=img mit aria-label, Textknoten
aria-hidden). Der Diff aus /root/reports/webklar-relaunch-2026-10-01/
hover/fix/text-hover-effect.fixA.diff ließ sich unverändert anwenden.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:43:43 +00:00
rootandClaude Sonnet 5.5 0d12bbc06a Navigation: Theme-Schalter und Kontakt-Button bekommen wieder Mausklicks
Der Link-Container der Desktop-Navigation ist "absolute inset-0" (hält die
Links mittig), lag dadurch aber über .navbar-actions und schluckte alle
Mausklicks auf Theme-Schalter und Kontakt-Button ab 1024 px. Der Container
ist jetzt pointer-events-none, die Links pointer-events-auto; Position und
Optik bleiben identisch, die Hover-Pille der Links funktioniert weiter.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:43:29 +00:00
rootandClaude Sonnet 5.5 b2e17d96c2 Hintergrundbilder nur im Dark Mode
problem_blitz.jpg, loesung.jpg und backgroud_effect.webp sind dunkle
Bilder mit 30 % Deckkraft und wurden im hellen Modus zu harten grauen
Quadraten. Die Bild-Ebenen sind jetzt "hidden dark:block"; der dunkle
Modus bleibt unverändert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:43:16 +00:00
rootandClaude Sonnet 5.5 2e65829e15 Anker: scroll-margin-top von 6rem für section[id]
Sprünge zu #features, #services, #process und #contact landeten unter
dem fixen Header; die Sektionen halten jetzt 6rem Abstand nach oben.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:43:01 +00:00
rootandClaude Sonnet 5.5 77672b55d3 Seitentitel und Beschreibung für Abo-, AGB-, Impressum- und 404-Seite
usePageMeta setzt jetzt auch auf /wartungs-abo, /hosting-abo, /agb,
/impressum und der 404-Seite Titel (Wartungs-Abo, Hosting-Abo, AGB,
Impressum, Seite nicht gefunden; der Hook ergänzt " | WEBklar") und
Beschreibung. Die Abo-Beschreibungen nennen nur Preis, Mindestlaufzeit und
Leistungen, die auf der jeweiligen Seite stehen; Impressum nennt Winnweiler
wie die Seite selbst. Die 404-Seite behält die Standardbeschreibung.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:42:43 +00:00
rootandClaude Sonnet 5.5 f0ae8c0be2 Kontaktformular: Datenschutzhinweis unter dem Senden-Button
"Wir verwenden Ihre Angaben nur, um Ihre Anfrage zu beantworten. Mehr dazu
in unserer Datenschutzerklärung." als kleiner, gedämpfter Text mit Link auf
https://project.webklar.com/datenschutz.html (gleiches Ziel wie im Footer).
Textfarben sind die Theme-Token (muted-foreground / foreground), in Hell
und Dunkel über 4,5:1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:42:27 +00:00
rootandClaude Sonnet 5.5 80099cbdb4 Schreibweise: WEBklar statt Webklar im sichtbaren Markennamen
Header (Desktop und Mobil) und die Kopfzeilen der Unterseiten (Kontakt,
Über uns, AGB, Impressum, Wartungs-Abo, Hosting-Abo) schrieben die Marke
"Webklar". Rechtstexte bleiben unverändert, nur der Marken-Schriftzug in
der Kopfzeile ändert sich.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:42:08 +00:00
rootandClaude Sonnet 5.5 8d3424967c 404-Seite auf Deutsch mit lesbaren Theme-Farben
Überschrift "Seite nicht gefunden", Hinweis "Diese Seite gibt es nicht
oder nicht mehr." und Link "Zur Startseite" statt der englischen Vorlage.
bg-background/text-foreground statt festem grauen Hintergrund, damit der
Text in Hell und Dunkel lesbar ist. Der HTTP-Status bleibt 200 (Serveränderung
außerhalb dieses Pakets).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:41:54 +00:00
rootandClaude Sonnet 5.5 900d3a9ab5 Unterseiten: gemeinsamer Footer über ein Layout
/kontakt, /ueber-uns, /agb, /impressum, /wartungs-abo, /hosting-abo und
die 404-Seite bekommen den vorhandenen Footer (Impressum, Datenschutz,
Kündigen/Widerrufen) über ein gemeinsames Layout in src/routes.tsx. Die
Seiten selbst bleiben unverändert, damit die bestehenden Abo- und
AGB-Tests nicht auf doppelte Footer-Links stoßen. Die Startseite behält
ihren eigenen Footer, /datenschutz leitet weiter und bleibt ohne.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:41:38 +00:00
rootandClaude Sonnet 5.5 c75befe8e5 Erfolgsversprechen entschärft, tote Sektionen gelöscht
"Planbare Neukunden" wird zur Karte "Kundenportal", das Umsatzversprechen
in der Lösungs-Überschrift entfällt, Punkt 04 heißt "Hosting & Betreuung"
statt "SEO & Betreuung" (Sichtbar bei Google), Schritt 4 im Ablauf und die
Über-uns-Karte verweisen auf das Wartungs-Abo statt auf offene Optimierung.
AgitationSection und DifferentiationSection waren nirgends eingebunden und
sind gelöscht.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:41:15 +00:00
rootandClaude Sonnet 5.5 f337102c7a Antwortzeit: "spätestens am nächsten Werktag" statt 24 Stunden
index.html (Meta-Beschreibung), Standard-Beschreibung im Meta-Hook,
Startseiten-Meta, Kontaktseite (Meta, Einleitung) und Erfolgsmeldung des
Formulars versprechen jetzt nur noch eine Antwort spätestens am nächsten
Werktag. Weitere 24-Stunden-Versprechen gibt es im Quelltext nicht.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:41:06 +00:00
rootandClaude Sonnet 5.5 b897ae834f Eine Telefonnummer überall: 0176 23726355
Footer, Kontaktabschnitt der Startseite und Kontaktseite zeigten noch
0170 4969375, Impressum und Abo-Seiten schon 0176 23726355. Jetzt überall
0176 23726355 mit tel:+4917623726355.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:40:17 +00:00
rootandClaude Sonnet 5.5 c246b9811f Projekte-Bereich ausgeblendet: erfundene Referenzen entfernt
ProjectShowcase (vier erfundene Referenzen mit Unsplash-Hotlinks) ist
gelöscht, aus der Startseite genommen und der Nav-Punkt "Projekte"
(#projects) fällt in Desktop- und Mobil-Menü weg. Kein Link auf
#projects bleibt übrig.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:40:05 +00:00
rootandClaude Sonnet 5.5 a27b1fe255 Hero: ehrliche Fakten statt Zahlenleiste
Die Leiste mit Projektzähler (/api/projects), "Am Markt"-Ticker,
99,9 % Systemverfügbarkeit und Flagge ist ersetzt durch drei belegbare
Fakten ohne Count-up: Falkenstein (Server in Deutschland), Live-Vorschau
(im Kundenportal) und ab 20 € (Hosting im Monat). Einspaltig auf dem
Handy, ab md drei Spalten. Der zweite Button "Projekte ansehen" ist
entfernt, ebenso Projekt-Query, FOUNDING_DATE und src/lib/projects.ts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:39:46 +00:00
rootandClaude Sonnet 5.5 79c3551934 Routen in eigene Datei ausgelagert
AppRoutes in src/routes.tsx, damit die Tests die echte Routenliste im
MemoryRouter rendern können. Keine Verhaltensänderung. setup.ts bekommt
Attrappen für IntersectionObserver und ResizeObserver (jsdom kennt sie nicht).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 10:35:48 +00:00
rootandClaude Opus 5.5 72f278b478 AGB § 9: Zeiterfassung in 15-Minuten-Schritten und Unterstützung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:27:33 +00:00
rootandClaude Opus 5.5 034892c899 AGB § 11 Haftung: wirksame Regel nach dem Muster der Abo-Bedingungen
Die alte Fassung schloss die Haftung für Umsatzausfälle, Datenverlust,
Systemausfälle und externe Dienste aus, haftete nur bei Vorsatz und
grober Fahrlässigkeit und deckelte auf den Auftragswert. Gegenüber
Verbrauchern unwirksam (§ 309 Nr. 7 a/b BGB), bei wesentlichen
Vertragspflichten auch gegenüber Unternehmern (§ 307 BGB).

Neu, wie § 13 der Abo-Bedingungen (v4-Entwurf): unbeschränkt bei
Vorsatz, grober Fahrlässigkeit, Leben/Körper/Gesundheit, nach dem
Produkthaftungsgesetz und bei Garantie; bei leichter Fahrlässigkeit
nur für wesentliche Vertragspflichten und nur für den vertragstypischen,
vorhersehbaren Schaden; Datenverlust außer in den Fällen von Absatz 1
nur in Höhe des Wiederherstellungsaufwands aus den Kopien, die der
Kunde aufbewahrt; gilt auch zugunsten von Mitarbeitern und Beauftragten.

src/test/agb-haftung.test.tsx hält die Kernsätze und die Reihenfolge
der Absätze fest. vitest.config.ts lädt @vitejs/plugin-react statt des
nicht installierten @vitejs/plugin-react-swc (vitest startete auf main
nicht); dieselbe Änderung wie auf feature/hosting-abo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:26:43 +00:00
rootandClaude Opus 5.5 3c9a879d4a Hosting-Abo: Preis 15 € -> 20 € pro Monat
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:29:29 +00:00
rootandClaude Fable 5.1 044f788835 AGB: Verbraucher-Kündigung als Ausnahme formuliert
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 08:41:26 +00:00
rootandClaude Fable 5.1 2bffca4b33 AGB: Haftungssatz Modul C entfällt, Kündigung nach Verlängerung für Verbraucher, Tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 08:16:17 +00:00
rootandClaude Fable 5.1 2c1a4c150c Website: Abo-Seiten an die Vertragsbedingungen angeglichen (Wechsel, Zusatzspeicher, Sicherheit), AGB-Verweise, Tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 20:10:07 +00:00
rootandClaude Fable 5.1 dc27e508dd Hosting-Abo-Seite, Wartungs-Abo mit beliebig vielen Postfächern, AGB Modul C (Entwurf)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 19:43:26 +00:00
191 changed files with 20411 additions and 1545 deletions

No files matched your search

+3
View File
@@ -23,3 +23,6 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
# Rohfotos der Beispiel-Bäckerei (werden von scripts/relaunch-baeckerei-fotos.py von Wikimedia Commons geladen)
docs/relaunch/assets/baeckerei/roh/
+62
View File
@@ -0,0 +1,62 @@
# webklar.com
Website of WEBklar GbR (a small web agency in Winnweiler, Rhineland-Palatinate). Vite + React 18 + TypeScript + Tailwind + shadcn/ui, client-rendered SPA with React Router.
**Current project: the relaunch.** Read `docs/superpowers/specs/2026-10-01-relaunch-design.md` before any change. The material for it (Germany map, customer portal screenshots) is in `docs/relaunch/`.
Status: the spec is waiting for the owner's approval. Once it is approved, write an implementation plan with the `superpowers:writing-plans` skill and save it to `docs/superpowers/plans/`. Implement only after that.
## Commands
```bash
npm install
npm run dev # local dev server
npm test # Vitest + Testing Library (must stay green)
npm run build # production build into dist/
npm run lint # ESLint (main already has old errors; add no new ones)
```
## How this repository relates to the live site
- This GitHub repository is the **workspace**. Its `main` is **not** live.
- webklar.com is built from a separate internal repository on the owner's server. Only the owner, or a session on that server, transfers an approved state there. Never try to deploy from here.
## Rules
- **Language:** everything user-facing is German, in the polite form **"Sie"**. Brand spelling is always **"WEBklar"**. Write commit messages, PR texts and notes for the owner in German.
- **The wording of the legal pages stays unchanged without the owner's explicit OK:** `src/pages/AGB.tsx`, `Impressum.tsx`, `WartungsAbo.tsx`, `HostingAbo.tsx`, and the privacy policy (the `/datenschutz` redirect to the customer portal).
- For the new layout you may change their markup and CSS classes: remove their own header block, replace uppercase and old font/button classes.
- Keep their wording, order and structure.
- The tests in `src/test/abos.test.tsx`, `agb-haftung.test.tsx` and `agb-verguetung.test.tsx` protect them; never weaken or delete these three files.
- `src/test/sofort-paket.test.tsx` may be adapted where it pins old copy or removed components. Keep its source-code scans against banned statements.
- **No invented content:**
- no numbers without a source (no "99,9 %", no project counters);
- no references without the customer's consent (AGB § 13);
- no stock or AI images of people;
- no promises of success (AGB § 2: no result is owed);
- SEO only as a separately ordered service;
- support after launch only through the subscriptions.
- **Fixed facts:**
- Phone: only 0176 23726355 (`tel:+4917623726355`). E-mail: support@webklar.com.
- Reply time: only "spätestens am nächsten Werktag".
- Servers: Hetzner in Falkenstein/Vogtland, Saxony. This applies **only to the website and the customer portal**. Never write "all data in Germany".
- Prices are final prices, small-business rule § 19 UStG, no VAT. Hosting-Abo 20 € per month, Wartungs-Abo 50 € per month, 12 months minimum term each. Extra work 100 € per hour in 15-minute steps, only after approval.
- **Customer portal screenshots** show only the invented demo customer "Bäckerei Sonnenschein". Never use real customer data.
- **Privacy:** no requests to third-party servers. Host fonts, images and scripts yourself; no Google Fonts, no CDNs, no tracking.
- **Secrets:** never commit them. `.env` stays local; `.env.example` holds only public client IDs.
- **This repository may be public.** Do not check in internal data: price calculations, customer names without consent, server details, logins.
- **Before every push:** run `npm test`, `npm run build`, and `npm run lint` (no new errors). For visual changes, put screenshots in the pull request: 1440 × 900 and 390 × 844, light and dark.
- **Work on branches and merge through pull requests.** The relaunch branch is `relaunch`.
- **Code style:**
- Match the existing code.
- German comments where the code already uses them.
- Only `transform`/`opacity` animations, always behind `prefers-reduced-motion`. The spec section 8.4 lists the only exceptions: map outline drawing, a 3× pulse around the map dot, the rotating hero headline (one pass, with a pause button), and the scroll-linked word highlight.
- Hover effects only for `(hover: hover) and (pointer: fine)`.
## Structure (short)
- `src/routes.tsx`: all routes. Subpages share one layout with the footer (`SubpageLayout`).
- `src/App.tsx`: providers, `ScrollToTop`. It jumps to anchors such as `/#leistungen` once the fonts have loaded.
- `src/hooks/use-page-meta.ts`: title and description per route.
- `src/pages/`: pages. `src/components/`: sections and parts. `src/components/ui/`: shadcn.
- `src/test/`: tests. `sofort-paket.test.tsx` holds source-code scans against banned statements: the old phone number, Unsplash, "24 Stunden", "99,9", success promises. Keep these scans when sections are removed.
+11 -68
View File
@@ -1,73 +1,16 @@
# Welcome to your Lovable project # webklar.com
## Project info Website der WEBklar GbR (Vite, React, TypeScript, Tailwind).
**URL**: https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID ```bash
npm install
## How can I edit this code? npm run dev # Entwicklung
npm test # Tests
There are several ways of editing your application. npm run build # Build nach dist/
**Use Lovable**
Simply visit the [Lovable Project](https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID) and start prompting.
Changes made via Lovable will be committed automatically to this repo.
**Use your preferred IDE**
If you want to work locally using your own IDE, you can clone this repo and push changes. Pushed changes will also be reflected in Lovable.
The only requirement is having Node.js & npm installed - [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
Follow these steps:
```sh
# Step 1: Clone the repository using the project's Git URL.
git clone <YOUR_GIT_URL>
# Step 2: Navigate to the project directory.
cd <YOUR_PROJECT_NAME>
# Step 3: Install the necessary dependencies.
npm i
# Step 4: Start the development server with auto-reloading and an instant preview.
npm run dev
``` ```
**Edit a file directly in GitHub** - Regeln für die Arbeit an diesem Repository: [`CLAUDE.md`](CLAUDE.md)
- Aktuelles Vorhaben, der Relaunch: [`docs/superpowers/specs/2026-10-01-relaunch-design.md`](docs/superpowers/specs/2026-10-01-relaunch-design.md)
- Material zum Relaunch: [`docs/relaunch/`](docs/relaunch/)
- Navigate to the desired file(s). Die Kartendaten stammen von Natural Earth (gemeinfrei).
- Click the "Edit" button (pencil icon) at the top right of the file view.
- Make your changes and commit the changes.
**Use GitHub Codespaces**
- Navigate to the main page of your repository.
- Click on the "Code" button (green button) near the top right.
- Select the "Codespaces" tab.
- Click on "New codespace" to launch a new Codespace environment.
- Edit files directly within the Codespace and commit and push your changes once you're done.
## What technologies are used for this project?
This project is built with:
- Vite
- TypeScript
- React
- shadcn-ui
- Tailwind CSS
## How can I deploy this project?
Simply open [Lovable](https://lovable.dev/projects/REPLACE_WITH_PROJECT_ID) and click on Share -> Publish.
## Can I connect a custom domain to my Lovable project?
Yes, you can!
To connect a domain, navigate to Project > Settings > Domains and click Connect Domain.
Read more here: [Setting up a custom domain](https://docs.lovable.dev/features/custom-domain#custom-domain)
+1 -1
View File
@@ -1,7 +1,7 @@
# WEBklar – Cold-Email-Paket (B2B KMU) # WEBklar – Cold-Email-Paket (B2B KMU)
Ziel: **kurze Antwort** oder **15-min. Potenzialanalyse** (unverbindlich). Ziel: **kurze Antwort** oder **15-min. Potenzialanalyse** (unverbindlich).
Link: https://webklar.com/kontakt · E-Mail: support@webklar.com · Tel. 0170 4969375 Link: https://webklar.com/kontakt · E-Mail: support@webklar.com · Tel. 0176 23726355
--- ---
+60
View File
@@ -0,0 +1,60 @@
# Material für den Relaunch
Dieses Verzeichnis gehört zur Spezifikation `docs/superpowers/specs/2026-10-01-relaunch-design.md`.
## Deutschland-Karte
- `assets/deutschland-umriss.svg` – Umriss von Deutschland mit rotem Punkt auf dem Serverstandort Falkenstein/Vogtland (Sachsen).
- Ein `<path>` (nur Linie, `stroke="currentColor"`, `vector-effect="non-scaling-stroke"`) plus ein `<circle>` (`#e5251c`), viewBox `0 0 663 892`, ca. 7 KB.
- Quelle: **Natural Earth**, „Admin 0 – Countries“, Maßstab 1:10 m. Gemeinfrei (Public Domain): „All versions of Natural Earth raster + vector map data … are in the public domain.“ Ein Quellenhinweis ist nicht nötig („Made with Natural Earth.“ ist freiwillig).
- Behalten: Festland, Rügen, Usedom, Fehmarn, Sylt, Föhr/Amrum. Vereinfacht (Douglas-Peucker, ca. 0,9 km Toleranz).
- Projektion: Lambert-Azimutal-flächentreu (wie ETRS89-LAEA), Mitte 10° O / 52° N, Kugelradius 6371 km. 1 Einheit ≈ 1 km. Geprüft: Berlin–München 504 km auf der Karte wie in echt.
- Punkt Falkenstein (50,477 N, 12,371 O): `cx = 469.6`, `cy = 520.5` (70,8 % von links, 58,4 % von oben; liegt im Festland, 13,9 km von der tschechischen Grenze).
**Weitere Punkte setzen** (z. B. Büro Winnweiler, ca. 49,567 N, 7,850 O – Koordinaten vor Einbau prüfen):
```js
const R = 6371, lon0 = 10 * Math.PI / 180, lat0 = 52 * Math.PI / 180;
// Grad -> SVG-Koordinaten in der viewBox 0 0 663 892
function project(lon, lat) {
const l = lon * Math.PI / 180, p = lat * Math.PI / 180;
const k = Math.sqrt(2 / (1 + Math.sin(lat0) * Math.sin(p) + Math.cos(lat0) * Math.cos(p) * Math.cos(l - lon0)));
const x = R * k * Math.cos(p) * Math.sin(l - lon0);
const y = R * k * (Math.cos(lat0) * Math.sin(p) - Math.sin(lat0) * Math.cos(p) * Math.cos(l - lon0));
return [x + 289.794 + 12, 341.920 - y + 12];
}
project(12.371, 50.477); // -> [469.6, 520.5] Falkenstein
```
Für das Zeichnen des Umrisses per `stroke-dasharray` das Attribut `vector-effect="non-scaling-stroke"` entfernen und `pathLength="1"` setzen (sonst stimmen die Strichlängen nicht).
## Kundenportal-Screenshots
`assets/portal/` – Aufnahmen des echten Kundenportals (project.webklar.com) mit **erfundenen Demodaten** („Bäckerei Sonnenschein“, Rechnungsbeträge, Chatverlauf). Es sind keine echten Kundendaten enthalten. Das Brotfoto ist eine selbst gezeichnete Grafik, die Demo-Website der Bäckerei eine selbst erstellte Seite; beides sind keine Stock- oder KI-Bilder. Aufgenommen am 2026-10-01 mit dem damaligen Stand des Portals.
Vor dem Livegang neu aufnehmen (das Aufnahme-Werkzeug liegt intern beim Inhaber): wenn sich das Portal sichtbar ändert, und damit Auftrags-, Rechnungs- und Kundennummern offensichtlich erfunden aussehen (z. B. Auftrag 1001, Rechnung 2026-0001). Die heutigen Nummern sind zwar erfunden, wirken aber wie echte Zählerstände.
| Datei | Größe (px) | Inhalt |
|---|---|---|
| `00-uebersicht.png` | 2360 × 1229 | Übersicht aller Aufnahmen (nur zur Orientierung, nicht ausliefern) |
| `desktop-1-projekte.webp` | 2880 × 1800 | Projektübersicht mit Live-Vorschau der Website |
| `desktop-2-fortschritt.webp` | 2880 × 1800 | „Was wir zuletzt erledigt haben“ – Fortschritt in Klartext |
| `desktop-3-chat.webp` | 2880 × 1800 | Chat mit Sprachnachricht und Datei |
| `desktop-3b-chat-foto.webp` | 2880 × 1800 | Chat mit Foto |
| `desktop-4-rechnungen.webp` | 2880 × 1800 | Rechnungen |
| `desktop-5-stundenkonto.webp` | 2880 × 1800 | Stundenkonto des Wartungs-Abos (nur nach Freigabe zeigen) |
| `desktop-6-funktionen.webp` | 2880 × 1800 | Funktionen (Terminplaner/Analyse dort sind Platzhalter – nicht bewerben) |
| `desktop-7-news-pinnwand.webp` | 2880 × 1800 | News-Pinnwand mit Vorschau |
| `phone-0-login.webp` | 780 × 1688 | Anmeldung (Handy) |
| `phone-1-projekte.webp` | 780 × 1688 | Projekte mit Vorschau (Handy-App) |
| `phone-1b-tabbar.webp` | 780 × 192 | untere Tab-Leiste der Handy-App |
| `phone-2-projekte-lang.webp` | 780 × 5428 | lange Projektseite zum Durchscrollen im Handy-Rahmen |
| `phone-3-projektdetails.webp` | 780 × 1688 | Projektdetails mit letzten Änderungen |
| `phone-4-chat.webp` | 780 × 1688 | Chat |
| `phone-4b-chat-foto.webp` | 780 × 1688 | Chat mit Foto |
| `phone-5-rechnungen.webp` | 780 × 1688 | Rechnungen |
| `phone-6-vertraege.webp` | 780 × 1688 | Verträge (nur nach Freigabe zeigen) |
| `phone-7-news.webp` | 780 × 1688 | News-Pinnwand bearbeiten |
| `phone-8-news-vorschau.webp` | 780 × 1688 | News-Pinnwand, Vorschau „So sehen es alle“ |
Die Handy-Aufnahmen haben oben 47 px und unten 34 px Rand für Uhrzeit und Home-Leiste (Safe Area). Die Dateien sind Rohexporte in doppelter Auflösung; für die Website verkleinern (Budgets in der Spec, Abschnitt 9.5).
+17
View File
@@ -0,0 +1,17 @@
# Fotos der Beispiel-Website „Bäckerei Sonnenschein“
Die Bäckerei ist erfunden (Demo-Kundin des Kundenportals). Die Fotos zeigen nur Backwaren, keine Menschen.
Alle stammen von Wikimedia Commons und stehen unter **CC0 1.0** (gemeinfrei, keine Namensnennung nötig).
Verkleinert und zugeschnitten mit `scripts/relaunch-baeckerei-fotos.py` nach `public/relaunch/demos/baeckerei/`.
| Datei in public | Commons-Datei |
| --- | --- |
| roggenbrot-600.webp | [Rye bread Roggenbengel 2025-02-14 13.jpg](https://commons.wikimedia.org/wiki/File:Rye_bread_Roggenbengel_2025-02-14_13.jpg) |
| roggenbrot-anschnitt-400.webp | [Cut rye bread Kartoffelkruste 2025-02-20 02.jpg](https://commons.wikimedia.org/wiki/File:Cut_rye_bread_Kartoffelkruste_2025-02-20_02.jpg) |
| broetchen-400.webp | [Bread-rolls.jpg](https://commons.wikimedia.org/wiki/File:Bread-rolls.jpg) |
| croissants-400.webp | [Freshippo croissant bread.jpg](https://commons.wikimedia.org/wiki/File:Freshippo_croissant_bread.jpg) |
| zimtschnecken-400.webp | [IKEA cinnamon bun.jpg](https://commons.wikimedia.org/wiki/File:IKEA_cinnamon_bun.jpg) (Ausschnitt ohne Logo oder Schrift) |
| baguettes-700.webp | [Baguette-246424.jpg](https://commons.wikimedia.org/wiki/File:Baguette-246424.jpg) |
| zwetschgenkuchen-520.webp | [Plum cake, Ehrenbach.jpg](https://commons.wikimedia.org/wiki/File:Plum_cake,_Ehrenbach.jpg) |
Lizenz von jeder Datei am 2026-10-04 über die Commons-API geprüft (`LicenseShortName` = CC0).
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 663 892" role="img" aria-labelledby="t d">
<title id="t">Umriss von Deutschland mit Serverstandort Falkenstein</title>
<desc id="d">Roter Punkt: Hetzner-Rechenzentrum Falkenstein/Vogtland, Sachsen (50,477 N, 12,371 O). Umriss: Natural Earth 1:10m (Public Domain), Projektion ETRS89-LAEA, vereinfacht.</desc>
<path d="M581.4 706.2l-2 5.8 2.5 2-1 2.4 0.4 7-1.8 4.8-2.9 1.2-1.1 4.3-7-4.5-7.7-1.8-4.8 1.5-1 1.4 1.6 4.9-3.3 15.7-9.3 8.2-10.1 2.2-7.6 3.4-7.1 6.3-5.2 1.6-8.8 9.6 1.8 5.4 5.4 5.2 2.6 5.8 5.3 4 4.8 8.5-6.9 14 5.5 1.8 4.1-1 4.2 5.8 0.1 4.1-2.3 4.3-0.6 4.8 0.9 5.3-2.5 3.2-5.4-0.7-11.8-8.6 0.4-5.3 2-1.1-4.8-4 0.7-1.9-8.2-0.7-8.4 5.2-3.1 0.4-4-2.9-1.7-3.9-13.8 1.8-1.2-1.8 1.3-4.1-4.8 3.1 2.4 6.6-2.4 4.6-17.8-0.1-6.3 1.4-2.3 2.7-15-1.2-5 8.5-10.4 1.1-3.5 4.1 1.8 0.8-0.4 1.9-5.8 1.2-5.1 4.7-1.8-0.2 0.1-3-1.6-0.7-8.3 4.5-7.8 0-1.9-2.1 0.3-2.5-3.8-4-3.9-1.8-0.5-0.8 3.1-2.5-2.6-1.7-7.3 2-1.6-1.7-11.8-3.6-3.5 3.6-4.1-0.3-2.1-1.5 0.2-3.6-2.1 0.2-1.1 0.5 1.3 3.4-0.9 5.5 2.2 2.2 0.3 3.8-1.7 4.7-4.3 3.2-4.9 7.3-5 2.7-6 0.8 2.3-3 1.4-8.3-1.4-0.7-8.2 2.2-2.2-5.1 1.7-1.3-0.3-2.5-3.7-5.4-3 0.8-2.8-6.7-8.4 0.7-1.5-0.8-0.4-3.8-2-1.4-2.2 0.7-3.6 5.6-6.9 1.1-4.7-1.4-20.7-13.1-5.7-0.7-1-1.6-12.5-1.1-5.4 2.7-4.7-0.3-3.3-2.8 1.4-1.1-4.3-3.3-2-0.1-0.1 2.8-4 0-1.2-3.2 1-3.8-2.3-0.2-2.8-3.6-2 3.7-1.1-4.2-1.4-0.6-3.1 0.5 0.7 1.8-1.1 0.5-5.5 1-2 4.5-3.6 3.4 0 3 4.9 2.9 8.3-2.4 2.9 0.8-0.5 2.6-1.5-0.7-0.1 4.3-1.5 1.2-2.8-3.7-4.6 1.7-1 2.4-7.1 0.2-3.6-1.3-1.3-2.7-4.1-0.8-4.1 0.6-7 5.2-3.4 0.7-9.8-0.3-0.9-3-5.9-1-4.1 4.2-6.3 1.1-5.4-2.4 1.9 0 1.9-3.4-5.6 1.1 0.4-2.6-5.1-7.6-0.4-3.6 2.1-2.7-0.7-5.7 2.9-6.1-0.1-4.9 2.1-6.3 2.8-3.4 0-3.4-3.1-5.8 0.5-6.8 2.8-7.1 5.7-8 1.3-6.4 3.7-3.3-0.7-7.2 3.1-10.1 2.8-2.3 0.1-10.6 3.2-4.8 8-6.7 1-4 3.4-0.4 1.7-3.2 3.8-1.8 8.5-16.5-8-2.5-11.4-6.5-5.5 0.1-6-1.9-3.4 1.2-6.7-0.4-1.9-2.8-6.3-2.6-3.4-5.1 0.4-2.5-4.4-0.9-0.3-1.2-4.3 0.9-1.4 2.7-2.7 0.6-1.7 2.5-8.7-1.4-5.3-3.2-0.6 2.3-2.3 1.4-1.5-1.4-0.6-7-6.8-3-5.8-0.6 0.2 5.2-1.4 1.6-6.9-1.1-1.7-2 0.3-4.6-1.6 0.6-2.7-7.2-5.7-6.4 0-2.3 1.7-0.9-4.5-7.8-7.6-5-4.2 1 1-12.4 5.2-7.8-1-1.9 7.3-5.9-0.9-2.5 1.5-6.8-7.5-1.8-4.3-3.7-2.3 0.5-1-3-4.4-3.1-3.2-7.9-0.9 1.5-4-12 2.8-9.4-0.4-3.1 3.2-2.1-1.1-3.6 3.2-3 6-2.5 1.2-4 6.4-1-2.4-5.9 1.4-7.3-1.8-3.7 1-1.6-5.7-1.8-3.4 0.8-1-3.3-1.4-0.4-0.6-2.9 5.4-4.5 1.3-3-6.3-1.2-0.8-1.7 1-0.5-5.2-8.2-4.9 0.3 0.1-3.2-2.6-6.6 2.1-0.5 1-4.4 3.4-1.2 0.8-6-4.3-2.7 0.5-4.9-5.3-0.8-3.7 1.2-1.1-8.7 2-0.9 3.9 2.6 3.4-4.7 11.9-8-1.4-1.5 2.3-1.5-1.6-0.6-3.6 2.2-1.6-1 0-4.6 6.9-11.4 3.3-2.8 1.3-4.7-1-1.3 0.7-12.3-6.9-10 1.2-5.4-6-4.4 0.4-3.8-4.9-2.1 2-4.8-2-4.5 10.7-3.6 4.9 1.5-4.1-5 2.4-1.2 6.5 4.2 2.7-0.3 1.5 2.3 3.7 1.3 0.1 1.7 2.6-0.6-0.1-3.6 5.7 1.6 4.7-2.9 14.9-2.9 4.8-5.6 0-2.1-8.9-6.6 0.5-2.8 2.2-0.6 2.8-3.9 5.6-0.9 3.9-5.1 3-1.1 7.1-6.8-0.9-5.5 2.8-8.8-4.6-9.8-5.1 1.9-10.6-2.9-3.1-2.6-1.2-7.4 5-1.8-2.6-2.7 0-4 2.5-1.6 8.7-0.4 10.2 2.2 1.3-2.3 2.5-19.6 7.2-11.8 2.4-7.2-0.8-15.6 2-11.8 2.7-3.1-0.2-2.6 1.8-2.5 2.4-0.4 4.9 2.6-4.1-3.5-15-1.1-2.4-1.4-0.6-11.3 2.2-6.8 0.6-1 3.9 0.7 1.6-2.2-3.4-4.2 0.9-2.7 8.6-7.3 5.3-1.8 10.1-0.8 2.9 1.9 29.6-3.9 5.2 1.7-0.5 4 1.7 4 4.8 5.2-0.6 2.1 3.2 2.2-0.9 3-6.5 2.5 1.7 5.1 4.3 0.9 3.4 4.4 3.6-0.1 3.9-6.9 0.4-3.8-1.8-2.3-4 0.4 2.8-9.7 3.9-0.5 3.7 4.7 10.8 3.9-4.2 6.3-0.5 9.5 1.1 4.8-0.1-13 4-4.7 0.6-3.2-3.6-8.8-1.3-8.4 6.1-17.4 5.2-3.7 8.5 5.7 5.1 1.3 10.2-0.9 6.5-2.6 10.5 0.1 8.3 13.7 5.4 5.9 5.3 10.1 8.6 4.8 8.9 1.5-3.3-2.9-5.9-0.8-7.2-4.1-2.1-2.4-1.2-8.5-6.3-4.3-2.6-9.1-9-6.2-10.7-0.9-4.2-1.7-1.4 1.6-1.5-0.5-4.5-4.5-5.2-11.1 3.8-1.2 5.5 2.7 1.4-0.7 1.5-7.5-2.8-3.4 0.2-1.9-1.6-0.5-6.1 2.6-3-6.2 2.3-5.8-0.4-2.5 5.4-2 2.7-4.8-5.3 3 0.4-1.5-9.7 1-3.9 2.6-3.6-2.4-1.4-5.3 2.7-1.9 3.6-0.3-2.9-2.2-1.1 1.4 1-4.6 15.4-1 7.8-5.2 0.5-4.5-8.4-11.2-3.5 0.8 1.8-2.3-2.9-7.6-7.4-5.5 0.4-2.7-3.1-9.4 1.2-5.8 4.6 0.9 5.9-1.8 5.1 1 18.5 5.5 3.1 5.5 5.7-0.2 3.4-1.7 4.2 1 8.3-6.1 0.2 4 9.7 2.6 4.3 2.6 1.4 3 4.6-1.4-0.3-2.3 3.6 1.2 4.6 8.7-3.1 0-2.6 3 4.3-0.5 2.7 1.3-0.8 11.9-3.9 6-8.1 3.4 2.8 1.5 16.8-3.3 3.9 3.4-1.3 1.3 0.5 5.8-3.2 2.7 0 5.4 2.5-1.9 2.8-8.1 6.1-3.3 2.7 0.2 15.6 8.4 5.9 5.4 2.6 0.6 3-0.6 10.2-7.2 5.4-0.8-2.8 0.8 3.8 1.2 4.5-2.9 2 0.8-3 3.8-1.4-0.7 2.1 14.8-1.8 4.6-11.2 9.2-5.7 0.9-3.4 5 3.2 5.5 4.9 0.5 0.8 3.5 2.4 0.3 8.4-5.4 8-1.2 1.9 3.2 2.7 1.1-0.1 3.7 3.4 0.5 2.6-2.4 8.2 6 0.8-7 1.6-1.5-0.4-2.2 2.2-2.3-0.5-1.6 3.5-0.3 3.3-5.1-0.1-2.9 4.1-5.1 8 0.4 17.8-5.4 0.5 8.9 1.6 1.7-1.5-5.2 3-4-2.6 0.8 0.4-1 4.9-6.9 9.9-7 9.7-19.2 2-1.7 1 2.9 2.8 1 18.7-0.4 2.7 1.6-3.2 2-11.4-1.4-1.5 1.5-3-0.2-1 1.9-1.7 0.1 0.9 2.2-2.7-0.4-1.6 2.1-2.4-1.9-3.4 0.4-0.8 4.8-1.8-0.7-0.2 3.4-1.8 1.2 0.5 4.6 2.9 1.9 3-0.6-3.2-1.4 3.9-4.7 0.3-2.4 5.2-3.2-1.3-1.4 2.1-0.9 1.3 1.1 5.3-2.8 0.2-2.1 2.3-0.4-2 4.7 1.3-3.1 2 2.3 3.3-1.6 2.2 3.7 2.3-0.1 4.6-6.5 5-2.8 1.7 0.4-0.2 3.6 4.5 3.1-1.2 2.2 2.9 7.9 3.6 0.3-1.7 1.6 9.6 2.7 4.2 5.8-1.7 1.6 5.3-1-1.3 2.4 2.3-0.2 3.4 6 2.7 0.7-2-3.7 8-2 6.9-4.2-0.8 2.3 7.5 5.1-2 6.6-1.7 1.1 6.4 5.3 0.8 3.4 4 2.2-5.5 6.5 0.1 2.8 15 10 9.9 1.2 3-2.6 2 1-0.1 2.6-3 2.4 3.4 0.7 4 21.1 4.1 7 4 18.1 2.7 2.9-3.6 7.1 0.9 6.4-2.5 9.4-12.7 10.6 1.8 7.1-2.4 5.2 18 11.4 3.7 5.6 15.1 11.2-2 6.7 1.7 2.3-2.4 5.4-3.3 3.3 0.7 4.4 3.5 7.9-0.3 2 8 3.7 1.1 1.9-0.9 12.8 5.4 4.6-3.6 9.7-0.3 9-5.7 8.1-0.4 4.3 3.5 3.7 1.4 5.6 6.2 6.2 1.1 5.7-1.8 4.5 0 4.1 14.4 5.9 3.3 3.5 1.1 11.1 4.6 6.8 0.4 3.8-5.9 27.7-3 8.6-3.2 4.7-1.3 6.4-3.3 1.Line truncated
<circle cx="469.6" cy="520.5" r="5" fill="#e5251c"/>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,633 @@
# Relaunch webklar.com – Design-Spezifikation
- **Stand:** 2026-10-01
- **Status:** Entwurf, wartet auf Freigabe durch den Inhaber. Nach der Freigabe wird daraus ein Umsetzungsplan geschrieben (Skill `superpowers:writing-plans`, Ablage `docs/superpowers/plans/`).
- **Grundlage:** Prüfung der Live-Seite vom 2026-10-01 (Desktop, Handy, Texte, Code, Kundenportal, Design-Recherche) und die Wünsche des Inhabers. Die Prüfberichte selbst liegen intern; alles, was für die Umsetzung nötig ist, steht in diesem Dokument.
- **Material:** Deutschland-Karte und Kundenportal-Screenshots in `docs/relaunch/assets/` (Übersicht: `docs/relaunch/README.md`).
---
## 1. Ziel
Wer eine Website oder eine digitale Dienstleistung sucht und auf webklar.com landet, versteht in den ersten Sekunden, wer WEBklar ist und was er bekommt. Das passiert über Bilder, Bewegung und kurze Sätze statt über Textblöcke. Die Seite wirkt modern und ruhig, mit Abständen und Typografie wie bei Apple-Produktseiten. Sie darf nicht nach KI-Vorlage aussehen, ist übersichtlich, sagt nichts doppelt und ist auf dem Handy kein Endlos-Scroll.
### Erfolgskriterien
| Nr. | Kriterium | Wer prüft, wie |
|---|---|---|
| E1 | Fünf-Sekunden-Test: Eine Testperson kann danach sagen „Die bauen Websites, Shops und Apps für kleine Betriebe, und man sieht alles in einem Kundenportal.“ | **Inhaber**, mit 2–3 Personen, die die Seite nicht kennen |
| E2 | Startseite auf dem Handy (390 × 844) höchstens 8 Bildschirmhöhen lang (heute 14) | Umsetzung: `document.documentElement.scrollHeight / innerHeight` |
| E3 | Jede Aussage steht genau einmal auf der Startseite (Regeln in 6.3) | Umsetzung: Textprüfung |
| E4 | Heller und dunkler Modus sind beide gestaltet, alle Texte erreichen mindestens 4,5 : 1 (ab 24 px mindestens 3 : 1) | Umsetzung: Kontrastmessung im Browser und Token-Test |
| E5 | Startseite lädt höchstens 130 KB JavaScript (gzip), kein WebGL; LCP auf dem Handy unter 2,5 s, CLS unter 0,1 | Umsetzung: `vite build`, Lighthouse Mobile |
| E6 | Alle Routen und Rechtsseiten funktionieren, die Texte der Rechtsseiten sind unverändert, die Rechtstext-Tests bleiben grün | Umsetzung: `npm test` |
| E7 | Austausch-Test: Mit einem anderen Agenturlogo passt die Seite nicht mehr, weil Texte, Karte und Portal nur zu WEBklar passen | **Inhaber**, Sichtprüfung |
---
## 2. Ausgangslage
- **Technik:** Vite, React 18, TypeScript, Tailwind, shadcn/ui, React Router. Reine Client-SPA. Das Kontaktformular schreibt per Appwrite-Client in eine Datenbank.
- **Live-Betrieb:** webklar.com wird aus einem internen Repository gebaut. **Dieses Repository ist der Arbeitsort für den Relaunch;** sein `main` ist nicht live. Was hier fertig ist, wird nach Freigabe des Inhabers übertragen (Abschnitt 12).
- **Befunde der Prüfung (Kurzfassung):**
- Die Startseite erzählt dieselbe Botschaft viermal (Problem, Lösung, Nutzen-Karten, Leistungen). „Leistungen“ und „Ablauf“ sind dieselben vier Schritte, „Potenzialanalyse“ steht fünfmal da.
- Positionierung passt nicht: Die Seite verkauft „Digitalisierung für KMU“ mit CRM, Pipeline und Kennzahlen. Tatsächlich baut WEBklar Websites, Online-Shops, Web-Apps und Automatisierungen für kleine Betriebe.
- Vorlagen-Look: drei Schriftfamilien, vier WebGL-/Effekt-Komponenten auf fast jedem Abschnitt, cyan-blaue Akzentfarbe, Karten mit Kreis-Symbolen, hochzählende Kennzahlen, Technik-Laufband.
- Handy: 14 Bildschirmhöhen bei rund 630 Wörtern, keine horizontale Interaktion, Hover-Effekte ohne Funktion auf Touch, Menü ohne Scrollsperre.
- Der helle Modus war nie gestaltet (dunkle Kopfleiste auf Weiß, Pixelpunkte über Text, unsichtbare Leuchtschrift).
- Die Startseite lädt 476 KB JavaScript (gzip), davon 232 KB nur für den 3D-Hintergrund im Hero. `prefers-reduced-motion` wird nirgends beachtet.
- **Bereits live behoben (Sofort-Paket, 2026-10-01):**
- Kontrast im hellen Modus, Theme-Schalter klickbar, Leuchtschrift-Fehler, Anker unter der Kopfleiste.
- Ehrliche Fakten statt „52+ Projekte“ und „99,9 %“; erfundene Referenzen und Unsplash-Bilder entfernt.
- Eine Telefonnummer; „spätestens am nächsten Werktag“ statt „24 Stunden“; Erfolgsversprechen entschärft.
- Footer und Rechtslinks auf allen Unterseiten (gemeinsames Layout in `src/routes.tsx`), Datenschutzhinweis am Kontaktformular, eigene Seitentitel, deutsche 404-Seite.
- Tests dazu: `src/test/sofort-paket.test.tsx`.
- **Übergangslösung:** Bis zum Relaunch kann die Live-Seite fest auf „dunkel“ stehen (nicht Teil dieses Repos). Der Relaunch gestaltet beide Modi neu.
---
## 3. Wünsche des Inhabers
1. Die leuchtenden Überschriften („Leistungen“, „Ablauf“, „Projekte“) reagieren erst, wenn die Maus schon im Wort ist. Das muss besser werden.
2. Auf dem Handy gibt es viele Fehler, und die Seite ist nur ein langer Scroll. Beispiel: die Schritte als Karten, die man von links nach rechts wischt.
3. „So arbeiten wir“ soll raus. Stattdessen wird das Kundenportal vorgestellt, mit Screenshots aus dem Portal, durch die man scrollen bzw. wischen kann.
4. Nichts soll doppelt vorkommen.
5. Das Programmiersprachen-Laufband: Hat das heute noch Sinn?
6. „Standort Deutschland“ ist sehr wichtig, aber nicht als Etikett in der unteren Leiste. Wunsch: rechts eine Umriss-Karte von Deutschland (SVG) mit einem roten Punkt dort, wo die Server stehen.
7. Mehr mit den Texten spielen, statt sie nur hinzustellen. Modern, aber nicht nach KI aussehend. Übersichtlicher. Eine echte „Experience“. Abstände und Texte wie auf der Apple-Website.
8. Ein Interessent soll sofort und spielerisch verstehen, was WEBklar ist und macht, am besten bildlich.
Wo jeder Wunsch umgesetzt wird: 1 → D10 und 8.4 · 2 → 7.2, 7.3, 9.4, 9.6 · 3 → 7.2 · 4 → 6.3 · 5 → D8 · 6 → 7.1 · 7 → 7.1, 7.3, Abschnitt 8 · 8 → 7.1 (wechselnde Überschrift), 7.2 direkt danach (Portal-Bilder als zweiter Abschnitt).
---
## 4. Bereits getroffene Entscheidungen
| Nr. | Entscheidung |
|---|---|
| D1 | **Positionierung:** „Websites, Shops und Apps für kleine Betriebe“. Das Kundenportal ist der sichtbare Unterschied zu anderen Agenturen. |
| D2 | **Anrede:** durchgehend „Sie“ (wie Verträge und Rechtstexte). |
| D3 | **Telefon:** nur 0176 23726355 (`tel:+4917623726355`). E-Mail: support@webklar.com. |
| D4 | **Keine erfundenen Zahlen oder Referenzen:** keine Verfügbarkeitsprozente, keine Projektzähler, keine Stockfotos und keine KI-Bilder von Personen. Referenzen nur mit Zustimmung des Kunden (AGB § 13); bis dahin gibt es keinen Projekte-Abschnitt. |
| D5 | **Antwortzeit:** nur „spätestens am nächsten Werktag“ (entspricht den Abo-Bedingungen). |
| D6 | **Keine Erfolgsversprechen** (AGB § 2: kein Erfolg geschuldet). SEO nur als gesondert beauftragte Leistung. Betreuung nach dem Start gibt es nur im Abo. |
| D7 | **Serverstandort:** Rechenzentrum von Hetzner in Falkenstein/Vogtland, Sachsen (50,477 N, 12,371 O). Die Aussage gilt nur für **Website und Kundenportal**. E-Mail-Postfächer und Zahlungsabwicklung laufen anders (siehe Datenschutzerklärung). Deshalb nie „alle Daten in Deutschland“ und nie „Ihre Daten aus Sachsen“ schreiben. |
| D8 | **Das Programmiersprachen-Laufband entfällt.** Es beantwortet keine Kundenfrage, und die Inhalte sind nicht belegt. |
| D9 | **„So arbeiten wir“ entfällt.** Der Ablauf wird eine kompakte Schrittzeile im Kundenportal-Abschnitt. |
| D10 | **Der Leuchtschrift-Effekt entfällt.** Abschnitte bekommen echte `h2`-Überschriften, die beim Scrollen sanft erscheinen. Das funktioniert auch auf dem Handy und für Screenreader. |
| D11 | **Heller und dunkler Modus** werden beide gestaltet. Standard folgt dem System, der Umschalter sitzt in der Kopfleiste (Handy: im Menü). |
| D12 | **Vorgehen:** Zuerst entsteht ein anklickbarer Entwurf der neuen Startseite (echte Seite auf einem Zweig), der Inhaber gibt Rückmeldung, danach folgt die Fertigstellung (Abschnitt 5). |
| D13 | **Die Texte der Rechtsseiten bleiben unverändert** (AGB, Impressum, Wartungs-Abo, Hosting-Abo, Datenschutz-Weiterleitung). Markup und CSS-Klassen dieser Seiten dürfen für das neue Layout angepasst werden: Der eigene Kopfbereich (`glass-nav`-Header) entfällt, weil das gemeinsame Layout ihn liefert; Versalien, alte Schriftklassen und alte Button-Klassen werden ersetzt. Wortlaut, Reihenfolge und Gliederung der Texte bleiben gleich. Die Rechtstext-Tests müssen ohne Änderung grün bleiben. |
---
## 5. Vorgehen in Phasen
### Phase 1 – Entwurf der Startseite
- Die neue Startseite wird **als echte Seite** im Code gebaut (kein Wegwerf-Mockup), auf dem Zweig `relaunch`. Sie ersetzt `src/pages/Index.tsx`.
- Dazu gehören die neue Kopfleiste (6.1), alle Abschnitte aus 6.2 außer „Projekte“, der neue Footer (7.8), beide Modi, Desktop und Handy, echte Texte, echte Portal-Screenshots und die Deutschland-Karte.
- Zum Vergleich gibt es **zwei Schriftvarianten** (8.1), umschaltbar über `?schrift=a` bzw. `?schrift=b`. Der Umschalter ist nur im Entwurf vorhanden.
- Unterseiten bekommen in Phase 1 nur so viel Anpassung, dass sie mit der neuen Kopfleiste und dem neuen Footer nicht kaputt aussehen.
- Die alten Startseiten-Abschnitte bleiben noch als Dateien liegen; Phase 2 räumt sie auf.
- Tests, die alte Texte oder alte Navigationspunkte festschreiben, werden in Phase 1 angepasst (9.8). Die Prüfungen gegen verbotene Aussagen bleiben.
- Für den Inhaber gibt es eine Vorschau (O10) und Screenshots jedes Abschnitts in 1440 × 900 und 390 × 844, hell und dunkel, **je Schriftvariante**, im Pull Request.
### Phase 2 – Fertigstellung
- Rückmeldungen des Inhabers einarbeiten und die gewählte Schrift fest einbauen; der Umschalter `?schrift` wird entfernt.
- `/kontakt`, `/ueber-uns` und die 404-Seite bekommen den neuen Stil. Die Rechtsseiten bekommen nur das neue Layout (D13).
- Aufräumen (9.2), Barrierefreiheit (9.6), Leistungsbudget (E5), Browserprüfung (9.8).
### Phase 3 – Livegang
- Nur mit ausdrücklichem OK des Inhabers. Übertragung ins Live-Repository (Abschnitt 12), danach Prüfung der Live-Seite.
---
## 6. Informationsarchitektur
### 6.1 Kopfleiste und Navigation
| Eintrag | Ziel | Hinweis |
|---|---|---|
| Logo „WEBklar“ | `/` | |
| Kundenportal | `/#kundenportal` | |
| Was wir bauen | `/#leistungen` | |
| Preise | `/#preise` | |
| Über uns | `/#ueber-uns` | Abschnitt auf der Startseite. Die ausführliche Seite `/ueber-uns` ist von dort und vom Footer verlinkt. |
| Projekte | `/#projekte` | erscheint nur, wenn es freigegebene Referenzen gibt |
| Button „Projekt besprechen“ | `/kontakt` | |
| Hell/Dunkel-Schalter | – | |
- **Desktop:** Alles in einer Leiste.
- **Handy (unter 900 px):** In der Leiste stehen nur Logo und Menü-Knopf. Der Menü-Knopf ist ein echter `button` mit `aria-expanded` und mindestens 44 × 44 px groß.
- **Menü-Panel auf dem Handy:**
- enthält die Navigation, den Button „Projekt besprechen“ und den Hell/Dunkel-Schalter;
- dunkelt den Hintergrund ab und sperrt das Scrollen der Seite;
- schließt per Escape, per Tipp daneben und bei Auswahl eines Links.
- Alle Anker-Ziele sind `<section id="…">`, damit `scroll-margin-top` greift (bereits global gesetzt für `section[id]`). Von Unterseiten aus springt `ScrollToTop` nach dem Laden der Schriften zum Anker (vorhanden in `src/App.tsx`).
### 6.2 Startseite (Reihenfolge)
| # | Abschnitt | Anker | Typ | Kernaussage |
|---|---|---|---|---|
| 1 | Hero mit Deutschland-Karte | – | Bühne | Wir bauen Ihre Website, Ihren Shop, Ihre Web-App oder Ihre Automatisierung. Website und Kundenportal laufen auf dem Server in Falkenstein. |
| 2 | Kundenportal | `#kundenportal` | Bühne | Sie sehen Ihr Projekt live: Vorschau, Fortschritt, Chat, Rechnungen. Darunter die Schrittzeile. |
| 3 | Was wir bauen | `#leistungen` | Beleg | Vier Produkte, dazu der Umzug bestehender Websites |
| 4 | Projekte | `#projekte` | Beleg | **nur mit freigegebenen Referenzen**, sonst entfällt der Abschnitt komplett |
| 5 | Preise | `#preise` | Beleg | Angebot vorher, Hosting-Abo 20 €, Wartungs-Abo 50 €, weitere Arbeit nur nach Freigabe |
| 6 | Über uns | `#ueber-uns` | Beleg | Wer dahinter steckt, wo |
| 7 | Kontakt | `#kontakt` | Bühne | Ein Satz, ein Button, E-Mail und Telefon |
| – | Footer | – | – | Navigation, Rechtliches, Anschrift |
- **Bühne** = etwa eine Bildschirmhöhe auf dem Desktop (`min-height: 100svh`), ein Satz plus ein Bild oder Objekt.
- **Beleg** = Höhe nach Inhalt, Text in einer Spalte von höchstens 46 rem.
- Das Kundenportal steht an zweiter Stelle, damit gleich nach dem Hero ein Bild zeigt, was WEBklar anders macht (Wunsch 8).
### 6.3 Jede Aussage hat genau einen Ort
| Aussage | Einziger Ort auf der Startseite |
|---|---|
| Positionierung „Websites, Shops und Apps für kleine Betriebe“ | kleine Zeile im Hero |
| Serverstandort Falkenstein | Kartenpunkt im Hero |
| Kundenportal und Live-Vorschau | Abschnitt Kundenportal (im Hero nur ein Satz im Untertext, ohne Funktionsliste) |
| Ablauf | Schrittzeile im Kundenportal-Abschnitt |
| Kennenlernen (15 Minuten, kostenlos) | Kontakt-Abschnitt; die Schrittzeile sagt nur „Kennenlernen“ |
| Preise | Abschnitt Preise |
| Winnweiler / Pfalz | Abschnitt Über uns (die Anschrift im Footer ist Pflichtangabe und zählt nicht) |
| E-Mail und Telefon | Kontakt-Abschnitt (Footer ohne eigene Kontaktspalte) |
- Den Erstkontakt nennt der Button überall „Projekt besprechen“. Das Wort „Potenzialanalyse“ entfällt.
- Die heutige Faktenzeile im Hero („Falkenstein / Live-Vorschau / ab 20 €“) entfällt.
### 6.4 Unterseiten
| Route | Inhalt im Relaunch |
|---|---|
| `/kontakt` | Formular mit der bestehenden Logik (Appwrite), neue Gestaltung, der Datenschutzhinweis bleibt |
| `/ueber-uns` | ausführlichere Fassung des Team-Abschnitts; der Block „Der Unterschied“ entfällt |
| `/wartungs-abo`, `/hosting-abo`, `/agb`, `/impressum` | Texte unverändert, neues Layout (D13) |
| `/datenschutz` | bleibt Weiterleitung auf die Datenschutzerklärung im Kundenportal |
| `*` | 404 im neuen Layout |
### 6.5 Seitentitel, Beschreibung, Vorschaubild
- **Startseite:**
- Titel: „WEBklar – Websites, Shops und Apps für kleine Betriebe“
- Beschreibung: „WEBklar baut Websites, Online-Shops und Web-Apps. Im Kundenportal sehen Sie jeden Schritt, mit Live-Vorschau Ihrer Website.“
- **Wo ändern:** `index.html` (`title`, `description`, `og:*`, `twitter:*`), `DEFAULT_TITLE` und `DEFAULT_DESCRIPTION` in `src/hooks/use-page-meta.ts` und `src/pages/Index.tsx`.
- **`/kontakt`:**
- Titel: „Projekt besprechen“
- Beschreibung: „Beschreiben Sie Ihr Vorhaben in zwei Sätzen. WEBklar meldet sich spätestens am nächsten Werktag.“
- **`public/og-image.png`** zeigt noch die alte Positionierung. Ein neues Bild (1200 × 630, höchstens 150 KB) im neuen Stil entsteht in Phase 2: Logo, Positionierungssatz und Karte, ohne Personen.
---
## 7. Abschnitte im Detail
Texte in **[eckigen Klammern]** brauchen die Freigabe des Inhabers (Abschnitt 11). Bis dahin gilt der jeweils genannte Ersatztext. Alle anderen Texte sind Entwürfe, die so eingebaut werden.
### 7.1 Hero mit Deutschland-Karte
**Text (links):**
- **Kleine Zeile darüber:** „Websites, Shops und Apps für kleine Betriebe“
- **Überschrift `h1` mit wechselndem Teil:** „Wir bauen **Ihre Website.**“ → „Wir bauen **Ihren Online-Shop.**“ → „Wir bauen **Ihre Web-App.**“ → „Wir bauen **Ihre Automatisierung.**“ → zurück zu „Wir bauen **Ihre Website.**“
- Es wechselt der ganze Teil nach „Wir bauen“ (wegen Ihre/Ihren).
- **Ablauf:** Wechsel alle 2,4 s mit 400 ms Überblendung. Nach **einem** Durchlauf bleibt „Ihre Website.“ stehen.
- **Anhalten:** Während des Durchlaufs gibt es einen unauffälligen Pause-Knopf (`aria-label` „Wechselnde Überschrift anhalten“, Tippfläche mindestens 44 × 44 px; WCAG 2.2.2). Hover oder Fokus auf der Überschrift halten den Wechsel ebenfalls an.
- **Ohne Bewegung:** Bei `prefers-reduced-motion: reduce` steht fest „Wir bauen Ihre Website.“, ohne Knopf. Ist der Tab nicht sichtbar, pausiert der Wechsel.
- **Screenreader:** Im `h1` steht ein fester, visuell versteckter Satz: „Wir bauen Websites, Online-Shops, Web-Apps und Automatisierungen.“ Der wechselnde Teil ist `aria-hidden`.
- **Layout:** Der Wechsel darf nichts springen lassen. Die Varianten liegen übereinander, die Breite richtet sich nach der längsten.
- **Untertext:** „In unserem Kundenportal sehen Sie jederzeit, wie weit Ihr Projekt ist – mit Live-Vorschau Ihrer Website.“
- **Buttons:** „Projekt besprechen“ (→ `/kontakt`, Hauptbutton) und „Kundenportal ansehen“ (→ `#kundenportal`, zweitrangig gestaltet).
**Karte (rechts):**
- **Datei:** `docs/relaunch/assets/deutschland-umriss.svg` (ein Pfad, nur Linie, `stroke="currentColor"`, viewBox `0 0 663 892`). Als Inline-SVG-Komponente einbauen.
- **Roter Punkt Falkenstein:** `cx = 469.6`, `cy = 520.5`. Ist schon in der Datei und geprüft: liegt im Südwesten Sachsens, 14 km von der tschechischen Grenze.
- **Beschriftung am Punkt:** „Falkenstein, Sachsen“, darunter klein „Hier laufen Ihre Website und das Kundenportal.“ Mindestgröße 14 px.
- **[Optional, O8]** zweiter, hohler Punkt für das Büro in Winnweiler (ca. 49,567 N, 7,850 O → `cx ≈ 146.7`, `cy ≈ 622.2`, Formel in der README). Falls ja, steht „Hier sitzen wir“ dort, und der Ort wird im Abschnitt Über uns nicht noch einmal genannt.
- **Farben:** Linie `var(--line-strong)`, Punkt `var(--accent)`.
- **Bewegung beim ersten Erscheinen:**
- Der Umriss zeichnet sich in 1,6 s (`stroke-dashoffset`; dafür `pathLength="1"` setzen und `vector-effect="non-scaling-stroke"` entfernen, die Strichstärke dann in der viewBox angeben).
- Danach erscheint der Punkt, dann pulsiert ein Ring um ihn dreimal (Skalierung 1 → 2,6, je 2,4 s, Deckkraft abnehmend) und bleibt dann stehen.
- Bei reduzierter Bewegung ist alles sofort sichtbar, ohne Ring.
- **Barrierefreiheit:** Die Karte ist Dekoration (`aria-hidden="true"`). Die Aussage steht als Text daneben.
**Layout:**
- **Desktop:** Zweispaltig, Text links (etwa 55 %), Karte rechts mit etwa 80 svh Höhe. Bei 1280 × 800 ist der ganze Hero ohne Scrollen sichtbar.
- **Handy:** Text oben, Karte darunter (40–46 svh hoch, mittig). Die Beschriftung am Punkt bleibt lesbar.
- **Hintergrund:** ruhige Fläche (`--bg`), kein WebGL-, Video- oder Effekthintergrund.
### 7.2 Kundenportal (`#kundenportal`)
**Text:**
- **Überschrift:** „Ihr Projekt. Live im Kundenportal.“
- **Einleitung:** „Sie sehen, was wir zuletzt erledigt haben, öffnen die Live-Vorschau Ihrer Website und schreiben uns im Chat – mit Fotos, Dateien oder Sprachnachricht. Ihre Rechnungen liegen ebenfalls dort, und aufs Handy installieren Sie das Portal wie eine App.“
- Die Installierbarkeit ist geprüft: Das Portal hat ein Web-App-Manifest und einen Service Worker und ist auf dem Handy als App installierbar.
**Funktionen in dieser Reihenfolge** (Bilder in `docs/relaunch/assets/portal/`, Dateiendung `.webp`):
| # | Titel | Bildunterschrift | Desktop-Bild | Handy-Bild |
|---|---|---|---|---|
| 1 | Live-Vorschau | „Ihre Website, noch während wir daran bauen.“ | `desktop-1-projekte` | `phone-1-projekte` |
| 2 | Fortschritt | „Was wir zuletzt erledigt haben – in einfachen Sätzen.“ | `desktop-2-fortschritt` | `phone-3-projektdetails` |
| 3 | Chat | „Frage, Foto oder Sprachnachricht direkt ans Team.“ | `desktop-3b-chat-foto` | `phone-4b-chat-foto` |
| 4 | Rechnungen | „Ansehen, bezahlen, herunterladen.“ | `desktop-4-rechnungen` | `phone-5-rechnungen` |
| 5 | Funktionen | „Bausteine zum Dazubuchen, zum Beispiel eine News-Pinnwand für Ihre Website.“ | `desktop-7-news-pinnwand` | `phone-8-news-vorschau` |
| 6 | Als App | „Aufs Handy installieren, fertig.“ | – (siehe unten) | `phone-2-projekte-lang` |
- **[Nur wenn O9 = ja]:** Zeile 4 heißt dann „Rechnungen und Verträge“ und bekommt zusätzlich `phone-6-vertraege`. Eine weitere Zeile „Stundenkonto“ mit `desktop-5-stundenkonto` kommt hinzu, weil diese Ansichten im Portal noch nicht für alle Kunden freigeschaltet sind.
- **Nicht bewerben:** „Terminplaner“ und „Analyse“. Sie sind im Portal nur Platzhalter („kommt bald“).
**Desktop (ab 900 px):**
- Eine festgepinnte Bühne (`position: sticky`, Scrollstrecke etwa 3–4 Bildschirmhöhen).
- **Links** steht die Liste der Funktionen. Die aktive Funktion ist hervorgehoben, ein roter Punkt dient als Marker.
- **Rechts:**
- Ein Desktop-Fenster: 1 px Linie, 12–14 px Rundung, oben eine schlichte Leiste mit der Adresse **project.webklar.com**, keine Ampelpunkte.
- Unten rechts überlappt ein Handy-Rahmen das Fenster, etwa 30 % der Breite.
- Die Bilder wechseln per Überblendung, sobald die nächste Funktion zur Hälfte im Blick ist (`IntersectionObserver`).
- **Bei Schritt 6** blendet das Desktop-Fenster aus. Der Handy-Rahmen rückt in die Mitte und wird größer. Darin schiebt sich der lange Screenshot `phone-2-projekte-lang` mit dem Seiten-Scroll nach oben. Das ist `transform: translateY`, gekoppelt an den Scrollfortschritt, kein eigener Scrollbereich.
**Handy (unter 900 px):**
- Keine festgepinnte Bühne.
- Stattdessen eine **Wisch-Reihe** (9.4) mit je einem Handy-Screenshot im Rahmen und der Bildunterschrift darunter.
- Die letzte Karte „Als App“ zeigt `phone-2-projekte-lang`. Es schiebt sich langsam nach oben, solange die Karte sichtbar ist.
- Bei O9 = ja höchstens 9 Karten (Stand mit „Öffnungszeiten live“: die Funktions-Reihe hat neun Stationen).
**Schrittzeile unter dem Portal** (ersetzt „So arbeiten wir“):
- Text: „**1** Kennenlernen · **2** Angebot mit Preis · **3** Bauen mit Vorschau und Ihrer Freigabe · **4** Online – Hosting und Pflege auf Wunsch im Abo“
- **Desktop:** eine Zeile mit vier Punkten.
- **Handy:** vier kleine Karten in einer Wisch-Reihe (Wunsch 2).
**Rahmen und Bildinhalt:**
- Es gibt **einen** Rahmenstil für alle Bilder. Er muss auf hellem und dunklem Grund funktionieren. Das Portal selbst ist dunkel und violett; das ist Bildinhalt, nicht Seitenfarbe.
- Die Screenshots zeigen nur die erfundene „Bäckerei Sonnenschein“. Nie echte Kundendaten verwenden, auch nicht verpixelt.
### 7.3 Was wir bauen (`#leistungen`)
**Text:**
- Überschrift: „Was wir für Sie bauen.“
- Einleitung:
- **Standard:** „Die meisten Betriebe starten mit einer Website. Wenn mehr nötig ist, bauen wir mehr.“
- **[Bei O2 = ja]:** „Die meisten Betriebe starten mit einer Website: Startseite und bis zu drei Unterseiten, mit Texten, Design und Anpassung für Handy und Computer. Wenn mehr nötig ist, bauen wir mehr.“
**Vier nummerierte Einträge**, als **Textzeilen statt Karten mit Symbolen**:
1. **Website**: „Startseite, Unterseiten, Impressum und Datenschutz – gestaltet, getextet und online.“
2. **Online-Shop**: „Warenkorb, Kasse, Zahlung und Versand. Auf Wunsch angebunden an Ihre Warenwirtschaft.“
3. **Web-App**: „Eigene Software für Ihren Betrieb mit Login, Verwaltung und Auswertungen, im Browser und auf dem Handy.“
4. **Automatisierung**: „Was sich ständig wiederholt, erledigt der Rechner: Bestätigungen, Rechnungen, Dokumente auslesen, ein KI-Assistent mit Ihrem Firmenwissen.“
**Schlusszeile:** „Sie haben schon eine Website? Wir ziehen sie um – mit Domain, Inhalten und Postfächern.“
**Mit Texten spielen:**
- Die Einleitung hellt sich beim Scrollen Wort für Wort auf (gedimmt 0,18 → voll), höchstens 30 Wörter.
- Nur wenn `animation-timeline` unterstützt wird und keine reduzierte Bewegung gewünscht ist. Sonst steht der Text normal da.
**Layout:**
- **Desktop:** große Überschrift links, nummerierte Liste rechts, viel Weißraum.
- **Handy:** die vier Einträge als **Wisch-Reihe** (9.4). Jede Karte ist 80 vw breit, die nächste ist angeschnitten sichtbar, dazu ein Zähler „1 / 4“.
### 7.4 Projekte (`#projekte`)
- **Solange keine Referenz freigegeben ist, entfällt der Abschnitt komplett.** Ist das Daten-Array leer, werden Abschnitt und Menüpunkt nicht gerendert.
- **Sobald Freigaben vorliegen:**
- Überschrift: „Echte Websites. Echte Betriebe.“
- Untertext: „Nur Projekte, die online sind und die wir zeigen dürfen.“
- Je Projekt ein **eigener** Screenshot (kein Stockfoto), ein echter Link und eine Zeile Beschreibung.
- Desktop mit großen Bildern, Handy als Wisch-Reihe, höchstens 5 Projekte.
### 7.5 Preise (`#preise`)
**Text:**
- Überschrift: „Was es kostet. Vorher.“
- Einleitung:
- **Standard:** „Für Ihr Projekt bekommen Sie vorher ein Angebot mit Preis. Für den Betrieb danach gibt es zwei Abos. Alle Preise sind Endpreise; als Kleinunternehmer (§ 19 UStG) berechnen wir keine Umsatzsteuer.“
- **[Bei O1 = ja]:** Der erste Satz wird ersetzt durch den freigegebenen Preisanker, z. B. „Eine Website gibt es ab … €.“
**Zwei Blöcke** (Desktop nebeneinander, Handy untereinander). Wortlaut aus den Abo-Seiten:
- **Hosting-Abo, 20 € im Monat:** „Für statische Websites: Hosting mit SSL-Zertifikat, täglicher Datensicherung und Überwachung der Erreichbarkeit, eine Domain und ein E-Mail-Postfach mit 5 GB. 12 Monate Mindestlaufzeit.“ → Link „Details zum Hosting-Abo“ (`/hosting-abo`)
- **Wartungs-Abo, 50 € im Monat:** „Für alle Websites und Web-Anwendungen, auch mit Login, Shop oder Datenbank: zusätzlich Sicherheitsupdates für die Software Ihrer Website, beliebig viele Postfächer mit zusammen 5 GB und eine Stunde Arbeitszeit pro Monat. 12 Monate Mindestlaufzeit.“ → Link „Details zum Wartungs-Abo“ (`/wartungs-abo`)
**Zeile darunter:** „Weitere Arbeit nur mit Ihrer Freigabe: 100 € pro Stunde, abgerechnet in 15-Minuten-Schritten.“ Das gilt für beide Abos, so steht es auf beiden Abo-Seiten.
**Eine Quelle für alle Angaben:**
- Preise, Laufzeiten und Leistungen müssen mit den Seiten `/hosting-abo` und `/wartungs-abo` übereinstimmen.
- Die Werte liegen in **einem** Datenmodul `src/data/abos.ts`, das die Startseite nutzt.
- Die Abo-Seiten selbst werden **nicht** auf das Modul umgebaut (D13). Stattdessen prüft ein Test, dass Preis, Laufzeit und Speicherangaben auf Startseite und Abo-Seiten gleich sind.
**Darstellung:** Preise zählen nicht hoch. Zahlen mit `tabular-nums`.
### 7.6 Über uns (`#ueber-uns`)
**Text:**
- Überschrift: „Wer hinter WEBklar steckt.“
- Text:
- **Standard:** „Wir sind ein kleines Team aus Winnweiler in der Pfalz. Sie sprechen bei uns direkt mit den Leuten, die Ihre Website bauen.“
- **[Bei O4 = ja]:** Namen und Rollen kommen dazu, z. B. „Justin kümmert sich um Websites, Angebote und Betreuung, Kenso um Shops, Software und Automatisierung.“
- **[Bei Freigabe]:** „Erreichbar Montag bis Freitag von 9 bis 17 Uhr.“
**Fotos:**
- Nur echte Fotos des Teams, die der Inhaber liefert.
- **Bis dahin keine Fotos**: keine Platzhalter-Gesichter, keine KI-Bilder, keine Initialen-Kreise.
**Link:** „Mehr über uns“ → `/ueber-uns`.
### 7.7 Kontakt (`#kontakt`)
- **Überschrift:** „Erzählen Sie uns, was Sie vorhaben.“
- **Text:** „Zwei Sätze genügen. Wir melden uns spätestens am nächsten Werktag. Im ersten Gespräch klären wir in 15 Minuten, ob und wie wir helfen können – kostenlos und unverbindlich.“
- **Button:** „Projekt besprechen“ (→ `/kontakt`), darunter E-Mail und Telefon.
- **Gestaltung:** Bühne mit viel Ruhe, kein Effektrahmen.
### 7.8 Footer
- **Links im Footer:** Logo, „WEBklar GbR“ und die Anschrift (Pflichtangabe, aus dem Impressum).
- **Spalte Seiten:**
- Startseite → `/`
- Kundenportal → `/#kundenportal`
- Was wir bauen → `/#leistungen`
- Preise → `/#preise`
- Über uns → `/ueber-uns`
- Kontakt → `/kontakt`
- Wartungs-Abo → `/wartungs-abo`
- Hosting-Abo → `/hosting-abo`
- **Spalte Rechtliches:**
- AGB, Impressum, Datenschutz
- „Verträge hier kündigen“ und „Vertrag widerrufen“: Beide bleiben hervorgehoben wie heute, weil sie rechtlich vorgeschrieben sind. Die Ziele bleiben wie im heutigen `Footer.tsx`.
- **Gestaltung:** Passt sich hellem und dunklem Modus an, also kein harter schwarzer Block im hellen Modus. Kein animiertes Raster.
---
## 8. Gestaltung
### 8.1 Schrift
- **Eine** Schriftfamilie, **zwei** Gewichte (400 und 600). Inter, Space Grotesk und Poppins entfallen.
- Im Entwurf gibt es zwei Varianten zum Vergleich:
- **A – Systemschrift:** `-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif`. Auf Apple-Geräten ist das echtes SF Pro, ohne Lizenz- oder Ladefrage.
- **B – selbst gehostete Variable-Schrift unter SIL Open Font License**, z. B. „Instrument Sans“, als `woff2` in `public/fonts/`. Nichts von Google Fonts oder anderen fremden Servern laden (Datenschutz).
- Vor der Entscheidung an echten deutschen Überschriften mit Umlauten und ß prüfen.
- **Keine Versalien-Überschriften** mehr, sondern normale Satzschreibung. Überschriften mit `text-wrap: balance`, Absätze mit `text-wrap: pretty`, dazu `hyphens: auto` mit `lang="de"`.
**Größen (Token):**
```css
:root {
--fs-hero: clamp(2.5rem, 1.569rem + 3.81vw, 5rem); /* 40 → 80 px */
--fs-h2: clamp(2rem, 1.444rem + 2.29vw, 3.5rem); /* 32 → 56 px */
--fs-lead: clamp(1.1875rem, 1.05rem + 0.6vw, 1.625rem); /* 19 → 26 px */
--fs-body: 1.0625rem; /* 17 px, Zeilenhöhe 1.47 */
--fs-small: 0.875rem; /* 14 px, kleinste Textgröße überhaupt */
--measure-text: 46rem;
--container: 1100px;
}
h1, h2 { font-weight: 600; line-height: 1.05; letter-spacing: -0.015em; }
```
- Abstand von der Überschrift zum ersten Absatz: 1,2 em.
- Fließtext und Formularfelder mindestens 16 px, sonst zoomt iOS.
### 8.2 Farben
**Grundsatz:**
- Neutral, also nahezu Schwarz und Weiß mit wenigen Grautönen, plus **eine** Akzentfarbe: **Rot** wie der Kartenpunkt.
- Das cyan-blaue Primär und das Türkis im dunklen Modus entfallen.
- Rot nur für den Kartenpunkt, aktive Zustände und Marker. Der Hauptbutton ist neutral: dunkel auf hell, hell auf dunkel.
**Startwerte** (geprüfte Kontraste, Feinabstimmung im Entwurf, O7):
| Token | Hell | Dunkel | Zweck |
|---|---|---|---|
| `--bg` | `#ffffff` | `#0b0b0c` | Seitenfläche |
| `--bg-alt` | `#f5f5f7` | `#161618` | jeder zweite Abschnitt |
| `--fg` | `#1d1d1f` | `#f5f5f7` | Text (≥ 15 : 1) |
| `--fg-muted` | `#6e6e73` | `#a1a1a6` | Nebentext (hell ≥ 4,66 : 1 auch auf `--bg-alt`, dunkel ≥ 7 : 1) |
| `--line` | `#d2d2d7` | `#2c2c2e` | Trennlinien, Rahmen |
| `--line-strong` | `#86868b` | `#636366` | Kartenumriss |
| `--accent` | `#e5251c` | `#e5251c` | Punkt und Marker (Nicht-Text, ≥ 3 : 1 auf allen Flächen) |
| `--accent-text` | `#c81e16` | `#ff5a4f` | Rot als Text oder Link (hell ≥ 5,28 : 1, dunkel ≥ 5,88 : 1, auch auf `--bg-alt`) |
| `--focus` | `#c81e16` | `#ff5a4f` | Fokusrahmen |
- Alle Kombinationen prüft ein Test auf Kontrast. Heute geschieht das schon so in `src/test/sofort-paket.test.tsx`, über die Token in `index.css`.
- Das Logo bleibt (`src/components/Logo.tsx`), Schreibweise „WEBklar“.
### 8.3 Abstände und Flächen
- **Abstände:** zwischen Abschnitten 120–160 px auf dem Desktop und 72–96 px auf dem Handy. Container 1100 px breit, Textspalte 46 rem.
- **Flächen:** Flächenwechsel statt Rahmen. Abschnitte stehen abwechselnd auf `--bg` und `--bg-alt`, der Text steht frei auf der Fläche.
- **Formen:** Rundungen und Rahmen nur um Bilder und Bildschirme.
### 8.4 Bewegung
**Grundregeln:**
- Eine Kurve: `cubic-bezier(.2, .8, .2, 1)`. Zwei Dauern: 200 ms für Zustände, 600 ms für das Erscheinen. Versatz höchstens 60 ms. Kein Federn.
- **Erscheinen beim Scrollen** per CSS `animation-timeline: view()` innerhalb von `@supports`. Der Grundzustand ist **sichtbar**: Ohne Unterstützung steht alles einfach da.
- **Reduzierte Bewegung:** Alles läuft hinter `@media (prefers-reduced-motion: no-preference)`. Bei `reduce` gibt es keine Bewegung außer Farbwechseln.
- **Animiert werden** nur `transform` und `opacity`.
- **Nicht erlaubt:** Scroll-Hijacking und `scroll-snap` auf der ganzen Seite.
- **Sticky:** Vorfahren von `position: sticky` nutzen `overflow: clip` statt `hidden`.
- **Hover:** Hover-Effekte nur in `@media (hover: hover) and (pointer: fine)`.
**Ausdrücklich erlaubte Ausnahmen**, auch nur ohne reduzierte Bewegung:
| Element | Abweichung |
|---|---|
| Kartenumriss (7.1) | `stroke-dashoffset` über 1,6 s |
| Ring um den Punkt (7.1) | 3 × 2,4 s, dann Stillstand |
| Wechselnde Überschrift (7.1) | 400 ms Überblendung alle 2,4 s, ein Durchlauf |
| Wort-für-Wort-Aufhellen (7.3) | an den Scrollfortschritt gekoppelt |
### 8.5 Regeln gegen den Vorlagen-Look
1. **Schrift:** eine Familie, zwei Gewichte.
2. **Farbe:** eine Akzentfarbe (Rot), und zwar mit Bedeutung.
3. **Hintergründe:** keine Effekte, also kein WebGL, keine Glüh-Ränder, kein Pixelraster, kein Glas.
4. **Layout:** kein Raster aus drei Karten mit Kreis-Symbolen. Stattdessen nummerierte Textzeilen, eine große Aussage plus Liste oder asymmetrische Layouts.
5. **Texte, die nur WEBklar sagen kann:** Falkenstein, Kundenportal, Winnweiler. Verboten sind „maßgeschneidert“, „ganzheitlich“, „innovativ“, „nahtlos“, „auf das nächste Level“, „Hebel“, „Tool-Inseln“, „Pipeline“ und „Potenzialanalyse“.
6. **Zahlen:** keine Kennzahlen ohne Beleg, keine hochzählenden Zahlen.
7. **Bewegung:** nur mit Aufgabe (8.4).
8. **Flächen:** wenige Kästen, viel Fläche.
9. **Bilder:** nur echtes Bildmaterial, also Portal-Screenshots mit Demodaten, echte Teamfotos und echte Projekte.
10. **Austausch-Test:** siehe E7.
---
## 9. Technik
### 9.1 Bleibt
- **Grundgerüst:** Vite, React, TypeScript, Tailwind und React Router mit allen bestehenden Routen.
- **Layout:** `src/routes.tsx` mit dem gemeinsamen Unterseiten-Layout `SubpageLayout`. Es wird zum neuen Layout mit Kopfleiste und Footer.
- **Seiten-Helfer:** `usePageMeta` setzt Titel und Beschreibung je Route. `ScrollToTop` springt zu Ankern.
- **Kontaktformular:** Die Logik in `src/pages/Contact.tsx` und `src/lib/appwrite.ts` bleibt, mit dem Datenschutzhinweis.
- **Rechtsseiten:** Ihre Texte und Tests bleiben: `src/test/abos.test.tsx`, `agb-haftung.test.tsx`, `agb-verguetung.test.tsx`.
- **Hell/Dunkel:** `next-themes` mit der Klassen-Strategie und dem Standard „system“, dazu das Skript in `index.html` gegen Aufblitzen.
### 9.2 Fällt weg (Phase 2)
Vor jedem Löschen prüfen, dass nichts mehr die Datei importiert.
- **Effekt-Komponenten und ihre Pakete:**
- Komponenten: `Silk`, `PixelBlast`, `LightRays`, `BorderGlow`, `FlickeringGrid`, `TextHoverEffect`, `CountUp`.
- Pakete: `three`, `@react-three/fiber`, `postprocessing`, `ogl`.
- **Alte Startseiten-Abschnitte:**
- `Partners` (das Laufband) mit `public/svg/*`.
- `ProblemSection`, `SolutionSection`, `Values`, `Services`, `Process`.
- Die Hintergrundbilder `problem*.jpg`, `loesung.jpg`, `backgroud_effect*`.
- **Alte Schriften:** `src/fonts.css` und `public/fonts/inter-*`, `space-grotesk-*`. Sie werden durch die gewählte Schrift ersetzt.
- **Alte Buttons:** das CSS für die Buchstaben-Animation.
- **Analytics:** `AnalyticsProvider`, `src/lib/analytics.ts`, die `data-analytics-*`-Attribute und `gtag`. Heute ist das inaktiv; es würde aber Google laden, und der Relaunch nutzt kein Tracking (Abschnitt 13).
- **Ungenutzte shadcn-Komponenten und Pakete:** laut Prüfung rund 45 tote Dateien und 29 Pakete, die nur von toten Dateien genutzt werden.
- **Tests:**
- Tests, die nur gelöschte Komponenten prüfen, werden entfernt oder auf die neuen Abschnitte umgeschrieben.
- Die Quelltext-Prüfungen gegen verbotene Aussagen bleiben in jedem Fall: alte Telefonnummer, Unsplash, „24 Stunden“, „99,9“, Erfolgsversprechen.
### 9.3 Neu
- **Startseite:** `src/pages/Index.tsx` mit den Abschnitten aus 7, je Abschnitt eine Komponente in `src/components/home/`:
- `HeroKarte.tsx`
- `Kundenportal.tsx`
- `Leistungen.tsx`
- `Preise.tsx`
- `UeberUns.tsx`
- `KontaktBuehne.tsx`
- **`src/components/DeutschlandKarte.tsx`:** Inline-SVG aus dem Asset. Die Punkte kommen als Props, dazu die Formel aus der README.
- **`src/components/WischReihe.tsx`:** die wiederverwendbare horizontale Reihe (9.4).
- **`src/components/GeraeteRahmen.tsx`:** Desktop-Fenster mit Adressleiste und Handy-Rahmen.
- **`src/data/abos.ts`:** Preise und Leistungen für die Startseite (7.5).
- **Design-Token:** neu in `src/index.css` und `tailwind.config.ts` (8.1, 8.2).
### 9.4 Wisch-Reihe (unter 900 px)
- **Technik:** natives CSS ohne Bibliothek.
- Reihe: `display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scroll-padding-inline: 16px; overscroll-behavior-x: contain;`
- Karten: `flex: 0 0 80vw; max-width: 380px; scroll-snap-align: start`.
- **Wischen erkennbar machen:**
- Die nächste Karte ist angeschnitten sichtbar. Das ist der wichtigste Hinweis.
- Dazu ein Zähler „2 / 4“. Die aktive Karte bestimmt ein `IntersectionObserver` (mindestens 60 % sichtbar).
- **Barrierefreiheit:**
- Liste mit `role="list"`.
- Bereich mit `tabindex="0"` und `aria-label`, z. B. „Vier Leistungen, seitlich wischen“.
- Die Fokusreihenfolge entspricht der Anzeige.
- Kein `aria-live`, keine automatische Bewegung.
- **Ab 900 px** zeigt jeder Abschnitt seine Desktop-Darstellung (Liste, Raster oder Bühne). Auf dem Desktop bleibt keine Wisch-Reihe übrig.
- **Grenze:** Höchstens 9 Karten je Reihe (Stand mit „Öffnungszeiten live“; vorher 7).
### 9.5 Bilder
- **Format und Auslieferung:**
- WebP mit Qualität 80–85.
- Über `srcset` ausliefern: Desktop 800, 1200 und 1600 w, Handy 390 und 780 w.
- `loading="lazy"` (außer Bildern im ersten Bildschirm), `decoding="async"`, feste `width`/`height`.
- **Budgets:**
- Ein einzelnes Desktop-Bild höchstens 120 KB, ein einzelnes Handy-Bild höchstens 60 KB.
- Was beim ersten Erreichen des Portal-Abschnitts sichtbar wird, höchstens 300 KB.
- Alle Bilder des Abschnitts für eine Gerätegröße zusammen höchstens 900 KB.
- **Rohdateien:** Die Dateien in `docs/relaunch/assets/portal/` sind Rohexporte in doppelter Auflösung und müssen verkleinert werden. Das geht einmalig vorab oder per Build-Skript mit `sharp`. In `public/` landen nur die verkleinerten Fassungen.
- **Alt-Texte:** jedes Bild bekommt einen, z. B. „Kundenportal: Projektübersicht mit Live-Vorschau der Website“.
### 9.6 Barrierefreiheit und Handy
- **Struktur:** genau ein `h1` je Seite, jeder Abschnitt ein `h2`, dazu die Landmarken `header`, `nav`, `main`, `footer`.
- **Bedienung:** sichtbarer Fokus (`--focus`), die Tastatur erreicht alles, Tippflächen mindestens 44 × 44 px.
- **Formulare:** mit `autocomplete` (name, email, organization) und `aria-live` für Fehler und Erfolg.
- **Handy-Checkliste** (aus der Prüfung):
- kein seitlicher Überlauf bei 360 und 390 px;
- `svh`/`dvh` statt `vh`;
- Safe-Area-Ränder (`env(safe-area-inset-*)`) bei festen Elementen;
- Menü mit Scrollsperre (6.1);
- Anker landen unter der Kopfleiste;
- Text mindestens 14 px, Fließtext 16–17 px;
- nichts ist nur per Hover erreichbar;
- Querformat 844 × 390 hat Inhalt und Buttons ohne erzwungene Höhe.
### 9.7 Leistung
- **Budgets:** E5, dazu CSS höchstens 15 KB gzip.
- **Routen per `React.lazy`:** Die Rechtsseiten müssen nicht im Startpaket sein. Die Render-Tests in `src/test/` fragen dann asynchron ab (`await screen.findBy…`).
- **Appwrite-SDK:** erst beim Absenden des Formulars laden (`import()`).
- **Keine fremden Server:** Schriften, Bilder und Skripte selbst hosten.
### 9.8 Tests und Prüfung
- **`npm test`** (Vitest und Testing Library) prüft:
- Abschnitte rendern mit `h2`, und die Anker passen zur Navigation.
- Wisch-Reihen haben `tabindex` und `aria-label`.
- Preise auf der Startseite stimmen mit den Abo-Seiten überein.
- Verbotene Aussagen und Wörter kommen nicht vor (8.5 Punkt 5).
- Die Rechtsseiten-Tests laufen unverändert grün.
- Die Kontrast-Token beider Modi erreichen mindestens 4,5 : 1.
- **Anpassung bestehender Tests in Phase 1:**
- In `src/test/sofort-paket.test.tsx` dürfen die Stellen geändert werden, die alte Texte oder Strukturen festschreiben: Meta-Beschreibung mit „Potenzialanalyse“, Navigationspunkte „Leistungen“ und „Ablauf“, Texte auf `/kontakt` und `/ueber-uns` sowie Tests gelöschter Komponenten.
- Nicht entfernt werden die Quelltext-Prüfungen gegen verbotene Aussagen.
- Die drei Rechtstext-Testdateien bleiben unverändert.
- **Browserprüfung** (Playwright) bei 390 × 844 und 1440 × 900, hell und dunkel:
- kein seitlicher Überlauf, keine Konsolenfehler;
- Seitenlänge auf dem Handy höchstens 8 Bildschirme;
- Karte und Punkt sind sichtbar, Wisch-Reihen lassen sich wischen;
- bei `prefers-reduced-motion: reduce` gibt es keine Bewegung;
- Screenshots für den Inhaber.
- **Vor jedem Push:** `npm test`, `npm run build` und `npm run lint` (keine **neuen** Fehler gegenüber `main`).
---
## 10. Rechtliche Leitplanken
- **Rechtstexte:** Die Texte von AGB, Impressum, Abo-Seiten und Datenschutzerklärung nicht ohne ausdrückliche Freigabe des Inhabers ändern (D13). Die Tests sichern das ab.
- **Inhalte:**
- keine Erfolgsversprechen und keine unbelegten Zahlen;
- keine Referenzen ohne Zustimmung;
- keine Stockfotos und keine KI-Bilder von Personen.
- **Preise:** als Endpreise, mit Hinweis auf § 19 UStG.
- **Server:** Die Aussage zum Serverstandort gilt nur für Website und Kundenportal (D7).
- **Kontaktformular:** Der Datenschutzhinweis mit Link bleibt direkt am Formular.
- **Keine Drittanbieter-Anfragen,** damit die Datenschutzerklärung stimmt.
- **Portal-Screenshots:** nur mit Demodaten.
- **Footer:** „Verträge hier kündigen“ und „Vertrag widerrufen“ bleiben auf jeder Seite erreichbar.
---
## 11. Offene Entscheidungen (Inhaber)
| Nr. | Frage | Bis zur Entscheidung gilt |
|---|---|---|
| O1 | Preisanker für Websites veröffentlichen? Welcher Betrag? | Standardtext in 7.5 (Angebot mit Preis, ohne Betrag) |
| O2 | Leistungsumfang „Startseite und bis zu drei Unterseiten“ als Einstieg nennen? | Standardtext in 7.3 |
| O3 | Zeitrahmen veröffentlichen (z. B. erste Vorschau nach wenigen Tagen)? | nicht nennen |
| O4 | Team: Wer wird mit Namen, Rolle und Foto gezeigt? | Standardtext in 7.6, keine Fotos |
| O5 | Welche Kunden stimmen einer Referenz zu? | kein Projekte-Abschnitt |
| O6 | Werden Eingänge über das Kontaktformular zuverlässig gemeldet? Nur dann ist „spätestens am nächsten Werktag“ haltbar | Formulierung bleibt; vor dem Livegang klären |
| O7 | Schrift A oder B, genauer Rotton | im Entwurf beide Schriften zeigen, Startwerte aus 8.2 |
| O8 | Zweiter Kartenpunkt für Winnweiler („Hier sitzen wir“)? | nur Falkenstein |
| O9 | Stundenkonto und Verträge im Portal-Abschnitt zeigen? Hängt an der Freischaltung im Portal | nicht zeigen |
| O10 | Wie sieht der Inhaber den Entwurf? | **GitHub Pages** über einen Actions-Workflow für den Zweig `relaunch` (siehe unten). Geht das nicht (z. B. privates Repo ohne Pages), Screenshots im Pull Request |
| O11 | Repository öffentlich oder privat? | nichts Internes einchecken: keine Preiskalkulation, keine Kundennamen, keine Serverdetails |
| O12 | Erreichbarkeit „Montag bis Freitag 9 bis 17 Uhr“ auf der Startseite nennen? | nicht nennen |
**Vorschau über GitHub Pages (O10):**
- Der Build bekommt den Unterpfad über `VITE_BASE` (`vite.config.ts`: `base`) und `BrowserRouter basename={import.meta.env.BASE_URL}`.
- Damit Unterseiten direkt aufrufbar sind, wird `dist/index.html` im Workflow nach `dist/404.html` kopiert.
- Das Kontaktformular sendet in der Vorschau nichts (ohne Appwrite-Variablen zeigt es eine Fehlermeldung, das ist in Ordnung).
- Schriftvarianten sind über `?schrift=a` bzw. `?schrift=b` erreichbar.
---
## 12. Arbeitsweise und Livegang
- **Zweige:** Gearbeitet wird auf Zweigen in diesem Repository, zusammengeführt per Pull Request nach `main` dieses Repos. **`main` hier ist nicht live.**
- **Pull Requests** enthalten Screenshots der geänderten Abschnitte: Desktop und Handy, hell und dunkel.
- **Geheimnisse:** Keine einchecken. `.env` bleibt lokal; `.env.example` enthält nur öffentliche Client-IDs.
- **Livegang:**
- Nach Freigabe des Inhabers wird der Stand ins interne Live-Repository übertragen, danach werden alle Routen live geprüft.
- Die Übertragung macht eine Sitzung auf dem Server oder der Inhaber; sie gehört nicht zur Cloud-Arbeit.
- Bei der Übertragung dürfen Dateien, die es nur im Live-Repository gibt (Server-Doku, Vorschau-Konfiguration), nicht gelöscht werden.
---
## 13. Nicht im Umfang
- Blog, CMS, Mehrsprachigkeit.
- Analytics, Tracking und Cookie-Banner.
- Vorab-Rendering (SSR/SSG), `sitemap.xml`, strukturierte Daten und echte 404-Statuscodes. Das ist ein eigenes Folgeprojekt; der Code soll dafür aber nichts verbauen.
- Änderungen am Kundenportal selbst, z. B. eine einheitliche Anrede oder der Mehrwertsteuer-Hinweis bei Funktionen.
- Änderungen an Server, Deploy und Kontakt-Backend.
---
## 14. Abnahme
**Phase 1 (Entwurf) ist fertig, wenn:**
- auf dem Zweig `relaunch` laufen:
- die neue Kopfleiste,
- alle Abschnitte aus 6.2 außer „Projekte“,
- der neue Footer;
- der Hero mit wechselnder Überschrift, gezeichneter Karte und Pause-Knopf funktioniert, ebenso die Wisch-Reihen auf dem Handy und die Portal-Bühne auf dem Desktop;
- beide Schriftvarianten umschaltbar sind;
- die Vorschau (O10) erreichbar ist und Screenshots vorliegen: 1440 × 900 und 390 × 844, hell und dunkel, je Schriftvariante;
- `npm test` und `npm run build` grün sind.
**Phase 2 ist fertig, wenn:**
- die Erfolgskriterien E2–E6 gemessen erfüllt sind;
- der Inhaber E1 und E7 bestätigt hat;
- alle Unterseiten das neue Layout haben und die Texte der Rechtsseiten unverändert sind;
- der Schriftumschalter entfernt ist und die gewählte Schrift fest eingebaut ist;
- Effekt-Komponenten, Analytics und tote Abhängigkeiten gelöscht sind;
- die Rückmeldungen des Inhabers eingearbeitet sind und er den Stand freigibt.
@@ -0,0 +1,140 @@
# Funktions-Demos im Abschnitt Kundenportal (2026-10-04)
Auftrag des Inhabers (2026-10-04, sinngemäß): In der Handy-Sektion (Kundenportal-Bühne) die eine Station
„Funktionen“ herausnehmen und stattdessen die einzelnen Funktionen zeigen, die WEBklar anbietet:
**Terminplaner**, **Website-Analyse (Heatmap)** und **News-Pinnwand**. Je Funktion eine erfundene, interessante
Kunden-Website, bei der die Funktion Sinn ergibt. Auf dem **Laptop die richtige Website groß**, auf dem **Handy
das Kundenportal** (die Einrichtung/Verwaltung). Die Heatmap soll „fancy“ wirken: simulierte Klicks, die Heatmap
baut sich auf; **beim Hovern** sieht man, wo viel geklickt wurde, **darunter eine kleine Diagnose**. Beim
Terminplaner soll ein **Kalender (Google Kalender)** mit den Einträgen im Bild sein. Bei der Pinnwand eine
Website wie eine Gemeinde/Stadtverwaltung – „irgendwas Interessantes“.
Dieses Dokument ergänzt `2026-10-01-relaunch-design.md` (Abschnitt 7.2 Kundenportal). Wo es abweicht, gilt es.
## 1. Stationen der Bühne (Reihenfolge)
1. Live-Vorschau · 2. Fortschritt · 3. Chat · 4. Rechnungen (unverändert)
5. **Terminplaner** · 6. **Website-Analyse** · 7. **News-Pinnwand** – Gruppe „Weitere Funktionen“ (Inhaber 2026-10-04: so nennen, Unterpunkte nur in Stichpunkten)
8. Als App (unverändert, bleibt die letzte Station mit dem langen Screenshot)
Texte (Sie-Form, keine Erfolgsversprechen, keine Zahlen ohne Quelle):
| Station | Titel | Stichpunkte |
|---|---|---|
| 5 | Terminplaner | Gäste buchen direkt auf Ihrer Website · Termine und Anfragen im Portal · Kalender-Abo, z. B. Google Kalender |
| 6 | Website-Analyse | Heatmap: wohin Besucher klicken · Aufrufe, Herkunft und Geräte · Im Wartungs-Abo enthalten |
| 7 | News-Pinnwand | Text und Foto, vom Handy aus · Hängt direkt auf Ihrer Website · Laufzeit je Zettel wählbar |
Bedienhinweis der Heatmap (nicht in den Stichpunkten, sondern im Hinweis unter dem MacBook bzw. in der Karte):
„Zeigen Sie mit der Maus auf die Seite.“ (Maus) / „Tippen Sie auf eine Stelle der Seite.“ (Touch).
Wahrheit der Aussagen (geprüft im Portal-Code und in den Spezifikationen):
- Terminplaner: Buchung im Widget, Gast bestätigt per Link in der E-Mail, Betrieb bekommt eine Mail
„Neue Buchung: {Leistung} am {Datum} um {Zeit}“ (K20), Termine stehen im Portal (Reiter Termine). Kalender:
**Kalender-Abo** je Person/Platz (geheimer .ics-Link), den man in Google Kalender, Apple Kalender oder Outlook
einfügt; Google aktualisiert nur alle paar Stunden. Es gibt **keinen** Zwei-Wege-Sync. Kalendereinträge für den
Betrieb heißen „{Leistung}: {Gastname}“.
- Website-Analyse: Heatmap der Klicks (Farbskala blau → grün → gelb → rot, wie `analytics/client/h.js`),
Seitenaufrufe, Besucher, Herkunft, Geräte, Kontakt-Klicks; nur mit Einwilligung; im Wartungs-Abo enthalten.
- News-Pinnwand: kurze Neuigkeiten mit Text und/oder Foto, „hängen wie Zettel an einer Pinnwand auf deiner Website“.
## 2. Erfundene Beispiel-Kunden
Alle drei sind erfunden und werden als Beispiel gekennzeichnet (kleiner Hinweis „Erfundenes Beispiel“ an der
Bühne). Adressen in der Safari-Leiste mit der reservierten Endung `.example` (RFC 2606), damit kein echter
Betrieb gemeint sein kann. Keine Fotos von Menschen, keine Stockfotos, keine fremden Logos: Bilder sind
selbst gezeichnete SVG/CSS-Grafiken. Keine Anfragen an fremde Server.
| Funktion | Beispiel | Adresse | Warum |
|---|---|---|---|
| Terminplaner | **Salon Schnittstelle** – Friseur, drei Personen (Mia, Can, Lea) | `salon-schnittstelle.example` | Klassischer Fall: Leistung, Person, Zeit |
| Website-Analyse | **Boulderhalle Überhang** – Kletterhalle | `boulderhalle-ueberhang.example` | Viele Klickziele (Tageskarte, Live-Auslastung, Kurse, Preise, Öffnungszeiten, Routenfoto) |
| News-Pinnwand | **Ortsgemeinde Musterbach** – Gemeinde-Website | `musterbach.example` | Ständig Neues: Kerwe, Straßensperrung, Bücherzelle, Sprechstunde |
## 3. Aufbau je Station
Ab 900 px (Bühne): **MacBook** = die Website als Live-Komponente (keine Bilddatei), gesetzt in 1512 x 893,
der Rahmen verkleinert sie (bei 1440 px Fensterbreite ca. Faktor 0,41). Darum große Schrift (Fließtext
mindestens 22 Designpunkte, Überschriften 64+), große Flächen, wenig Kleinkram. **iPhone** = Kundenportal-Ansicht
der Funktion als Screenshot aus dem echten Portal-Code mit erfundenen Daten (Fixture-Server), Format wie die
übrigen Handy-Bilder (804 x 1748 @2x, ausgeliefert 390/780 px webp). Darüber dürfen kleine Live-Ebenen liegen
(Mitteilung, Tipp-Welle), die mit dem Ablauf der Website synchron laufen.
Unter 900 px (Wisch-Reihe): je Funktion eine Karte; im iPhone-Rahmen läuft die **Website** in ihrer
Handy-Fassung (402 x 874, oben 59 / unten 34 frei für Statusleiste und Home-Balken). Die Heatmap-Karte
reagiert auf Tippen (Zone antippen ⇒ Diagnose unter der Karte).
### 3.1 Terminplaner (Salon Schnittstelle)
- Website: Kopf mit Name/Navigation, große Überschrift, rechts das **Buchungs-Widget** im Stil des echten
Widgets (`kundenbereich…/build/public/embed/termine.js`, `termine-demo.html`): Leistung wählen → Person
(„Egal“ möglich) → Tag und freie Zeit → Daten → „Termin buchen“ → Hinweis „Bitte bestätigen Sie den Termin
über den Link in Ihrer E-Mail.“ (der Gast bestätigt per Link).
- Kalender-Fenster über der Website (rechts oben/unten, im Stil einer Kalender-App, **ohne Google-Logo und ohne
Google-Farben/-Schrift als Marke**): Wochenansicht mit 6–9 Einträgen „{Leistung}: {Gastname}“, Kopfzeile
„Kalender-Abo · Salon Schnittstelle – Mia“, kleiner Hinweis „z. B. in Google Kalender“. Nach der Buchung
erscheint der neue Eintrag (weich eingeblendet).
- Ablauf (ca. 9–11 s, Schleife): Zeiger wählt Leistung → Person → Zeit → klickt „Termin buchen“ → Bestätigung
→ Eintrag erscheint im Kalender; auf dem iPhone gleitet eine Mitteilung herein „Neue Buchung: Damenhaarschnitt
am Do., 8. Okt. um 14:30“ (Absender WEBklar Termine). Ohne Bewegung: Endzustand (gebucht, Eintrag da).
- iPhone: Portal-Ansicht Terminplaner, Reiter „Termine“ (Liste der Woche mit Status, ein Eintrag „angefragt“).
### 3.2 Website-Analyse (Boulderhalle Überhang)
- Website: Navigation (Routen, Kurse, Preise, Öffnungszeiten), Held „Heute 14 neue Boulder“ mit großem Knopf
„Tageskarte kaufen“, Kachel **Live-Auslastung** (z. B. 42 %), Karten Schnupperkurs / Preise / Öffnungszeiten,
ein großes **Routenbild** (SVG-Wand mit bunten Griffen) – das ist kein Link, wird aber oft angeklickt
(„toter Klick“), Telefonnummer im Kopf. 8–10 Zonen mit `data-zone`.
- Heatmap: Leinwand über der Website, Zeichnung mit `heat.ts` (gleicher Algorithmus wie das echte Overlay).
Klickpunkte sind Beispieldaten aus einer festen Saat (`zufall`), je Zone ein Gewicht; Summe z. B. 1.284 Klicks.
- Ablauf (Schleife): Zeiger fliegt Zonen an, Klickwelle, die Heatmap wird mit jedem Klick dichter (je Klick ein
Schub Punkte), nach ca. 10 s ist sie voll, bleibt ca. 6 s stehen, blendet aus, neu. Ohne Bewegung: volle Heatmap.
- **Hover (nur Maus)**: Sobald die Maus über der Website ist, stoppt der simulierte Zeiger, die Heatmap ist voll
sichtbar; die Zone unter der Maus bekommt einen Rahmen und ein Schildchen „Tageskarte kaufen · 402 Klicks“.
**Diagnose darunter** (unter dem MacBook, links neben dem iPhone, normaler Seitenmaßstab, Seitenfarben hell/dunkel):
Kopf „Diagnose · letzte 30 Tage · Beispieldaten“, Rangliste der 4 meistgeklickten Zonen mit Balken und Prozent,
dazu ein Satz zur gewählten Zone (ohne Hover: zur meistgeklickten). Beispiel-Sätze: „Am meisten geklickt:
‚Tageskarte kaufen‘ – 31 % aller Klicks.“ / „Auffällig: 12 % klicken auf das Routenbild. Es ist kein Link –
hier lohnt sich ein Link zur Routenliste.“ Keine Erfolgsversprechen („mehr Umsatz“ o. Ä. verboten).
- Tastatur/Screenreader: die Ranglisten-Zeilen sind Knöpfe; Fokus/Klick wählt die Zone (Rahmen auf der Website).
Die Website selbst ist `aria-hidden` und enthält keine fokussierbaren Elemente.
- Handy-Karte: die Website in Handy-Fassung mit Heatmap; Tippen auf eine Zone zeigt die Diagnose unter der Karte.
- iPhone (Bühne): Portal-Ansicht Analyse (Kennzahlen + Diagramm).
### 3.3 News-Pinnwand (Ortsgemeinde Musterbach)
- Website: Kopf mit erfundenem, schlichtem Wappen (SVG), Navigation (Rathaus, Leben in Musterbach,
Veranstaltungen, Pinnwand), Bereich „Neues aus Musterbach“ als **Pinnwand** (Kork o. ä. per CSS) mit 4–5
leicht gedrehten Zetteln im Stil der echten News-Pinnwand (`…/public/embed/news.js`, `news-demo.html`):
„Kerwe 2026: Freitag bis Montag auf dem Dorfplatz“, „Hauptstraße ab 13.10. halbseitig gesperrt (Kanalbau)“,
„Neue Bücherzelle am Spielplatz“, „Sprechstunde des Ortsbürgermeisters: donnerstags 17–19 Uhr“ usw.
Mindestens ein Zettel mit „Foto“ (gezeichnete Grafik, z. B. Kerwe-Karussell oder Bücherzelle).
- Ablauf (Schleife): auf dem iPhone (Portal-Screenshot „News-Pinnwand“ mit dem neuen Zettel) tippt eine
Tipp-Welle auf „Veröffentlichen“ (Koordinaten aus der Aufnahme), kurz danach fällt der neue Zettel mit Nadel
auf die Pinnwand der Website (transform/opacity), Plakette „Neu“. Ohne Bewegung: Zettel hängt schon.
- iPhone: Portal-Ansicht News-Pinnwand (Musterbach-Daten).
## 4. Technik
- Ordner `src/components/home/demos/`: `gemeinsam.tsx/.css` (Taktgeber `useAblauf`, `useDemoLauf`,
`useBewegungErlaubt`, `useFeinerZeiger`, `useImBild`, `DemoZeiger`, `zufall`, `zahl`), `heat.ts`
(Heatmap-Zeichnung wie h.js). Je Demo ein Unterordner `termin/`, `heatmap/`, `pinnwand/`.
- Schnittstelle je Demo (eine Quelle der Wahrheit für Website und Handy-Ebene):
- `use<Name>Ablauf(laeuft: boolean, statisch: boolean)` liefert den Zustand (Phase usw.).
- `<Name>Seite` (`{ zustand, variante: "desktop" | "handy" }`) = die Website auf der Fläche.
- optional `<Name>Handy` (`{ zustand }`) = Live-Ebene über dem Portal-Screenshot im iPhone.
- Heatmap zusätzlich `HeatmapDiagnose` (`{ zustand }`) und Auswahl/Hover im Zustand.
- `laeuft` = Station aktiv **und** Bühne/Karte im Bild **und** Tab sichtbar **und** Bewegung erlaubt.
Nichts läuft im Hintergrund (Lehre aus dem Leistungs-Audit 2026-09-25).
- Bewegung nur `transform`/`opacity` hinter `prefers-reduced-motion: no-preference`; Ausnahme (Auftrag des
Inhabers, ergänzt 8.4): das Neuzeichnen der Heatmap-Leinwand (nur bei Klicks, nicht je Frame).
- Hover-Effekte nur bei `(hover: hover) and (pointer: fine)`.
- Die Demos werden erst geladen, wenn die Bühne die Station fast erreicht (wie die Bilder, `eingehaengt`),
möglichst als eigener Chunk (`React.lazy`), damit die Startseite nicht schwerer wird.
- Inaktive Ebenen der Bühne bekommen `pointer-events: none`, damit nur die sichtbare Website auf die Maus reagiert.
- Tests: Vitest je Demo (Rendern Desktop/Handy, statischer Endzustand ohne Bewegung, Diagnose-Logik,
keine fokussierbaren Elemente in der Website); `relaunch-kundenportal.test.tsx` auf 8 Stationen angepasst (seit „Öffnungszeiten live“: 9 Stationen).
`sofort-paket.test.tsx` (Scans gegen verbotene Aussagen) bleibt grün.
## 5. Offen für den Inhaber
- Terminplaner ist erst im Pilotbetrieb (TP2 „Verkauf“ noch nicht live). Vor dem Livegang des Relaunchs:
entweder TP2 live oder die Station kennzeichnen („bald buchbar“).
- Namen der Beispiel-Kunden und ihre Texte sind Vorschläge.
@@ -0,0 +1,63 @@
# Kundenportal-Bühne: Live-Vorschau, Chat-Demo und weiches Zuklappen
Stand 2026-10-04. Wunsch des Inhabers (diktiert): Das Durchscrollen der Bühne von „Live-Vorschau“ bis „Als App“ soll schöner werden.
1. Die Testseite in der Live-Vorschau wirkt primitiv. Sie soll wie eine richtige Website aussehen, auch übergreifend im Chat.
2. Beim Herausscrollen aus „Funktionen“ soll das Zuklappen der Unterpunkte schöner sein.
3. Der Chat bekommt eine erklärende Animation: Nachrichten ploppen auf, es wird hineingezoomt, damit man sie lesen kann.
Die Texte sollen spielerisch sein, nicht kalt.
## 1. Live-Vorschau
- Im MacBook steht jetzt die Beispiel-Website selbst (`demos/baeckerei/BaeckereiSeite.tsx`), nicht mehr die Portal-Aufnahme.
Die Adresse in der Safari-Leiste ist `baeckerei-sonnenschein.project.webklar.com`, also dieselbe wie im Portal.
- Die Seite ist eine ganze Website: Leiste, Kopf, Startbereich („Frisch gebacken. Jeden Morgen.“), Band, Sortiment mit vier
Karten, Backstube, Torten, Öffnungszeiten mit Stadtplan und Fuß. Die Schrift ist Instrument Serif (selbst gehostet, OFL)
zusammen mit Instrument Sans.
- Die Fotos stammen von Wikimedia Commons und stehen unter CC0. Sie zeigen nur Backwaren, keine Menschen.
Quellen stehen in `docs/relaunch/assets/baeckerei/QUELLEN.md`, das Skript ist `scripts/relaunch-baeckerei-fotos.py`.
- Scroll-Erlebnis: Die Station ist länger (`--kp-l-vorschau: 110svh`). Beim Scrollen durch die Station läuft die Website im
MacBook einmal von oben nach unten durch. Die Bühne schreibt dafür `--kp-v` (0 bis 1); bewegt wird nur `transform`.
Bei reduzierter Bewegung steht die Website oben, und die Station ist kurz.
- Das iPhone zeigt die Projektübersicht des Portals. Die Aufnahme ist neu und zeigt dieselbe Website im Vorschaukasten.
Das gilt auch für die lange Aufnahme in „Als App“. Aufgenommen wurde mit der Fixture des Portals; `demo-site.html` wird mit
`gen-baeckerei.mjs` aus der Komponente erzeugt.
- Hinweiskarte: „Geschützt: Ihre Vorschau öffnet sich nur nach der Anmeldung im Kundenportal – Fremde sehen nichts.“
Das stimmt: `/api/auth/preview-access` lässt nur Kundensitzungen des Projekts und das Team durch.
## 2. Chat-Demo
- Im MacBook ist der Portal-Chat zu sehen. Darunter liegt die Aufnahme des leeren Chats (`desktop-3c-chat-leer`, auch in
2400 px, damit sie beim Zoomen scharf bleibt). Darüber liegt eine Live-Ebene (`demos/chat/`) mit den Blasen, dem
Eingabefeld, dem Anhang, der Sprachaufnahme, dem Senden-Knopf und einem Zeiger. Formen und Farben sind wie im Portal.
- Ablauf (etwa 28 s, Schleife): Zuerst die Übersicht, dann zoomt die Kamera auf das Gespräch (1,8-fach, nur `transform`).
Marlene hängt ein Foto an und tippt Zeichen für Zeichen. Danach wird gesendet, Kenso antwortet, und Marlene schickt eine
Sprachnachricht. Es folgen noch eine Antwort, eine Nachricht von Marlene und die letzte Antwort. Dann zoomt die Kamera
heraus, und die Nachrichten blenden aus.
Neue Blasen ploppen aus ihrer Ecke auf und federn leicht nach. Die älteren Blasen rutschen weich nach oben (FLIP).
- Texte (Sie-Form vom Team, warm und spielerisch):
- Marlene: „Guten Morgen! ☀️ Unser neues Roggenbrot ist gerade aus dem Ofen. Darf das auf die Startseite?“ (mit Foto)
- Kenso: „Und wie das darf! 😍 Das Foto ist so knusprig, ich rieche es bis hierher.“
- Marlene: Sprachnachricht 0:12
- Kenso: „Alles notiert: Brot ganz nach oben, Preis dazu, Öffnungszeiten etwas größer. Schauen Sie mal in die Vorschau! 🥖“
- Marlene: „Sieht super aus! Morgen gibt’s Croissants fürs ganze Team. 🥐“
- Kenso: „Dann stellen wir schon mal den Kaffee an. ☕“
- Das Foto im Chat ist dasselbe Roggenbrot wie auf der Website, denn die Geschichte geht über beide Stationen.
- Die Uhrzeiten (07:52 bis 10:31) versprechen keine Sofort-Antwort. Ein Tippen-Hinweis des Teams wird nicht gezeigt, weil
das Portal keinen hat.
- In der Station Chat gleitet das iPhone nach rechts unten weg. Sonst würde es die eigenen Nachrichten und den
Senden-Knopf verdecken.
- Hinweiskarte: „Antwortet das Team, kommt zusätzlich eine E-Mail – so verpassen Sie keine Nachricht.“
Das stimmt: Bei jeder Team-Nachricht geht die Mail K17 hinaus, gedrosselt.
- In der Wisch-Reihe auf dem Handy läuft dasselbe Gespräch über der Handy-Aufnahme des leeren Chats, dort ohne Zoom.
- Bei reduzierter Bewegung steht sofort der Endzustand da: alle Nachrichten, hineingezoomt.
## 3. Weiches Auf- und Zuklappen von „Funktionen“
- Zustand `zu`, `offen` oder `schliesst`. Beim Verlassen der drei Funktionen falten sich die Unterpunkte von unten nach
oben ein, und die Linie links zieht sich nach oben zurück. Das dauert etwa 260 ms. Danach verschwindet die Unterliste.
- „Als App“ springt dabei nicht, sondern gleitet per FLIP von der alten an die neue Stelle (Web Animations API, nur
`transform`). Beim Aufklappen gleitet es genauso nach unten, und die Unterpunkte gleiten nacheinander herein.
- Ein kleiner Pfeil hinter „Funktionen“ zeigt den Zustand: nach rechts heißt zu, nach unten heißt offen.
- Für Screenreader ist die Gruppe sofort zu (`aria-expanded="false"`, die Unterliste ist während des Einfaltens
`aria-hidden`). Bei reduzierter Bewegung klappt die Gruppe sofort zu.
+8 -6
View File
@@ -25,18 +25,18 @@
</script> </script>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WEBklar – Webagentur für KMU | Digitalisierung &amp; Automatisierung</title> <title>WEBklar – Websites, Shops und Apps für kleine Betriebe</title>
<meta <meta
name="description" name="description"
content="WEBklar digitalisiert KMU: Website, Prozesse und Systeme aus einer Hand. Kostenlose Potenzialanalyse – Antwort innerhalb von 24 Stunden." content="WEBklar baut Websites, Online-Shops und Web-Apps. Im Kundenportal sehen Sie jeden Schritt, mit Live-Vorschau Ihrer Website."
/> />
<meta name="author" content="WEBklar" /> <meta name="author" content="WEBklar" />
<link rel="canonical" href="https://webklar.com/" /> <link rel="canonical" href="https://webklar.com/" />
<meta property="og:title" content="WEBklar – Webagentur für KMU" /> <meta property="og:title" content="WEBklar – Websites, Shops und Apps für kleine Betriebe" />
<meta <meta
property="og:description" property="og:description"
content="Website, Automatisierung und vernetzte Prozesse für wachsende Unternehmen – ohne Mehraufwand im Alltag." content="WEBklar baut Websites, Online-Shops und Web-Apps. Im Kundenportal sehen Sie jeden Schritt, mit Live-Vorschau Ihrer Website."
/> />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="https://webklar.com/" /> <meta property="og:url" content="https://webklar.com/" />
@@ -45,12 +45,14 @@
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@WEBklar" /> <meta name="twitter:site" content="@WEBklar" />
<meta name="twitter:title" content="WEBklar – Webagentur für KMU" /> <meta name="twitter:title" content="WEBklar – Websites, Shops und Apps für kleine Betriebe" />
<meta <meta
name="twitter:description" name="twitter:description"
content="Digitalisierung für KMU: Website, Prozesse und Systeme. Kostenlose Potenzialanalyse." content="WEBklar baut Websites, Online-Shops und Web-Apps. Im Kundenportal sehen Sie jeden Schritt, mit Live-Vorschau Ihrer Website."
/> />
<meta name="twitter:image" content="https://webklar.com/og-image.png" /> <meta name="twitter:image" content="https://webklar.com/og-image.png" />
<script data-wk-analytics src="https://project.webklar.com/a/w.js?s=yiy992rvq7vchfd7bqtqivah" data-site="yiy992rvq7vchfd7bqtqivah" data-color="#1d1d1f" data-variant="eigen" defer></script>
</head> </head>
<body> <body>
+94
View File
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Datenschutz bei der Reichweitenmessung – WEBklar</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #f7f6f3; color: #1d1d1f; font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 12rem; }
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 13px; color: #6d1fd1; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #6b6b70; font-size: 14px; }
.box { background: #fff; border: 2px solid #6d1fd1; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
a { color: #6d1fd1; }
nav.top { font-size: 14px; margin-bottom: 22px; }
</style>
</head>
<body>
<main>
<nav class="top"><a href="/">← Zur Startseite</a> · <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a> · <a href="/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Datenschutz bei der Reichweitenmessung</h1>
<p class="meta">Ergänzung zu unserer Datenschutzerklärung (Abschnitt 4a) · gilt für webklar.com · Stand 17.10.2026</p>
<p>Diese Seite beschreibt nur die Reichweitenmessung auf webklar.com. Alle übrigen Hinweise finden Sie in unserer <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a>.</p>
<h2>Verantwortlicher</h2>
<p>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler<br>
E-Mail: <a href="mailto:support@webklar.com">support@webklar.com</a></p>
<p>Einen Datenschutzbeauftragten haben wir nicht benannt, weil wir dazu gesetzlich nicht verpflichtet sind.</p>
<h2>Worum es geht</h2>
<p>Wir möchten verstehen, wie unsere Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir unsere eigene Website-Analyse, dieselbe, die wir auch unseren Kunden anbieten. Sie kommt ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken aus. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf unserem Server in Deutschland.</p>
<p>Damit die Messung arbeiten kann, lädt Ihr Browser bei jedem Seitenaufruf ein Skript von unserem Server (project.webklar.com). Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Ihre IP-Adresse und Ihr Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Sie ablehnen oder „Global Privacy Control“ aktiviert haben. Beim Abruf des Skripts und bei der Messung speichern und protokollieren wir beides nicht.</p>
<h2>Solange Sie im Banner nichts ausgewählt haben</h2>
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
<ul>
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
<li>das Land, das wir aus Ihrer IP-Adresse ableiten,</li>
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
</ul>
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Ihrer IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Sie wiedererkennen könnten, speichern wir ohne Ihre Einwilligung nicht.</p>
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
<h2>Wenn Sie „Ablehnen“ wählen</h2>
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Ihr Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
<h2>Wenn Sie „Annehmen“ wählen</h2>
<p>Dann messen wir zusätzlich:</p>
<ul>
<li>von welcher Website Sie gekommen sind (nur der Domainname),</li>
<li>die Größenklasse Ihres Browserfensters,</li>
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Ihrer IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
<li>wie lange Sie auf einer Seite bleiben (ab dem Laden der Seite) und wie weit Sie scrollen,</li>
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
<li>ob Sie auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klicken (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
<li>einen Zähler dafür, dass Sie „Annehmen“ gewählt haben.</li>
</ul>
<p>Rechtsgrundlage ist Ihre Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
<h2>Was in Ihrem Browser gespeichert wird</h2>
<p>Wir speichern Ihre Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Ihres Browsers (Eintrag „wk-consent“), damit wir Sie nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Sie können den Eintrag jederzeit selbst löschen, indem Sie die Website-Daten in Ihrem Browser löschen.</p>
<h2>Speicherdauer</h2>
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten. Beenden wir die Messung, löschen wir alle Messdaten spätestens rund 60 Tage danach. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
<h2>Empfänger</h2>
<p>Die Messung betreiben wir selbst. Die Daten sehen nur unsere Mitarbeiter. Der Server steht bei der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland; Hetzner ist für uns als Auftragsverarbeiter tätig. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
<h2>Freiwilligkeit</h2>
<p>Sie sind weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Sie ablehnen oder Ihre Einwilligung widerrufen, können Sie unsere Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
<div class="box">
<h2 style="border:0;margin-top:0;padding-top:0">Einwilligung widerrufen</h2>
<p>Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicken Sie auf „Datenschutz-Einstellungen“ (das Keks-Symbol unten rechts auf jeder Seite) und wählen Sie „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
<h2 style="border:0;padding-top:0">Widerspruchsrecht</h2>
<p>Sie können der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Ihrer besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählen Sie im Banner oder über „Datenschutz-Einstellungen“ (das Keks-Symbol) „Ablehnen“ oder aktivieren in Ihrem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Sie können Ihren Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
</div>
<h2>Ihre Rechte</h2>
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Ihnen zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle unseres Webservers noch vorhanden sind (Abschnitt 3 der Datenschutzerklärung). Wenn Sie uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Sie möchten, Ihre IP-Adresse nennen, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Ihnen das mit.</p>
<p>Mit Ihren Anliegen können Sie sich jederzeit an die oben genannte Adresse wenden. Außerdem haben Sie das Recht, sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO), für uns zuständig ist der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz.</p>
</main>
</body>
</html>
+93
View File
@@ -0,0 +1,93 @@
Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) InstrumentSans-Italic[wdth,wght].ttf: Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2022 The Instrument Serif Project Authors (https://github.com/Instrument/instrument-serif)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""
Relaunch: Fotos der erfundenen Beispiel-Website „Bäckerei Sonnenschein“ (Station Live-Vorschau und Chat-Demo).
Alle Fotos stammen von Wikimedia Commons und stehen unter CC0 (gemeinfrei, keine Namensnennung nötig).
Es sind nur Backwaren zu sehen, keine Menschen. Quellen: docs/relaunch/assets/baeckerei/QUELLEN.md.
Wikimedia Commons (einmalig, Zwischenspeicher docs/relaunch/assets/baeckerei/roh/, nicht im Repository)
-> public/relaunch/demos/baeckerei/<name>-<breite>.webp
Aufruf (aus dem Projektverzeichnis): python3 scripts/relaunch-baeckerei-fotos.py
Braucht Python 3 und Pillow mit WebP-Unterstützung.
"""
from pathlib import Path
import sys
import urllib.parse
import urllib.request
from PIL import Image
WURZEL = Path(__file__).resolve().parent.parent
ROH = WURZEL / "docs" / "relaunch" / "assets" / "baeckerei" / "roh"
ZIEL = WURZEL / "public" / "relaunch" / "demos" / "baeckerei"
KENNUNG = {"User-Agent": "WEBklar-relaunch/1.0 (support@webklar.com)"}
KB = 1024
QUALITAET = 78
MIN_QUALITAET = 50
# Name -> (Datei auf Commons, Ausschnitt als Anteile (links, oben, rechts, unten), Zielbreiten, Seitenverhältnis B/H, Budget je Datei)
FOTOS = {
# Startseite und Chat-Foto: dasselbe Roggenbrot (Marlene schickt es im Chat, danach steht es auf der Website)
"roggenbrot": ("Rye bread Roggenbengel 2025-02-14 13.jpg", (0.0, 0.0, 1.0, 1.0), (600,), 1.08, 58 * KB),
"roggenbrot-anschnitt": ("Cut rye bread Kartoffelkruste 2025-02-20 02.jpg", (0.12, 0.12, 0.92, 0.95), (400,), 0.8, 30 * KB),
"broetchen": ("Bread-rolls.jpg", (0.0, 0.0, 1.0, 1.0), (400,), 0.8, 30 * KB),
"croissants": ("Freshippo croissant bread.jpg", (0.05, 0.0, 0.95, 1.0), (400,), 0.8, 34 * KB),
"zimtschnecken": ("IKEA cinnamon bun.jpg", (0.05, 0.0, 0.95, 1.0), (400,), 0.8, 30 * KB),
"baguettes": ("Baguette-246424.jpg", (0.0, 0.0, 1.0, 1.0), (700,), 1.2, 40 * KB),
"zwetschgenkuchen": ("Plum cake, Ehrenbach.jpg", (0.18, 0.08, 1.0, 1.0), (520,), 1.0, 50 * KB),
}
def lade(datei: str) -> Path:
"""Lädt das Foto einmal in 2000 px Breite von Commons (Special:FilePath) und hält es im Zwischenspeicher."""
ROH.mkdir(parents=True, exist_ok=True)
ziel = ROH / datei.replace(" ", "_")
if not ziel.exists():
url = "https://commons.wikimedia.org/w/index.php?title=Special:FilePath/" + urllib.parse.quote(datei) + "&width=2000"
with urllib.request.urlopen(urllib.request.Request(url, headers=KENNUNG), timeout=60) as antwort:
ziel.write_bytes(antwort.read())
return ziel
def zuschneiden(bild: Image.Image, anteile: tuple, verhaeltnis: float) -> Image.Image:
"""Schneidet erst den Ausschnitt aus, dann mittig auf das Seitenverhältnis."""
b, h = bild.size
l, o, r, u = anteile
bild = bild.crop((round(l * b), round(o * h), round(r * b), round(u * h)))
b, h = bild.size
if b / h > verhaeltnis:
neu = round(h * verhaeltnis)
return bild.crop(((b - neu) // 2, 0, (b - neu) // 2 + neu, h))
neu = round(b / verhaeltnis)
return bild.crop((0, (h - neu) // 2, b, (h - neu) // 2 + neu))
def speichern(bild: Image.Image, ziel: Path, budget: int) -> tuple[int, int]:
qualitaet = QUALITAET
while True:
bild.save(ziel, "WEBP", quality=qualitaet, method=6)
groesse = ziel.stat().st_size
if groesse <= budget or qualitaet - 4 < MIN_QUALITAET:
return groesse, qualitaet
qualitaet -= 4
def main() -> int:
ZIEL.mkdir(parents=True, exist_ok=True)
for alt in ZIEL.glob("*.webp"):
alt.unlink()
summe = 0
for name, (datei, anteile, breiten, verhaeltnis, budget) in FOTOS.items():
roh = zuschneiden(Image.open(lade(datei)).convert("RGB"), anteile, verhaeltnis)
for breite in breiten:
klein = roh.resize((breite, round(breite / verhaeltnis)), Image.LANCZOS)
groesse, q = speichern(klein, ZIEL / f"{name}-{breite}.webp", budget)
summe += groesse
print(f"{name}-{breite}.webp {klein.size[0]}x{klein.size[1]} {groesse / KB:.1f} KB q{q}")
print(f"Summe: {summe / KB:.0f} KB")
return 0
if __name__ == "__main__":
sys.exit(main())
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
Relaunch: verkleinert die Rohexporte der Kundenportal-Screenshots für die Website.
docs/relaunch/assets/portal/*.webp (Rohdateien, doppelte Auflösung)
-> public/relaunch/portal/<name>-<breite>.webp
Aufruf (aus dem Projektverzeichnis): python3 scripts/relaunch-bilder.py [NAME ...]
Ohne Argument werden alle Bilder neu geschrieben (alte Ausgaben vorher entfernt). Mit einem oder mehreren NAME-Filtern
(Dateiname ohne .webp, auch als Anfang, z. B. "phone-9" oder "phone-10-analyse") entstehen nur die passenden Bilder;
alle anderen Dateien in public/relaunch/portal bleiben unangetastet.
Nur die Bilder, die der Abschnitt "Kundenportal" (Spezifikation 7.2) wirklich zeigt, landen in public/.
Format und Budgets laut Spezifikation 9.5:
- WebP, Qualität etwa 80, Desktop-Bilder in 800 / 1200 / 1600 px Breite, Handy-Bilder in 390 / 780 px
- ein einzelnes Desktop-Bild höchstens 120 KB, ein einzelnes Handy-Bild höchstens 60 KB
(der lange Screenshot hat ein eigenes, größeres Budget, weil er mehr als dreimal so hoch ist)
- wird ein Budget überschritten, sinkt die Qualität pro Datei schrittweise (nicht unter MIN_QUALITAET)
Braucht Python 3 und Pillow mit WebP-Unterstützung.
"""
from pathlib import Path
import sys
from PIL import Image
WURZEL = Path(__file__).resolve().parent.parent
QUELLE = WURZEL / "docs" / "relaunch" / "assets" / "portal"
ZIEL = WURZEL / "public" / "relaunch" / "portal"
QUALITAET = 80
MIN_QUALITAET = 46
SCHRITT = 4
KB = 1024
BUDGET_DESKTOP = 120 * KB
BUDGET_HANDY = 60 * KB
# Der lange Screenshot ist gut dreimal so hoch wie ein Handy-Bild (5131 statt 1748 px). Sein Budget
# wächst mit der Höhe (je Bildschirmhöhe weiterhin unter 60 KB), sonst wäre der Text nur noch Matsch.
BUDGET_LANG = {390: 80 * KB, 780: 150 * KB}
# Unter der letzten Inhaltszeile (Zeile 5091) ist der Rohexport nur noch schwarz. Abgeschnitten wird
# 40 px darunter, sonst endet die Verschiebung im Handy-Rahmen auf einem leeren Streifen.
LANG_HOEHE = 5131
# Dateiname (ohne .webp) -> (Art, Zielbreiten)
BILDER = {
# Desktop-Fenster (Roh 3024 x 1786 = MacBook-Safari-Fläche 1512 x 893 @2x); die Live-Vorschau zeigt im MacBook die
# Beispiel-Website selbst (BaeckereiSeite), kein Bild
"desktop-2-fortschritt": ("desktop", (800, 1200, 1600)),
# Chat-Demo (Station Chat): leerer Chat als Kulisse, die Nachrichten sind live; 2400 px, weil die Kamera hineinzoomt
"desktop-3c-chat-leer": ("desktop", (1600, 2400)),
"desktop-4-rechnungen": ("desktop", (800, 1200, 1600)),
# Handy (Roh 804 x 1748 = iPhone 16 Pro @2x, Safe Area 59/34; das lange Bild 804 x 5340, beschnitten auf 5131)
"phone-1-projekte": ("handy", (390, 780)),
"phone-3-projektdetails": ("handy", (390, 780)),
"phone-4c-chat-leer": ("handy", (390, 780)),
"phone-5-rechnungen": ("handy", (390, 780)),
# Funktions-Stationen (Spezifikation 2026-10-04-funktionen-demos.md): Handy-Ansicht der Funktion im Kundenportal
"phone-9-termine": ("handy", (390, 780)),
"phone-10-analyse": ("handy", (390, 780)),
"phone-11-news-musterbach": ("handy", (390, 780)),
"phone-12-zeiten": ("handy", (390, 780)),
# langer Screenshot: behält sein hohes Seitenverhältnis (eigenes Budget, siehe BUDGET_LANG)
"phone-2-projekte-lang": ("handy", (390, 780)),
}
def speichern(bild: Image.Image, ziel: Path, budget: int) -> tuple[int, int]:
"""Speichert als WebP; senkt die Qualität, bis das Budget passt. Gibt (Bytes, Qualität) zurück."""
qualitaet = QUALITAET
while True:
bild.save(ziel, "WEBP", quality=qualitaet, method=6)
groesse = ziel.stat().st_size
if groesse <= budget or qualitaet - SCHRITT < MIN_QUALITAET:
return groesse, qualitaet
qualitaet -= SCHRITT
def waehle(filter_: list[str]) -> dict:
"""Alle Bilder ohne Filter, sonst die, deren Name genau so heißt oder mit einem Filter beginnt (z. B. "phone-9")."""
if not filter_:
return BILDER
# "phone-1" soll nicht auch "phone-10-analyse" treffen: Anfang nur bis zum nächsten Bindestrich-Segment
treffer = {
name: wert for name, wert in BILDER.items()
if any(name == f or name.startswith(f + "-") for f in filter_)
}
unbekannt = [f for f in filter_ if not any(n == f or n.startswith(f + "-") for n in BILDER)]
if unbekannt:
raise SystemExit(f"Unbekannter Bildname: {', '.join(unbekannt)}")
return treffer
def main(argv: list[str]) -> int:
ZIEL.mkdir(parents=True, exist_ok=True)
bilder = waehle(argv)
if not argv:
# alte Ausgaben entfernen, damit nichts Verwaistes liegen bleibt (nur bei einem Lauf über alle Bilder)
for alt in ZIEL.glob("*.webp"):
alt.unlink()
fehler = 0
summe = 0
print(f"{'Datei':44} {'Pixel':>11} {'KB':>7} {'Q':>3} Budget")
for name, (art, breiten) in bilder.items():
quelle = QUELLE / f"{name}.webp"
if not quelle.exists():
print(f"FEHLT: {quelle}", file=sys.stderr)
fehler += 1
continue
roh = Image.open(quelle).convert("RGB")
if name.endswith("-lang"):
roh = roh.crop((0, 0, roh.width, LANG_HOEHE))
for breite in breiten:
if name.endswith("-lang"):
budget = BUDGET_LANG[breite]
else:
budget = BUDGET_DESKTOP if art == "desktop" else BUDGET_HANDY
if breite > roh.width:
print(f"übersprungen: {name} {breite} (Rohbild nur {roh.width} px breit)")
continue
hoehe = round(roh.height * breite / roh.width)
klein = roh if breite == roh.width else roh.resize((breite, hoehe), Image.LANCZOS)
datei = ZIEL / f"{name}-{breite}.webp"
groesse, qualitaet = speichern(klein, datei, budget)
summe += groesse
ok = "ok" if groesse <= budget else f"ÜBER {budget // KB} KB"
print(f"{datei.name:44} {breite:>5}x{hoehe:<5} {groesse / KB:7.1f} {qualitaet:>3} {ok}")
print(f"Summe aller Dateien: {summe / KB:.0f} KB")
return 1 if fehler else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
+42 -20
View File
@@ -2,20 +2,51 @@ import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner"; import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { useEffect } from "react";
import { BrowserRouter, useLocation } from "react-router-dom";
import { ThemeProvider } from "@/components/ThemeProvider"; import { ThemeProvider } from "@/components/ThemeProvider";
import { AnalyticsProvider } from "@/components/AnalyticsProvider"; import { AnalyticsProvider } from "@/components/AnalyticsProvider";
import Index from "./pages/Index"; import { AppRoutes } from "./routes";
import ContactPage from "./pages/Contact";
import AGBPage from "./pages/AGB";
import ImpressumPage from "./pages/Impressum";
import AboutPage from "./pages/About";
import WartungsAboPage from "./pages/WartungsAbo";
import DatenschutzPage from "./pages/Datenschutz";
import NotFound from "./pages/NotFound";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
/**
* Scrollt bei einem Seitenwechsel nach oben. Mit Anker (URL mit #) springt es zum Abschnitt,
* sobald er gerendert ist, z. B. beim Footer-Link „/#services“ von einer Unterseite aus.
*/
export const ScrollToTop = () => {
const { pathname, hash } = useLocation();
useEffect(() => {
if (!hash) {
window.scrollTo(0, 0);
return;
}
const id = decodeURIComponent(hash.slice(1));
let versuche = 0;
let frame = 0;
let abgebrochen = false;
const springe = () => {
const ziel = document.getElementById(id);
if (!ziel) {
if (versuche++ < 60) frame = requestAnimationFrame(springe); // höchstens ca. 1 s warten
return;
}
// Erst springen, wenn die Schriften geladen sind: Vorher ist der Text darüber höher,
// und der Abschnitt landet nach dem Nachladen ein Stück zu weit oben.
const schriften = document.fonts?.ready ?? Promise.resolve();
schriften.then(() => {
if (!abgebrochen) ziel.scrollIntoView();
});
};
springe();
return () => {
abgebrochen = true;
cancelAnimationFrame(frame);
};
}, [pathname, hash]);
return null;
};
const App = () => ( const App = () => (
<ThemeProvider> <ThemeProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@@ -23,18 +54,9 @@ const App = () => (
<Toaster /> <Toaster />
<Sonner /> <Sonner />
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}> <BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ScrollToTop />
<AnalyticsProvider> <AnalyticsProvider>
<Routes> <AppRoutes />
<Route path="/" element={<Index />} />
<Route path="/kontakt" element={<ContactPage />} />
<Route path="/agb" element={<AGBPage />} />
<Route path="/impressum" element={<ImpressumPage />} />
<Route path="/ueber-uns" element={<AboutPage />} />
<Route path="/wartungs-abo" element={<WartungsAboPage />} />
<Route path="/datenschutz" element={<DatenschutzPage />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</AnalyticsProvider> </AnalyticsProvider>
</BrowserRouter> </BrowserRouter>
</TooltipProvider> </TooltipProvider>
-45
View File
@@ -1,45 +0,0 @@
const AgitationSection = () => {
return (
<section className="py-24 md:py-32 bg-secondary/30 relative overflow-hidden">
{/* Decorative element */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] rounded-full border border-foreground" />
</div>
<div className="container mx-auto px-6 relative z-10">
<div className="max-w-4xl mx-auto text-center">
<div className="label-tag mb-4">Die Realität</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-12">
Mehr Arbeit darf nicht die einzige Antwort auf Wachstum sein.
</h2>
<div className="space-y-8 text-lg md:text-xl text-muted-foreground leading-relaxed">
<p>
Neue Kunden sollten Ihr Unternehmen nicht ins Chaos stürzen.
</p>
<p>
Wenn mehr Umsatz automatisch mehr Stress bedeutet, liegt das Problem nicht an Ihrem Markt – <span className="text-foreground font-medium">sondern an fehlenden Systemen.</span>
</p>
<div className="pt-8 border-t border-border">
<p className="text-2xl md:text-3xl text-foreground font-display font-medium uppercase tracking-tight">
Erfolgreiche Unternehmen skalieren Prozesse.
</p>
<p className="text-2xl md:text-3xl text-muted-foreground font-display font-medium uppercase tracking-tight mt-2">
Nicht Arbeitsstunden.
</p>
</div>
<p className="text-foreground font-medium pt-4">
Genau dort setzen wir an.
</p>
</div>
</div>
</div>
</section>
);
};
export default AgitationSection;
+22
View File
@@ -6,6 +6,8 @@
--border-radius: 28px; --border-radius: 28px;
--glow-padding: 40px; --glow-padding: 40px;
--cone-spread: 25; --cone-spread: 25;
/* Kartenfarbe nach Theme: --card-bg-dark / --card-bg-light kommen aus BorderGlow.tsx */
--card-bg: var(--card-bg-dark, #000);
position: relative; position: relative;
border-radius: var(--border-radius); border-radius: var(--border-radius);
isolation: isolate; isolation: isolate;
@@ -25,6 +27,12 @@
rgba(0, 0, 0, 0.1) 0px 32px 64px; rgba(0, 0, 0, 0.1) 0px 32px 64px;
} }
/* Heller Modus: helle Kartenfarbe und ein sichtbarer Rand (weiß mit 8 % Deckkraft wäre auf Weiß unsichtbar) */
html:not(.dark) .border-glow-card {
--card-bg: var(--card-bg-light, var(--card-bg-dark, #000));
border-color: hsl(var(--border));
}
.border-glow-card::before, .border-glow-card::before,
.border-glow-card::after, .border-glow-card::after,
.border-glow-card > .edge-light { .border-glow-card > .edge-light {
@@ -147,6 +155,20 @@
0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%)); 0 0 50px 2px var(--glow-color-10, hsl(40deg 80% 80% / 10%));
} }
/* Heller Modus: "plus-lighter" ginge auf einer hellen Karte im Weiß unter, und die blasse Glow-Farbe wäre kaum sichtbar.
Der Kantenschein wird deshalb normal gemischt und in einem kräftigeren Violett gezeichnet. */
html:not(.dark) .border-glow-card > .edge-light {
mix-blend-mode: normal;
}
html:not(.dark) .border-glow-card > .edge-light::before {
box-shadow:
inset 0 0 0 1px hsl(268deg 83% 62% / 70%),
inset 0 0 3px 0 hsl(268deg 83% 62% / 45%),
inset 0 0 12px 0 hsl(268deg 83% 62% / 30%),
inset 0 0 25px 2px hsl(268deg 83% 62% / 15%);
}
.border-glow-inner { .border-glow-inner {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+6 -1
View File
@@ -56,7 +56,10 @@ interface BorderGlowProps {
className?: string; className?: string;
edgeSensitivity?: number; edgeSensitivity?: number;
glowColor?: string; glowColor?: string;
/** Kartenfarbe im dunklen Modus (und Standard, wenn lightBackgroundColor fehlt). */
backgroundColor?: string; backgroundColor?: string;
/** Kartenfarbe im hellen Modus; ohne Angabe gilt backgroundColor in beiden Themes. */
lightBackgroundColor?: string;
borderRadius?: number; borderRadius?: number;
glowRadius?: number; glowRadius?: number;
glowIntensity?: number; glowIntensity?: number;
@@ -72,6 +75,7 @@ const BorderGlow = ({
edgeSensitivity = 30, edgeSensitivity = 30,
glowColor = '40 80 80', glowColor = '40 80 80',
backgroundColor = '#120F17', backgroundColor = '#120F17',
lightBackgroundColor,
borderRadius = 28, borderRadius = 28,
glowRadius = 40, glowRadius = 40,
glowIntensity = 1.0, glowIntensity = 1.0,
@@ -183,7 +187,8 @@ const BorderGlow = ({
onPointerMove={animated ? undefined : handlePointerMove} onPointerMove={animated ? undefined : handlePointerMove}
className={`border-glow-card ${className}`} className={`border-glow-card ${className}`}
style={{ style={{
'--card-bg': backgroundColor, '--card-bg-dark': backgroundColor,
'--card-bg-light': lightBackgroundColor ?? backgroundColor,
'--edge-sensitivity': edgeSensitivity, '--edge-sensitivity': edgeSensitivity,
'--border-radius': `${borderRadius}px`, '--border-radius': `${borderRadius}px`,
'--glow-padding': `${glowRadius}px`, '--glow-padding': `${glowRadius}px`,
+6 -3
View File
@@ -12,6 +12,7 @@ const Contact = () => {
edgeSensitivity={30} edgeSensitivity={30}
glowColor="40 80 80" glowColor="40 80 80"
backgroundColor="#120F17" backgroundColor="#120F17"
lightBackgroundColor="hsl(0 0% 96%)"
borderRadius={28} borderRadius={28}
glowRadius={40} glowRadius={40}
glowIntensity={1} glowIntensity={1}
@@ -38,9 +39,11 @@ const Contact = () => {
data-analytics-cta="Potenzialanalyse starten" data-analytics-cta="Potenzialanalyse starten"
data-analytics-location="contact_section" data-analytics-location="contact_section"
> >
{/* Etwas dunkleres Blau als der Standard-Button (3,45:1 -> 4,76:1 mit weißem Text, Hover 6,08:1);
die Farbe gilt in beiden Themes, weil der Button nicht von Theme-Farben abhängt. */}
<Button <Button
size="lg" size="lg"
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group" className="btn-minimal rounded-full px-8 py-6 text-base font-medium group bg-[hsl(198,93%,35%)] hover:bg-[hsl(198,93%,30%)]"
> >
Potenzialanalyse starten Potenzialanalyse starten
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" /> <ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
@@ -54,9 +57,9 @@ const Contact = () => {
<Mail className="w-5 h-5" /> <Mail className="w-5 h-5" />
<span>support@webklar.com</span> <span>support@webklar.com</span>
</a> </a>
<a href="tel:+491704969375" className="flex items-center gap-3 hover:text-foreground transition-colors group"> <a href="tel:+4917623726355" className="flex items-center gap-3 hover:text-foreground transition-colors group">
<Phone className="w-5 h-5" /> <Phone className="w-5 h-5" />
<span>0170 4969375</span> <span>0176 23726355</span>
</a> </a>
</div> </div>
</div> </div>
+130
View File
@@ -0,0 +1,130 @@
import { useLayoutEffect, useRef, useState } from "react";
import "./deutschland-karte.css";
/**
* Umriss von Deutschland als Inline-SVG (Spezifikation 7.1).
* Quelle der Pfaddaten: docs/relaunch/assets/deutschland-umriss.svg (Natural Earth 1:10m, Public Domain,
* Lambert-Azimutal-flächentreu, viewBox 0 0 663 892, 1 Einheit ≈ 1 km).
*
* Der Umriss besteht aus sechs geschlossenen Teilpfaden (Festland, Rügen, Usedom, Fehmarn, Sylt, Föhr/Amrum).
* Jeder ist ein eigener <path> mit pathLength="1", damit alle beim Zeichnen gleichzeitig fertig werden
* (bei einem einzigen Pfad wären die kleinen Inseln schon nach den ersten Millisekunden da).
* Die Pfade beginnen mit absolutem M; die Befehle danach sind unverändert aus der Datei übernommen.
*/
const UMRISS = [
"M581.4 706.2l-2 5.8 2.5 2-1 2.4 0.4 7-1.8 4.8-2.9 1.2-1.1 4.3-7-4.5-7.7-1.8-4.8 1.5-1 1.4 1.6 4.9-3.3 15.7-9.3 8.2-10.1 2.2-7.6 3.4-7.1 6.3-5.2 1.6-8.8 9.6 1.8 5.4 5.4 5.2 2.6 5.8 5.3 4 4.8 8.5-6.9 14 5.5 1.8 4.1-1 4.2 5.8 0.1 4.1-2.3 4.3-0.6 4.8 0.9 5.3-2.5 3.2-5.4-0.7-11.8-8.6 0.4-5.3 2-1.1-4.8-4 0.7-1.9-8.2-0.7-8.4 5.2-3.1 0.4-4-2.9-1.7-3.9-13.8 1.8-1.2-1.8 1.3-4.1-4.8 3.1 2.4 6.6-2.4 4.6-17.8-0.1-6.3 1.4-2.3 2.7-15-1.2-5 8.5-10.4 1.1-3.5 4.1 1.8 0.8-0.4 1.9-5.8 1.2-5.1 4.7-1.8-0.2 0.1-3-1.6-0.7-8.3 4.5-7.8 0-1.9-2.1 0.3-2.5-3.8-4-3.9-1.8-0.5-0.8 3.1-2.5-2.6-1.7-7.3 2-1.6-1.7-11.8-3.6-3.5 3.6-4.1-0.3-2.1-1.5 0.2-3.6-2.1 0.2-1.1 0.5 1.3 3.4-0.9 5.5 2.2 2.2 0.3 3.8-1.7 4.7-4.3 3.2-4.9 7.3-5 2.7-6 0.8 2.3-3 1.4-8.3-1.4-0.7-8.2 2.2-2.2-5.1 1.7-1.3-0.3-2.5-3.7-5.4-3 0.8-2.8-6.7-8.4 0.7-1.5-0.8-0.4-3.8-2-1.4-2.2 0.7-3.6 5.6-6.9 1.1-4.7-1.4-20.7-13.1-5.7-0.7-1-1.6-12.5-1.1-5.4 2.7-4.7-0.3-3.3-2.8 1.4-1.1-4.3-3.3-2-0.1-0.1 2.8-4 0-1.2-3.2 1-3.8-2.3-0.2-2.8-3.6-2 3.7-1.1-4.2-1.4-0.6-3.1 0.5 0.7 1.8-1.1 0.5-5.5 1-2 4.5-3.6 3.4 0 3 4.9 2.9 8.3-2.4 2.9 0.8-0.5 2.6-1.5-0.7-0.1 4.3-1.5 1.2-2.8-3.7-4.6 1.7-1 2.4-7.1 0.2-3.6-1.3-1.3-2.7-4.1-0.8-4.1 0.6-7 5.2-3.4 0.7-9.8-0.3-0.9-3-5.9-1-4.1 4.2-6.3 1.1-5.4-2.4 1.9 0 1.9-3.4-5.6 1.1 0.4-2.6-5.1-7.6-0.4-3.6 2.1-2.7-0.7-5.7 2.9-6.1-0.1-4.9 2.1-6.3 2.8-3.4 0-3.4-3.1-5.8 0.5-6.8 2.8-7.1 5.7-8 1.3-6.4 3.7-3.3-0.7-7.2 3.1-10.1 2.8-2.3 0.1-10.6 3.2-4.8 8-6.7 1-4 3.4-0.4 1.7-3.2 3.8-1.8 8.5-16.5-8-2.5-11.4-6.5-5.5 0.1-6-1.9-3.4 1.2-6.7-0.4-1.9-2.8-6.3-2.6-3.4-5.1 0.4-2.5-4.4-0.9-0.3-1.2-4.3 0.9-1.4 2.7-2.7 0.6-1.7 2.5-8.7-1.4-5.3-3.2-0.6 2.3-2.3 1.4-1.5-1.4-0.6-7-6.8-3-5.8-0.6 0.2 5.2-1.4 1.6-6.9-1.1-1.7-2 0.3-4.6-1.6 0.6-2.7-7.2-5.7-6.4 0-2.3 1.7-0.9-4.5-7.8-7.6-5-4.2 1 1-12.4 5.2-7.8-1-1.9 7.3-5.9-0.9-2.5 1.5-6.8-7.5-1.8-4.3-3.7-2.3 0.5-1-3-4.4-3.1-3.2-7.9-0.9 1.5-4-12 2.8-9.4-0.4-3.1 3.2-2.1-1.1-3.6 3.2-3 6-2.5 1.2-4 6.4-1-2.4-5.9 1.4-7.3-1.8-3.7 1-1.6-5.7-1.8-3.4 0.8-1-3.3-1.4-0.4-0.6-2.9 5.4-4.5 1.3-3-6.3-1.2-0.8-1.7 1-0.5-5.2-8.2-4.9 0.3 0.1-3.2-2.6-6.6 2.1-0.5 1-4.4 3.4-1.2 0.8-6-4.3-2.7 0.5-4.9-5.3-0.8-3.7 1.2-1.1-8.7 2-0.9 3.9 2.6 3.4-4.7 11.9-8-1.4-1.5 2.3-1.5-1.6-0.6-3.6 2.2-1.6-1 0-4.6 6.9-11.4 3.3-2.8 1.3-4.7-1-1.3 0.7-12.3-6.9-10 1.2-5.4-6-4.4 0.4-3.8-4.9-2.1 2-4.8-2-4.5 10.7-3.6 4.9 1.5-4.1-5 2.4-1.2 6.5 4.2 2.7-0.3 1.5 2.3 3.7 1.3 0.1 1.7 2.6-0.6-0.1-3.6 5.7 1.6 4.7-2.9 14.9-2.9 4.8-5.6 0-2.1-8.9-6.6 0.5-2.8 2.2-0.6 2.8-3.9 5.6-0.9 3.9-5.1 3-1.1 7.1-6.8-0.9-5.5 2.8-8.8-4.6-9.8-5.1 1.9-10.6-2.9-3.1-2.6-1.2-7.4 5-1.8-2.6-2.7 0-4 2.5-1.6 8.7-0.4 10.2 2.2 1.3-2.3 2.5-19.6 7.2-11.8 2.4-7.2-0.8-15.6 2-11.8 2.7-3.1-0.2-2.6 1.8-2.5 2.4-0.4 4.9 2.6-4.1-3.5-15-1.1-2.4-1.4-0.6-11.3 2.2-6.8 0.6-1 3.9 0.7 1.6-2.2-3.4-4.2 0.9-2.7 8.6-7.3 5.3-1.8 10.1-0.8 2.9 1.9 29.6-3.9 5.2 1.7-0.5 4 1.7 4 4.8 5.2-0.6 2.1 3.2 2.2-0.9 3-6.5 2.5 1.7 5.1 4.3 0.9 3.4 4.4 3.6-0.1 3.9-6.9 0.4-3.8-1.8-2.3-4 0.4 2.8-9.7 3.9-0.5 3.7 4.7 10.8 3.9-4.2 6.3-0.5 9.5 1.1 4.8-0.1-13 4-4.7 0.6-3.2-3.6-8.8-1.3-8.4 6.1-17.4 5.2-3.7 8.5 5.7 5.1 1.3 10.2-0.9 6.5-2.6 10.5 0.1 8.3 13.7 5.4 5.9 5.3 10.1 8.6 4.8 8.9 1.5-3.3-2.9-5.9-0.8-7.2-4.1-2.1-2.4-1.2-8.5-6.3-4.3-2.6-9.1-9-6.2-10.7-0.9-4.2-1.7-1.4 1.6-1.5-0.5-4.5-4.5-5.2-11.1 3.8-1.2 5.5 2.7 1.4-0.7 1.5-7.5-2.8-3.4 0.2-1.9-1.6-0.5-6.1 2.6-3-6.2 2.3-5.8-0.4-2.5 5.4-2 2.7-4.8-5.3 3 0.4-1.5-9.7 1-3.9 2.6-3.6-2.4-1.4-5.3 2.7-1.9 3.6-0.3-2.9-2.2-1.1 1.4 1-4.6 15.4-1 7.8-5.2 0.5-4.5-8.4-11.2-3.5 0.8 1.8-2.3-2.9-7.6-7.4-5.5 0.4-2.7-3.1-9.4 1.2-5.8 4.6 0.9 5.9-1.8 5.1 1 18.5 5.5 3.1 5.5 5.7-0.2 3.4-1.7 4.2 1 8.3-6.1 0.2 4 9.7 2.6 4.3 2.6 1.4 3 4.6-1.4-0.3-2.3 3.6 1.2 4.6 8.7-3.1 0-2.6 3 4.3-0.5 2.7 1.3-0.8 11.9-3.9 6-8.1 3.4 2.8 1.5 16.8-3.3 3.9 3.4-1.3 1.3 0.5 5.8-3.2 2.7 0 5.4 2.5-1.9 2.8-8.1 6.1-3.3 2.7 0.2 15.6 8.4 5.9 5.4 2.6 0.6 3-0.6 10.2-7.2 5.4-0.8-2.8 0.8 3.8 1.2 4.5-2.9 2 0.8-3 3.8-1.4-0.7 2.1 14.8-1.8 4.6-11.2 9.2-5.7 0.9-3.4 5 3.2 5.5 4.9 0.5 0.8 3.5 2.4 0.3 8.4-5.4 8-1.2 1.9 3.2 2.7 1.1-0.1 3.7 3.4 0.5 2.6-2.4 8.2 6 0.8-7 1.6-1.5-0.4-2.2 2.2-2.3-0.5-1.6 3.5-0.3 3.3-5.1-0.1-2.9 4.1-5.1 8 0.4 17.8-5.4 0.5 8.9 1.6 1.7-1.5-5.2 3-4-2.6 0.8 0.4-1 4.9-6.9 9.9-7 9.7-19.2 2-1.7 1 2.9 2.8 1 18.7-0.4 2.7 1.6-3.2 2-11.4-1.4-1.5 1.5-3-0.2-1 1.9-1.7 0.1 0.9 2.2-2.7-0.4-1.6 2.1-2.4-1.9-3.4 0.4-0.8 4.8-1.8-0.7-0.2 3.4-1.8 1.2 0.5 4.6 2.9 1.9 3-0.6-3.2-1.4 3.9-4.7 0.3-2.4 5.2-3.2-1.3-1.4 2.1-0.9 1.3 1.1 5.3-2.8 0.2-2.1 2.3-0.4-2 4.7 1.3-3.1 2 2.3 3.3-1.6 2.2 3.7 2.3-0.1 4.6-6.5 5-2.8 1.7 0.4-0.2 3.6 4.5 3.1-1.2 2.2 2.9 7.9 3.6 0.3-1.7 1.6 9.6 2.7 4.2 5.8-1.7 1.6 5.3-1-1.3 2.4 2.3-0.2 3.4 6 2.7 0.7-2-3.7 8-2 6.9-4.2-0.8 2.3 7.5 5.1-2 6.6-1.7 1.1 6.4 5.3 0.8 3.4 4 2.2-5.5 6.5 0.1 2.8 15 10 9.9 1.2 3-2.6 2 1-0.1 2.6-3 2.4 3.4 0.7 4 21.1 4.1 7 4 18.1 2.7 2.9-3.6 7.1 0.9 6.4-2.5 9.4-12.7 10.6 1.8 7.1-2.4 5.2 18 11.4 3.7 5.6 15.1 11.2-2 6.7 1.7 2.3-2.4 5.4-3.3 3.3 0.7 4.4 3.5 7.9-0.3 2 8 3.7 1.1 1.9-0.9 12.8 5.4 4.6-3.6 9.7-0.3 9-5.7 8.1-0.4 4.3 3.5 3.7 1.4 5.6 6.2 6.2 1.1 5.7-1.8 4.5 0 4.1 14.4 5.9 3.3 3.5 1.1 11.1 4.6 6.8 0.4 3.8-5.9 27.7-3 8.6-3.2 4.7-1.3 6.4-3.3 1.3-8.9-3.Line truncated
"M577.2 130.4l-2 3.6 1.8 3.1-7.4 1.3-2.6-0.9-3.2 3.1-3.7 1.2-6.5-0.3-1.3-1.6 7-4.2 0.3-3.2-2.7-4.5 0-2.8 4.5 0.4-0.6 5.4 1.7-2.4 2.7-0.1-0.8 1.6 2.2 0.6-0.2-7.1-1-1.8-1.5 0.7-2.3-4.4-2.4-1-1.9 1.2 0.7 2.2-2.1 3.1-2.2 1 1.1-3.9-0.9-1.5-4.3 3.8-1.4-0.4 2.2-8.9-4.3-5.5 0.5-1.3 2.2-1.7 1.2 0.5 4.8 7.6 9 4.3z",
"M382.9 77.6l3.9 7.2-8.1-1.3-1.2 0.7 0.6 0.7-4.3-0.1-1-4-4.2 0.5-0.5-1.2-1.1 0.4 1.5-6 2.5-2.5 3.1-0.6 5.6 2.3z",
"M541.2 81.8l4.6 5-2.4 7.6-2.1-0.5 1.3-2.3-3.9 0.9 3.7-4-6.1 1.8 4.7-3.2-1.8-0.9-11.7 2.3-7.6 7.3-0.1 3 3.9-2.1-0.5 4.2-6.3-2.2 1.3-3.7-4.2 3.2-3.8-2-0.5-2.3-3.5 0.9 1.7-2.3-4.2-2.9 2-4.5 7.6-1.3-3.8-4.8-3.5 0.6 7.2-6.5-2.6-1.1-0.2-2.5-3 1-2.9-3.7 7.5-1.6 2.6 3.7 0-3.4 4.3-4.1-1 6.9 2.2-1-0.6-3.6 3.8 7.8 5 0.2 0.7-3.1-1.6-3.7 1.3-2.4-5 1.8-5-5.9-7.2 5.6 2.2-8.7-4.4 1 1-2.8 11.2-3.7 1.8 1.5-3.8 4.4 2.9 5.5 13.9-0.7 2.6 4.9-5.6 6.3-0.2 3.5 2.2 4.5z",
"M205.4 46.4l3.5 0.3 2.6 3-1.9 4.5-5.2-0.2-5.7-3.1 2.7-3.7z",
"M197.1 24.9l-0.5 4 2.1 2.1 3.1 0.8 13.9-0.4-1.6 1.2-8-0.6-3.8 2.2-4.4-0.1-2.1-1.5-2.4 1.5-1.7 13.6 1.3-18.9 5.1-13.5 2.8-3.3 3 1.7-4.4 0.7 1.7 0.2 0.5 1.3-4.4 4.8z",
];
export const KARTE_BREITE = 663;
export const KARTE_HOEHE = 892;
/** Grad -> SVG-Koordinaten in der viewBox 0 0 663 892 (Formel aus docs/relaunch/README.md). */
const R = 6371;
const LON0 = (10 * Math.PI) / 180;
const LAT0 = (52 * Math.PI) / 180;
// eslint-disable-next-line react-refresh/only-export-components -- Hilfsfunktion und Daten gehören zur Karte (Spezifikation 9.3)
export function project(lon: number, lat: number): [number, number] {
const l = (lon * Math.PI) / 180;
const p = (lat * Math.PI) / 180;
const k = Math.sqrt(2 / (1 + Math.sin(LAT0) * Math.sin(p) + Math.cos(LAT0) * Math.cos(p) * Math.cos(l - LON0)));
const x = R * k * Math.cos(p) * Math.sin(l - LON0);
const y = R * k * (Math.cos(LAT0) * Math.sin(p) - Math.sin(LAT0) * Math.cos(p) * Math.cos(l - LON0));
return [x + 289.794 + 12, 341.92 - y + 12];
}
export type KartenPunkt = {
/** Position in der viewBox (siehe project) */
cx: number;
cy: number;
/** server = roter Punkt mit Ring, buero = hohler Punkt */
art: "server" | "buero";
label: string;
sublabel?: string;
};
/** Serverstandort Hetzner Falkenstein/Vogtland (50,477 N, 12,371 O), nur Website und Kundenportal. */
// eslint-disable-next-line react-refresh/only-export-components -- siehe project()
export const FALKENSTEIN: KartenPunkt = {
cx: 469.6,
cy: 520.5,
art: "server",
label: "Falkenstein, Sachsen",
sublabel: "Hier laufen Ihre Website und das Kundenportal.",
};
type DeutschlandKarteProps = {
punkte?: KartenPunkt[];
className?: string;
};
/**
* Ablauf der Bewegung: aus = alles sofort sichtbar (reduzierte Bewegung, Grundzustand),
* wartet = versteckt, bis die Karte im Bild ist, laeuft = Umriss zeichnet sich, dann Punkt, Ring, Beschriftung.
* Die Animationen selbst stehen in deutschland-karte.css hinter prefers-reduced-motion: no-preference.
*/
type Lauf = "aus" | "wartet" | "laeuft";
const DeutschlandKarte = ({ punkte = [FALKENSTEIN], className }: DeutschlandKarteProps) => {
const wurzel = useRef<HTMLDivElement>(null);
const [lauf, setLauf] = useState<Lauf>("aus");
// Vor dem ersten Zeichnen entscheiden, damit die fertige Karte nicht kurz aufblitzt
useLayoutEffect(() => {
if (typeof window === "undefined") return;
const reduziert = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
if (reduziert) return;
const el = wurzel.current;
if (!el || typeof IntersectionObserver === "undefined") {
setLauf("laeuft");
return;
}
setLauf("wartet");
const beobachter = new IntersectionObserver(
([eintrag]) => {
if (eintrag?.isIntersecting) {
setLauf("laeuft");
beobachter.disconnect();
}
},
{ threshold: 0.3 },
);
beobachter.observe(el);
return () => beobachter.disconnect();
}, []);
return (
<div ref={wurzel} className={`wk-karte${className ? ` ${className}` : ""}`} data-lauf={lauf}>
{/* Dekoration: die Aussage steht als Text daneben */}
<svg className="wk-karte__svg" viewBox={`0 0 ${KARTE_BREITE} ${KARTE_HOEHE}`} aria-hidden="true" focusable="false">
<g className="wk-karte__umriss">
{UMRISS.map((d, i) => (
<path key={i} d={d} pathLength={1} />
))}
</g>
{punkte.map((p) => (
<g key={`${p.cx}-${p.cy}`} className={`wk-karte__punkt wk-karte__punkt--${p.art}`}>
{p.art === "server" && <circle className="wk-karte__ring" cx={p.cx} cy={p.cy} r={7} />}
<circle className="wk-karte__kern" cx={p.cx} cy={p.cy} r={7} />
</g>
))}
</svg>
{punkte.map((p) => (
<div
key={`${p.cx}-${p.cy}`}
className="wk-karte__label"
style={{ left: `${(p.cx / KARTE_BREITE) * 100}%`, top: `${(p.cy / KARTE_HOEHE) * 100}%` }}
>
<p className="wk-karte__titel">{p.label}</p>
{p.sublabel && <p className="wk-karte__zusatz">{p.sublabel}</p>}
</div>
))}
</div>
);
};
export default DeutschlandKarte;
-74
View File
@@ -1,74 +0,0 @@
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { ArrowRight } from "lucide-react";
import CountUp from "@/components/CountUp";
const DifferentiationSection = () => {
return (
<section className="py-24 md:py-32 bg-secondary/20 relative">
<div className="container mx-auto px-6">
<div className="max-w-4xl mx-auto text-center">
<div className="label-tag mb-4">Der Unterschied</div>
<h2 className="text-4xl md:text-5xl lg:text-6xl font-display font-medium text-foreground tracking-tight uppercase mb-12">
Warum Unternehmen zu uns wechseln.
</h2>
<div className="grid md:grid-cols-3 gap-8 mb-12">
<div className="p-6 border border-border rounded-lg bg-background">
<div className="text-4xl font-display font-medium text-foreground mb-2">
<CountUp from={0} to={1} duration={1} padMinLength={2} startWhen={true} />
</div>
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
Alles aus einer Hand
</h3>
<p className="text-muted-foreground text-sm">
Website, Automation und Schnittstellen aus einer Hand – ein Ansprechpartner statt fünf Agenturen.
</p>
</div>
<div className="p-6 border border-border rounded-lg bg-background">
<div className="text-4xl font-display font-medium text-foreground mb-2">
<CountUp from={0} to={2} duration={1} padMinLength={2} startWhen={true} />
</div>
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
Systeme statt Inseln
</h3>
<p className="text-muted-foreground text-sm">
CRM, Formulare und Website tauschen Daten – ohne manuelles Nachpflegen.
</p>
</div>
<div className="p-6 border border-border rounded-lg bg-background">
<div className="text-4xl font-display font-medium text-foreground mb-2">
<CountUp from={0} to={3} duration={1} padMinLength={2} startWhen={true} />
</div>
<h3 className="text-lg font-display font-medium text-foreground uppercase tracking-tight mb-2">
Langfristige Partnerschaft
</h3>
<p className="text-muted-foreground text-sm">
Nach dem Go-live bleiben wir dran: Optimierung, wenn Ihr Team und Umsatz wachsen.
</p>
</div>
</div>
<Link
to="/kontakt"
data-analytics-cta="Potenzialanalyse starten"
data-analytics-location="differentiation_section"
>
<Button
size="lg"
className="btn-minimal rounded-full px-8 py-6 text-base font-medium group"
>
Potenzialanalyse starten
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</div>
</div>
</section>
);
};
export default DifferentiationSection;
+2 -1
View File
@@ -25,13 +25,14 @@ const footerLinks: { title: string; links: FooterLink[] }[] = [
{ id: 3, title: "Leistungen", href: "/#services" }, { id: 3, title: "Leistungen", href: "/#services" },
{ id: 4, title: "Kontakt", to: "/kontakt" }, { id: 4, title: "Kontakt", to: "/kontakt" },
{ id: 9, title: "Wartungs-Abo", to: "/wartungs-abo" }, { id: 9, title: "Wartungs-Abo", to: "/wartungs-abo" },
{ id: 13, title: "Hosting-Abo", to: "/hosting-abo" },
], ],
}, },
{ {
title: "Kontakt", title: "Kontakt",
links: [ links: [
{ id: 5, title: "support@webklar.com", href: "mailto:support@webklar.com" }, { id: 5, title: "support@webklar.com", href: "mailto:support@webklar.com" },
{ id: 6, title: "0170 4969375", href: "tel:+491704969375" }, { id: 6, title: "0176 23726355", href: "tel:+4917623726355" },
], ],
}, },
{ {
+77
View File
@@ -0,0 +1,77 @@
import type { ReactNode } from "react";
import { MacBookFrame, IPhoneFrame } from "./device-frames/DeviceFrames";
import "./geraete-rahmen.css";
/** Ein Bild samt Größenangaben. Alle Felder landen 1:1 im <img> (Spezifikation 9.5). */
export type RahmenBildDaten = {
src: string;
srcSet: string;
sizes: string;
/** Breite und Höhe des größten Rohbilds: sie legen nur das Seitenverhältnis fest, damit nichts springt. */
width: number;
height: number;
alt: string;
/** Standard: lazy. Nur Bilder im ersten Bildschirm bekommen "eager". */
loading?: "lazy" | "eager";
};
type BildProps = {
bild: RahmenBildDaten;
className?: string;
};
/** Das <img> eines Rahmens: immer mit Alt-Text, fester Größe, srcset und asynchroner Dekodierung. */
export const RahmenBild = ({ bild, className }: BildProps) => (
<img
className={className}
src={bild.src}
srcSet={bild.srcSet}
sizes={bild.sizes}
width={bild.width}
height={bild.height}
alt={bild.alt}
loading={bild.loading ?? "lazy"}
decoding="async"
/>
);
type RahmenProps = {
/** Ein einzelnes Bild. Für überblendete Bilder stattdessen children (mehrere Ebenen) übergeben. */
bild?: RahmenBildDaten;
children?: ReactNode;
className?: string;
};
type FensterProps = RahmenProps & {
/** Schlichter Adresstext in der Leiste, keine Ampelpunkte. */
adresse?: string;
};
/**
* Desktop: MacBook Pro 14" mit Safari und der Adresse project.webklar.com (Rahmen aus
* components/device-frames, übernommen aus KNSONWS/homepage „device-mockups“). Der Inhalt wird
* in 1512 x 893 gesetzt und verkleinert; der Screenshot füllt die Fläche von oben.
*/
export const DesktopFenster = ({ bild, children, className, adresse = "project.webklar.com" }: FensterProps) => (
<MacBookFrame url={adresse} className={className}>
<div className="wk-geraet-inhalt">{bild ? <RahmenBild bild={bild} /> : children}</div>
</MacBookFrame>
);
/**
* Handy: iPhone 16 Pro (Inhalt in 402 x 874). Die Portal-Aufnahmen sind genau in diesem Format
* mit Safe Area 59 / 34 aufgenommen, Statusleiste und Home-Balken liegen also passend darüber.
*/
export const HandyRahmen = ({ bild, children, className }: RahmenProps) => (
<IPhoneFrame className={className}>
<div className="wk-geraet-inhalt wk-geraet-inhalt--handy">{bild ? <RahmenBild bild={bild} /> : children}</div>
</IPhoneFrame>
);
type GeraeteRahmenProps = (FensterProps & { art: "desktop" }) | (RahmenProps & { art: "handy" });
/** Ein Einstieg für beide Rahmen: <GeraeteRahmen art="desktop" .../> oder art="handy". */
const GeraeteRahmen = (props: GeraeteRahmenProps) =>
props.art === "handy" ? <HandyRahmen {...props} /> : <DesktopFenster {...props} />;
export default GeraeteRahmen;
+4 -5
View File
@@ -20,7 +20,6 @@ const Header = () => {
const navItems = [ const navItems = [
{ name: "Über uns", link: "/ueber-uns" }, { name: "Über uns", link: "/ueber-uns" },
{ name: "Leistungen", link: "#services" }, { name: "Leistungen", link: "#services" },
{ name: "Projekte", link: "#projects" },
{ name: "Ablauf", link: "#process" }, { name: "Ablauf", link: "#process" },
]; ];
@@ -33,8 +32,8 @@ const Header = () => {
<NavBody> <NavBody>
<NavbarLogo href="#"> <NavbarLogo href="#">
<Logo width={30} height={30} /> <Logo width={30} height={30} />
<span className="font-display text-lg font-medium tracking-tight"> <span className="font-display text-lg font-medium tracking-normal">
Webklar WEBklar
</span> </span>
</NavbarLogo> </NavbarLogo>
<NavItems items={navItems} /> <NavItems items={navItems} />
@@ -60,8 +59,8 @@ const Header = () => {
<MobileNavHeader> <MobileNavHeader>
<NavbarLogo href="#"> <NavbarLogo href="#">
<Logo width={30} height={30} /> <Logo width={30} height={30} />
<span className="font-display text-lg font-medium tracking-tight"> <span className="font-display text-lg font-medium tracking-normal">
Webklar WEBklar
</span> </span>
</NavbarLogo> </NavbarLogo>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
+14 -116
View File
@@ -1,25 +1,12 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import React, { useState, useEffect, useRef, lazy, Suspense } from "react"; import { lazy, Suspense } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useQuery } from "@tanstack/react-query";
import CountUp from "@/components/CountUp";
import { trackCtaClick } from "@/lib/analytics"; import { trackCtaClick } from "@/lib/analytics";
import { fetchProjects } from "@/lib/projects";
// WebGL-Effekte als eigene Chunks laden, damit der Text nicht auf three.js/ogl warten muss // WebGL-Effekte als eigene Chunks laden, damit der Text nicht auf three.js/ogl warten muss
const Silk = lazy(() => import("@/components/Silk")); const Silk = lazy(() => import("@/components/Silk"));
const SPARKLE_SVG = (
<svg className="btn-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"
/>
</svg>
);
function DemoButtonLetters({ text }: { text: string }) { function DemoButtonLetters({ text }: { text: string }) {
const chars = text.split(""); const chars = text.split("");
return ( return (
@@ -33,49 +20,16 @@ function DemoButtonLetters({ text }: { text: string }) {
); );
} }
const FOUNDING_DATE = new Date("2026-01-25"); // Samstag, 25. Januar 2026 // Drei belegbare Fakten statt Zahlen: Wort-Werte, deshalb kleiner gesetzt als die frühere Zahlenleiste.
const FAKTEN = [
{ wert: "Falkenstein", label: "Server in Deutschland" },
{ wert: "Live-Vorschau", label: "im Kundenportal" },
{ wert: "ab 20 €", label: "Hosting im Monat" },
];
const Hero = () => { const Hero = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const [companyAge, setCompanyAge] = useState("");
const secondBtnRef = useRef<HTMLButtonElement>(null);
// Projektzähler kommt live aus Gitea – gleicher queryKey wie im Abschnitt
// „Projekte", react-query teilt sich den einen Request.
const { data: projectsSnapshot } = useQuery({
queryKey: ["projects"],
queryFn: ({ signal }) => fetchProjects(signal),
staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false,
retry: 1,
});
const projectCount = projectsSnapshot?.total ?? 10;
useEffect(() => {
const calculateAge = () => {
const now = new Date();
const diff = now.getTime() - FOUNDING_DATE.getTime();
const totalSeconds = Math.floor(diff / 1000);
const days = Math.floor(totalSeconds / (60 * 60 * 24));
const hours = Math.floor((totalSeconds % (60 * 60 * 24)) / (60 * 60));
const years = Math.floor(days / 365);
const remainingDays = days % 365;
if (years > 0) {
setCompanyAge(`${years}J ${remainingDays}T ${hours}h`);
} else {
setCompanyAge(`${days}T ${hours}h`);
}
};
calculateAge();
const interval = setInterval(calculateAge, 60 * 60 * 1000); // Update every hour (only days/hours shown)
return () => clearInterval(interval);
}, []);
// ── Silk-Hintergrund Farben ── // ── Silk-Hintergrund Farben ──
const isDark = resolvedTheme === "dark"; const isDark = resolvedTheme === "dark";
@@ -149,32 +103,6 @@ const Hero = () => {
</div> </div>
</button> </button>
</div> </div>
<div className="btn-wrapper w-full sm:w-auto">
<button
ref={secondBtnRef}
type="button"
className="btn w-full sm:w-auto justify-center"
onClick={() => {
const el = document.getElementById("projects");
if (el) el.scrollIntoView({ behavior: "smooth" });
else navigate("/#projects");
}}
aria-label="Projekte ansehen"
>
{SPARKLE_SVG}
<div className="txt-wrapper">
<span className="txt-width-helper" aria-hidden="true">
<DemoButtonLetters text="Projekte ansehen" />
</span>
<div className="txt-1">
<DemoButtonLetters text="Projekte ansehen" />
</div>
<div className="txt-2">
<DemoButtonLetters text="Wird geladen..." />
</div>
</div>
</button>
</div>
</div> </div>
{/* Trust Line */} {/* Trust Line */}
@@ -184,46 +112,16 @@ const Hero = () => {
</div> </div>
</div> </div>
{/* Bottom Stats Row */} {/* Bottom Facts Row */}
<div className="container mx-auto px-6 mt-auto pb-12 pt-20 relative z-10"> <div className="container mx-auto px-6 mt-auto pb-12 pt-20 relative z-10">
<div className="divider mb-12" /> <div className="divider mb-12" />
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
<div className="animate-fade-in" style={{ animationDelay: '0.6s' }}> {FAKTEN.map((fakt, i) => (
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2"> <div key={fakt.wert} className="animate-fade-in" style={{ animationDelay: `${0.6 + i * 0.1}s` }}>
<CountUp <div className="stat-number text-3xl lg:text-4xl xl:text-5xl text-foreground mb-2">{fakt.wert}</div>
from={0} <div className="label-tag">{fakt.label}</div>
to={projectCount}
direction="up"
duration={1.2}
className="count-up-text"
startWhen={true}
suffix="+"
/>
</div> </div>
<div className="label-tag">Projekte</div> ))}
</div>
<div className="animate-fade-in" style={{ animationDelay: '0.7s' }}>
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">{companyAge}</div>
<div className="label-tag">Am Markt</div>
</div>
<div className="animate-fade-in" style={{ animationDelay: '0.8s' }}>
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">
<CountUp
from={0}
to={99.9}
direction="up"
duration={1.5}
className="count-up-text"
startWhen={true}
suffix="%"
/>
</div>
<div className="label-tag">Systemverfügbarkeit</div>
</div>
<div className="animate-fade-in" style={{ animationDelay: '0.9s' }}>
<div className="stat-number text-4xl md:text-5xl text-foreground mb-2">🇩🇪</div>
<div className="label-tag">Serverstandort in Deutschland</div>
</div>
</div> </div>
</div> </div>
</section> </section>
+2 -2
View File
@@ -58,9 +58,9 @@ const ProblemSection = () => {
</Suspense> </Suspense>
</div> </div>
)} )}
{/* Hintergrundbild: nur Blitze rechts, auf Handy maximale Breite */} {/* Hintergrundbild: nur Blitze rechts, auf Handy maximale Breite; nur im Dark Mode (dunkles Bild wäre im hellen ein graues Quadrat) */}
<div <div
className="problem-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3] z-0" className="problem-section-bg hidden dark:block absolute inset-0 bg-right bg-no-repeat opacity-[0.3] z-0"
style={{ style={{
backgroundImage: "url(/problem_blitz.jpg)", backgroundImage: "url(/problem_blitz.jpg)",
}} }}
+1 -1
View File
@@ -25,7 +25,7 @@ const Process = () => {
{ {
number: 4, number: 4,
title: "Launch & Betreuung", title: "Launch & Betreuung",
description: "Go-live mit Tests und Übergabe. Danach Updates, wenn sich Ihr Unternehmen weiterentwickelt.", description: "Go-live mit Tests und Übergabe. Pflege und Updates danach im Wartungs-Abo.",
}, },
]; ];
-110
View File
@@ -1,110 +0,0 @@
import { ArrowUpRight } from "lucide-react";
import { useTheme } from "next-themes";
import BorderGlow from "@/components/BorderGlow";
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
type Project = {
title: string;
description: string;
image: string;
url: string;
};
const projects: Project[] = [
{
title: "Email Sorter",
description: "Automatisierung · E-Mail-Workflows für Teams",
image: "/project%20pics/emailsorter.webp",
url: "https://emailsorter.webklar.com/",
},
{
title: "Neutral",
description: "Website · Markenauftritt & Custom Development",
image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=600&fit=crop",
url: "#",
},
{
title: "Verbatim Labs",
description: "Website · UI/UX & individuelle Entwicklung",
image: "https://images.unsplash.com/photo-1559028012-481c04fa702d?w=800&h=600&fit=crop",
url: "#",
},
{
title: "JMK Engineers",
description: "Website · Technische Präsentation & Lead-Generierung",
image: "https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=800&h=600&fit=crop",
url: "#",
},
{
title: "GOODZ Club",
description: "Website · Mehrsprachig & skalierbare Plattform",
image: "https://images.unsplash.com/photo-1542744094-3a31f272c490?w=800&h=600&fit=crop",
url: "#",
},
];
const ProjectShowcase = () => {
const { resolvedTheme } = useTheme();
const cardBg = resolvedTheme === "dark" ? "hsl(0 0% 6%)" : "hsl(0 0% 96%)";
return (
<section id="projects" className="pt-8 pb-24 md:pt-12 md:pb-32 bg-background relative">
{/* TextHoverEffect */}
<div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10">
<TextHoverEffect text="Projekte" />
</div>
<div className="container mx-auto px-6">
{/* Projects Grid */}
<div className="space-y-2">
{projects.map((project, index) => (
<BorderGlow
key={project.title}
edgeSensitivity={30}
glowColor="40 80 80"
backgroundColor={cardBg}
borderRadius={8}
glowRadius={30}
glowIntensity={0.8}
coneSpread={25}
colors={['#c084fc', '#f472b6', '#38bdf8']}
>
<a
href={project.url}
target={project.url.startsWith("http") ? "_blank" : undefined}
rel={project.url.startsWith("http") ? "noopener noreferrer" : undefined}
className="group block p-6 md:p-8"
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex-1">
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground mb-2 group-hover:text-muted-foreground transition-colors uppercase tracking-tight">
{project.title}
</h3>
<p className="text-muted-foreground text-sm uppercase tracking-wider">
{project.description}
</p>
</div>
<div className="flex items-center gap-4">
<div className="w-24 h-16 md:w-32 md:h-20 rounded overflow-hidden opacity-0 group-hover:opacity-100 transition-opacity duration-500">
<img
src={project.image}
alt={project.title}
loading="lazy"
decoding="async"
className="w-full h-full object-cover"
/>
</div>
<ArrowUpRight className="w-6 h-6 text-muted-foreground group-hover:text-foreground transition-colors" />
</div>
</div>
</a>
</BorderGlow>
))}
</div>
</div>
</section>
);
};
export default ProjectShowcase;
+4 -4
View File
@@ -1,4 +1,4 @@
import { Lightbulb, Palette, Code, Search } from "lucide-react"; import { Lightbulb, Palette, Code, Server } from "lucide-react";
const Services = () => { const Services = () => {
const services = [ const services = [
@@ -21,9 +21,9 @@ const Services = () => {
number: "03", number: "03",
}, },
{ {
icon: Search, icon: Server,
title: "SEO & Betreuung", title: "Hosting & Betreuung",
description: "Sichtbar bei Google, stabil im Betrieb – mit Updates, wenn Ihr Unternehmen wächst.", description: "Hosting auf unserem Server in Deutschland, Pflege und Updates im Wartungs-Abo.",
number: "04", number: "04",
}, },
]; ];
+3 -3
View File
@@ -47,9 +47,9 @@ const SolutionSection = () => {
</Suspense> </Suspense>
</div> </div>
)} )}
{/* Hintergrundbild mittig, auf Handy maximale Breite */} {/* Hintergrundbild mittig, auf Handy maximale Breite; nur im Dark Mode (dunkles Bild wäre im hellen ein graues Quadrat) */}
<div <div
className="solution-section-bg absolute inset-0 bg-center bg-no-repeat opacity-[0.3] z-0" className="solution-section-bg hidden dark:block absolute inset-0 bg-center bg-no-repeat opacity-[0.3] z-0"
style={{ style={{
backgroundImage: "url(/loesung.jpg)", backgroundImage: "url(/loesung.jpg)",
}} }}
@@ -121,7 +121,7 @@ const SolutionSection = () => {
<div className="space-y-6"> <div className="space-y-6">
<div className="text-sm uppercase tracking-wider text-muted-foreground">Das Ergebnis</div> <div className="text-sm uppercase tracking-wider text-muted-foreground">Das Ergebnis</div>
<h3 className="text-2xl md:text-3xl font-display font-medium text-foreground uppercase tracking-tight"> <h3 className="text-2xl md:text-3xl font-display font-medium text-foreground uppercase tracking-tight">
Sie sehen, was läuft – und was Umsatz bringt. Mehr Zeit für Kunden statt für Tool-Pflege: Sie sehen, was läuft. Mehr Zeit für Kunden statt für Tool-Pflege:
</h3> </h3>
<div className="text-4xl md:text-5xl font-display font-medium text-primary uppercase tracking-tight"> <div className="text-4xl md:text-5xl font-display font-medium text-primary uppercase tracking-tight">
Kontrolle. Kontrolle.
+7 -6
View File
@@ -1,4 +1,4 @@
import { Users, Cog, MessageSquare, Target, BarChart3, Layers } from "lucide-react"; import { Users, Cog, MessageSquare, MonitorSmartphone, BarChart3, Layers } from "lucide-react";
import BorderGlow from "@/components/BorderGlow"; import BorderGlow from "@/components/BorderGlow";
import { TextHoverEffect } from "@/components/ui/text-hover-effect"; import { TextHoverEffect } from "@/components/ui/text-hover-effect";
@@ -20,9 +20,9 @@ const Values = () => {
description: "Jeder Kanal landet im System – mit Status, Zuständigkeit und Historie.", description: "Jeder Kanal landet im System – mit Status, Zuständigkeit und Historie.",
}, },
{ {
icon: Target, icon: MonitorSmartphone,
title: "Planbare Neukunden", title: "Kundenportal",
description: "Website und Prozesse ziehen an einer Schnur – Sie sehen, was funktioniert.", description: "Fortschritt, Chat und Rechnungen an einem Ort, im Browser oder als App.",
}, },
{ {
icon: BarChart3, icon: BarChart3,
@@ -42,9 +42,9 @@ const Values = () => {
<div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10"> <div className="h-[14rem] flex items-center justify-center -mb-4 relative z-10">
<TextHoverEffect text="Leistungen" /> <TextHoverEffect text="Leistungen" />
</div> </div>
{/* Hintergrundbild: auf Handy maximale Breite */} {/* Hintergrundbild: auf Handy maximale Breite; nur im Dark Mode (dunkles Bild wäre im hellen ein graues Quadrat) */}
<div <div
className="values-section-bg absolute inset-0 bg-right bg-no-repeat opacity-[0.3] pointer-events-none" className="values-section-bg hidden dark:block absolute inset-0 bg-right bg-no-repeat opacity-[0.3] pointer-events-none"
style={{ style={{
backgroundImage: "url(/backgroud_effect.webp)", backgroundImage: "url(/backgroud_effect.webp)",
}} }}
@@ -69,6 +69,7 @@ const Values = () => {
edgeSensitivity={30} edgeSensitivity={30}
glowColor="40 80 80" glowColor="40 80 80"
backgroundColor="hsl(0 0% 6%)" backgroundColor="hsl(0 0% 6%)"
lightBackgroundColor="hsl(0 0% 96%)"
borderRadius={8} borderRadius={8}
glowRadius={30} glowRadius={30}
glowIntensity={0.8} glowIntensity={0.8}
+56
View File
@@ -0,0 +1,56 @@
import { Children, useEffect, useRef, useState, type ReactNode } from "react";
import "./wisch-reihe.css";
type WischReiheProps = {
/** Beschreibung für Screenreader, z. B. „Vier Leistungen, seitlich wischen“ */
ariaLabel: string;
children: ReactNode;
className?: string;
/** Zähler „2 / 4“ unter der Reihe anzeigen (Standard: ja) */
zaehler?: boolean;
};
/**
* Horizontale Wisch-Reihe für Handys (Spezifikation 9.4): natives Scroll-Snap ohne Bibliothek,
* die nächste Karte ist angeschnitten sichtbar, darunter ein Zähler. Keine automatische Bewegung.
* Ab 900 px entscheidet der Abschnitt selbst, ob er die Reihe überhaupt zeigt.
*/
const WischReihe = ({ ariaLabel, children, className, zaehler = true }: WischReiheProps) => {
const karten = Children.toArray(children);
const reiheRef = useRef<HTMLUListElement>(null);
const [aktiv, setAktiv] = useState(0);
useEffect(() => {
const reihe = reiheRef.current;
if (!reihe || typeof IntersectionObserver === "undefined") return;
const beobachter = new IntersectionObserver(
(eintraege) => {
for (const e of eintraege) {
if (e.isIntersecting) setAktiv(Number((e.target as HTMLElement).dataset.index));
}
},
{ root: reihe, threshold: 0.6 },
);
reihe.querySelectorAll<HTMLElement>(":scope > li").forEach((li) => beobachter.observe(li));
return () => beobachter.disconnect();
}, [karten.length]);
return (
<div className={`wk-wisch${className ? ` ${className}` : ""}`}>
<ul ref={reiheRef} className="wk-wisch__reihe" role="list" tabIndex={0} aria-label={ariaLabel}>
{karten.map((karte, i) => (
<li key={i} className="wk-wisch__karte" data-index={i}>
{karte}
</li>
))}
</ul>
{zaehler && karten.length > 1 && (
<p className="wk-wisch__zaehler wk-num" aria-hidden>
{aktiv + 1} / {karten.length}
</p>
)}
</div>
);
};
export default WischReihe;
+169
View File
@@ -0,0 +1,169 @@
/* Deutschland-Karte (Spezifikation 7.1): ruhiger Umriss, roter Punkt auf Falkenstein */
.wk-karte {
position: relative;
width: 100%;
aspect-ratio: 663 / 892;
/* Strichstärke in viewBox-Einheiten, damit die Linie bei jeder Kartengröße etwa 1,25 px stark ist
(Desktop: Karte ca. 0,75 px je Einheit, Handy ca. 0,43 px je Einheit) */
--karte-strich: 1.7;
--karte-punkt: 7px;
/* Abstand zwischen Beschriftung und Punkt: Die tschechische Grenze (Ascher Zipfel) liegt knapp links unter dem Punkt */
--karte-abstand: 22px;
}
@media (max-width: 899.98px) {
.wk-karte {
--karte-strich: 2.9;
--karte-punkt: 10px;
--karte-abstand: 16px;
}
}
.wk-karte__svg {
display: block;
width: 100%;
height: 100%;
overflow: visible;
}
.wk-karte__umriss path {
fill: none;
stroke: var(--line-strong);
stroke-width: var(--karte-strich);
stroke-linejoin: round;
stroke-linecap: round;
}
.wk-karte__kern {
fill: var(--accent);
r: var(--karte-punkt);
transform-box: fill-box;
transform-origin: center;
}
/* Büro (optional, O8): hohler Punkt */
.wk-karte__punkt--buero .wk-karte__kern {
fill: var(--bg);
stroke: var(--accent);
stroke-width: 2.4;
}
/* Der Ring ist im Grundzustand unsichtbar: ohne Bewegung gibt es ihn nicht */
.wk-karte__ring {
fill: none;
stroke: var(--accent);
stroke-width: 1.2;
opacity: 0;
r: var(--karte-punkt);
transform-box: fill-box;
transform-origin: center;
}
/* Beschriftung als echter Text, links neben dem Punkt: dort liegt die leere Fläche innerhalb von
Deutschland. Rechts und darunter verläuft die tschechische Grenze dicht am Punkt. */
.wk-karte__label {
position: absolute;
width: max-content;
max-width: min(58%, 18rem);
text-align: right;
/* erste Zeile mittig auf Höhe des Punktes, rechter Rand links vom Punkt */
transform: translate(calc(-100% - var(--karte-abstand)), -0.72em);
pointer-events: none;
}
.wk-karte__titel {
font-size: 1rem;
line-height: 1.25;
font-weight: 600;
color: var(--fg);
letter-spacing: -0.005em;
text-wrap: balance;
}
.wk-karte__zusatz {
margin-top: 0.3rem;
font-size: var(--fs-small);
line-height: 1.4;
color: var(--fg-muted);
text-wrap: balance;
}
/* Handy: Die Karte ist klein, links vom Punkt wäre bis zur Westgrenze zu wenig Platz. Dort steht die
Beschriftung oberhalb des Punktes, über der leeren Fläche von Thüringen und Sachsen. */
@media (max-width: 899.98px) {
.wk-karte__label {
transform: translate(calc(-100% + 6px), calc(-100% - 14px));
}
}
@media (min-width: 900px) {
.wk-karte__titel {
font-size: 1.1875rem;
}
.wk-karte__zusatz {
font-size: 0.9375rem;
max-width: 24ch;
margin-left: auto;
}
}
/* ── Bewegung (nur ohne reduzierte Bewegung) ────────────────────────────────
Ablauf: Umriss zeichnet sich in 1,6 s, danach erscheinen Punkt und Beschriftung, dann pulsiert
ein Ring dreimal (2,4 s je Durchlauf) und bleibt dann weg. Ausnahmen laut Spezifikation 8.4.
Der Umriss wird über stroke-dashoffset gezeichnet (pathLength="1"). Das Muster „1 2“ mit Startversatz
1,06 lässt auch bei runden Linienenden nichts übrig, das vor dem Start als Pünktchen sichtbar wäre. */
@media (prefers-reduced-motion: no-preference) {
.wk-karte[data-lauf="wartet"] .wk-karte__umriss path,
.wk-karte[data-lauf="laeuft"] .wk-karte__umriss path {
stroke-dasharray: 1 2;
stroke-dashoffset: 1.06;
}
.wk-karte[data-lauf="wartet"] .wk-karte__kern,
.wk-karte[data-lauf="wartet"] .wk-karte__label {
opacity: 0;
}
.wk-karte[data-lauf="laeuft"] .wk-karte__umriss path {
animation: wk-karte-zeichnen 1600ms var(--ease) 150ms forwards;
}
.wk-karte[data-lauf="laeuft"] .wk-karte__kern {
animation: wk-karte-punkt 500ms var(--ease) 1750ms backwards;
}
.wk-karte[data-lauf="laeuft"] .wk-karte__label {
animation: wk-karte-label 600ms var(--ease) 1950ms backwards;
}
.wk-karte[data-lauf="laeuft"] .wk-karte__ring {
animation: wk-karte-ring 2400ms var(--ease) 2250ms 3 forwards;
}
}
@keyframes wk-karte-zeichnen {
from {
stroke-dashoffset: 1.06;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes wk-karte-punkt {
from {
opacity: 0;
transform: scale(0.3);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes wk-karte-label {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes wk-karte-ring {
from {
opacity: 0.55;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(2.6);
}
}
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more