diff --git a/public/fonts/instrument-sans/LICENSE b/public/fonts/instrument-sans/LICENSE
new file mode 100644
index 0000000..9a39a8e
--- /dev/null
+++ b/public/fonts/instrument-sans/LICENSE
@@ -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.
diff --git a/public/fonts/instrument-sans/instrument-sans-latin-ext-wght-normal.woff2 b/public/fonts/instrument-sans/instrument-sans-latin-ext-wght-normal.woff2
new file mode 100644
index 0000000..0ea0da0
Binary files /dev/null and b/public/fonts/instrument-sans/instrument-sans-latin-ext-wght-normal.woff2 differ
diff --git a/public/fonts/instrument-sans/instrument-sans-latin-wght-normal.woff2 b/public/fonts/instrument-sans/instrument-sans-latin-wght-normal.woff2
new file mode 100644
index 0000000..8611e41
Binary files /dev/null and b/public/fonts/instrument-sans/instrument-sans-latin-wght-normal.woff2 differ
diff --git a/src/components/home/HeroKarte.tsx b/src/components/home/HeroKarte.tsx
new file mode 100644
index 0000000..67c980f
--- /dev/null
+++ b/src/components/home/HeroKarte.tsx
@@ -0,0 +1,10 @@
+/** Platzhalter: wird im Relaunch-Workflow gebaut (Spezifikation Abschnitt 7). */
+const HeroKarte = () => (
+
+);
+
+export default HeroKarte;
diff --git a/src/components/home/KontaktBuehne.tsx b/src/components/home/KontaktBuehne.tsx
new file mode 100644
index 0000000..756c462
--- /dev/null
+++ b/src/components/home/KontaktBuehne.tsx
@@ -0,0 +1,32 @@
+import { Link } from "react-router-dom";
+
+/** Spezifikation 7.7: ein Satz, ein Button, E-Mail und Telefon. */
+const KontaktBuehne = () => (
+
+
+
+ Erzählen Sie uns, was Sie vorhaben.
+
+
+ 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.
+
+
+
+ Projekt besprechen
+
+
+
+
+ support@webklar.com
+
+ ·
+
+ 0176 23726355
+
+
+
+
+);
+
+export default KontaktBuehne;
diff --git a/src/components/home/Kundenportal.tsx b/src/components/home/Kundenportal.tsx
new file mode 100644
index 0000000..44d1c46
--- /dev/null
+++ b/src/components/home/Kundenportal.tsx
@@ -0,0 +1,10 @@
+/** Platzhalter: wird im Relaunch-Workflow gebaut (Spezifikation Abschnitt 7). */
+const Kundenportal = () => (
+
+);
+
+export default Kundenportal;
diff --git a/src/components/home/Leistungen.tsx b/src/components/home/Leistungen.tsx
new file mode 100644
index 0000000..4bc99aa
--- /dev/null
+++ b/src/components/home/Leistungen.tsx
@@ -0,0 +1,10 @@
+/** Platzhalter: wird im Relaunch-Workflow gebaut (Spezifikation Abschnitt 7). */
+const Leistungen = () => (
+
+);
+
+export default Leistungen;
diff --git a/src/components/home/Preise.tsx b/src/components/home/Preise.tsx
new file mode 100644
index 0000000..07c3615
--- /dev/null
+++ b/src/components/home/Preise.tsx
@@ -0,0 +1,10 @@
+/** Platzhalter: wird im Relaunch-Workflow gebaut (Spezifikation Abschnitt 7). */
+const Preise = () => (
+
+);
+
+export default Preise;
diff --git a/src/components/home/UeberUns.tsx b/src/components/home/UeberUns.tsx
new file mode 100644
index 0000000..51ff471
--- /dev/null
+++ b/src/components/home/UeberUns.tsx
@@ -0,0 +1,25 @@
+import { Link } from "react-router-dom";
+
+/** Spezifikation 7.6: wer dahinter steckt. Keine Fotos, bis echte Teamfotos vorliegen. */
+const UeberUns = () => (
+
+
+
+ Wer hinter WEBklar steckt.
+
+
+
+ Wir sind ein kleines Team aus Winnweiler in der Pfalz. Sie sprechen bei uns direkt mit den Leuten, die Ihre
+ Website bauen.
+
+
+
+ Mehr über uns
+
+
+
+
+
+);
+
+export default UeberUns;
diff --git a/src/components/home/home.css b/src/components/home/home.css
new file mode 100644
index 0000000..007878d
--- /dev/null
+++ b/src/components/home/home.css
@@ -0,0 +1,46 @@
+/* Kleine Abschnitte der Startseite: Über uns und Kontakt */
+
+.wk-ueber-uns {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
+ gap: 2rem 4rem;
+ align-items: start;
+}
+.wk-ueber-uns__link {
+ margin-top: 1.5rem;
+}
+@media (max-width: 899.98px) {
+ .wk-ueber-uns {
+ grid-template-columns: 1fr;
+ }
+}
+
+.wk-kontakt__inhalt {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+.wk-kontakt .wk-h2 {
+ max-width: 16ch;
+}
+.wk-kontakt .wk-lead {
+ max-width: 38rem;
+}
+.wk-kontakt__aktion {
+ margin-top: 2.25rem;
+}
+.wk-kontakt__wege {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: center;
+ gap: 0.25rem 0.9rem;
+ margin-top: 1.75rem;
+ color: var(--fg-muted);
+}
+.wk-kontakt__wege a {
+ display: inline-flex;
+ align-items: center;
+ min-height: 44px;
+}
diff --git a/src/components/site/SiteFooter.tsx b/src/components/site/SiteFooter.tsx
new file mode 100644
index 0000000..c7597c4
--- /dev/null
+++ b/src/components/site/SiteFooter.tsx
@@ -0,0 +1,78 @@
+import { Link } from "react-router-dom";
+import Logo from "@/components/Logo";
+import "./site.css";
+
+type FooterLink = { label: string; to?: string; href?: string; pflicht?: boolean };
+
+/** Spezifikation 7.8: Seiten und Rechtliches; Kündigen und Widerrufen bleiben hervorgehoben. */
+const SEITEN: FooterLink[] = [
+ { label: "Startseite", to: "/" },
+ { label: "Kundenportal", to: "/#kundenportal" },
+ { label: "Was wir bauen", to: "/#leistungen" },
+ { label: "Preise", to: "/#preise" },
+ { label: "Über uns", to: "/ueber-uns" },
+ { label: "Kontakt", to: "/kontakt" },
+ { label: "Wartungs-Abo", to: "/wartungs-abo" },
+ { label: "Hosting-Abo", to: "/hosting-abo" },
+];
+
+const RECHTLICHES: FooterLink[] = [
+ { label: "AGB", to: "/agb" },
+ { label: "Impressum", to: "/impressum" },
+ { label: "Datenschutz", href: "https://project.webklar.com/datenschutz.html" },
+ { label: "Verträge hier kündigen", href: "https://project.webklar.com/kuendigen.html", pflicht: true },
+ { label: "Vertrag widerrufen", href: "https://project.webklar.com/widerrufen.html", pflicht: true },
+];
+
+const Eintrag = ({ link }: { link: FooterLink }) => (
+
+ {link.to ? (
+ {link.label}
+ ) : (
+
+ {link.label}
+
+ )}
+
+);
+
+const SiteFooter = () => (
+
+);
+
+export default SiteFooter;
diff --git a/src/components/site/SiteHeader.tsx b/src/components/site/SiteHeader.tsx
new file mode 100644
index 0000000..bca6c60
--- /dev/null
+++ b/src/components/site/SiteHeader.tsx
@@ -0,0 +1,155 @@
+import { useEffect, useId, useRef, useState } from "react";
+import { Link, useLocation } from "react-router-dom";
+import { useTheme } from "next-themes";
+import { Moon, Sun } from "lucide-react";
+import Logo from "@/components/Logo";
+import { projekte } from "@/data/projekte";
+import "./site.css";
+
+/** Navigation der Kopfleiste (Spezifikation 6.1). „Projekte“ erscheint erst mit freigegebenen Referenzen. */
+export const NAVIGATION = [
+ { label: "Kundenportal", to: "/#kundenportal" },
+ { label: "Was wir bauen", to: "/#leistungen" },
+ ...(projekte.length > 0 ? [{ label: "Projekte", to: "/#projekte" }] : []),
+ { label: "Preise", to: "/#preise" },
+ { label: "Über uns", to: "/#ueber-uns" },
+];
+
+/** Hell/Dunkel umschalten. Vor dem ersten Rendern ist das Thema unbekannt, daher kein Symbolwechsel beim Laden. */
+const ThemaSchalter = ({ mitText = false }: { mitText?: boolean }) => {
+ const { resolvedTheme, setTheme } = useTheme();
+ const [bereit, setBereit] = useState(false);
+ useEffect(() => setBereit(true), []);
+ const dunkel = bereit && resolvedTheme === "dark";
+
+ return (
+ setTheme(dunkel ? "light" : "dark")}
+ aria-label={dunkel ? "Hellen Modus einschalten" : "Dunklen Modus einschalten"}
+ >
+ {dunkel ? : }
+ {mitText && {dunkel ? "Heller Modus" : "Dunkler Modus"} }
+
+ );
+};
+
+/** Ein zweiter Klick auf denselben Anker ändert die URL nicht; dann selbst hinscrollen. */
+const zumAnker = (to: string) => {
+ const [pfad, id] = to.split("#");
+ if (!id || window.location.pathname !== (pfad || "/") || window.location.hash !== `#${id}`) return;
+ document.getElementById(id)?.scrollIntoView();
+};
+
+const SiteHeader = () => {
+ const [offen, setOffen] = useState(false);
+ const [gescrollt, setGescrollt] = useState(false);
+ const { pathname, hash } = useLocation();
+ const panelId = useId();
+ const knopfRef = useRef(null);
+
+ // Trennlinie erst nach dem ersten Scrollen
+ useEffect(() => {
+ const pruefe = () => setGescrollt(window.scrollY > 4);
+ pruefe();
+ window.addEventListener("scroll", pruefe, { passive: true });
+ return () => window.removeEventListener("scroll", pruefe);
+ }, []);
+
+ // Menü schließt bei jedem Seiten- oder Ankerwechsel
+ useEffect(() => setOffen(false), [pathname, hash]);
+
+ // Offenes Menü: Seite sperren, Escape schließt und gibt den Fokus an den Knopf zurück
+ useEffect(() => {
+ if (!offen) return;
+ const vorher = document.body.style.overflow;
+ document.body.style.overflow = "hidden";
+ const taste = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ setOffen(false);
+ knopfRef.current?.focus();
+ }
+ };
+ window.addEventListener("keydown", taste);
+ return () => {
+ document.body.style.overflow = vorher;
+ window.removeEventListener("keydown", taste);
+ };
+ }, [offen]);
+
+ return (
+
+
+
+
+
WEBklar
+
+
+
+
+ {NAVIGATION.map((eintrag) => (
+
+ zumAnker(eintrag.to)}>
+ {eintrag.label}
+
+
+ ))}
+
+
+
+
+
+
+ Projekt besprechen
+
+
+
+
setOffen((o) => !o)}
+ >
+
+
+
+
+
+
+
+ {/* Handy-Menü: Abdunkelung schließt per Tipp daneben */}
+ setOffen(false)} aria-hidden />
+
+
+
+ {NAVIGATION.map((eintrag) => (
+
+ {
+ setOffen(false);
+ zumAnker(eintrag.to);
+ }}
+ >
+ {eintrag.label}
+
+
+ ))}
+
+
+
+ setOffen(false)}>
+ Projekt besprechen
+
+
+
+
+
+ );
+};
+
+export default SiteHeader;
diff --git a/src/components/site/site.css b/src/components/site/site.css
new file mode 100644
index 0000000..2cc736e
--- /dev/null
+++ b/src/components/site/site.css
@@ -0,0 +1,283 @@
+/* Kopfleiste, Handy-Menü und Footer (Spezifikation 6.1 und 7.8) */
+
+.wk-header {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ background: color-mix(in srgb, var(--bg) 94%, transparent);
+ border-bottom: 1px solid transparent;
+ transition: border-color var(--dur-state) var(--ease);
+}
+.wk-header--linie {
+ border-bottom-color: var(--line);
+}
+
+.wk-header__zeile {
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+ height: var(--header-h);
+}
+
+.wk-header__marke {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.55rem;
+ font-weight: 600;
+ font-size: 1.0625rem;
+ letter-spacing: 0;
+ color: var(--fg);
+ min-height: 44px;
+}
+.wk-header__marke svg {
+ border-radius: 7px;
+ box-shadow: inset 0 0 0 1px var(--line);
+}
+
+.wk-header__nav {
+ flex: 1;
+}
+.wk-header__nav ul {
+ display: flex;
+ justify-content: center;
+ gap: 0.25rem;
+}
+.wk-header__nav a {
+ display: inline-flex;
+ align-items: center;
+ min-height: 44px;
+ padding: 0 0.85rem;
+ font-size: 0.9375rem;
+ color: var(--fg-muted);
+ transition: color var(--dur-state) var(--ease);
+}
+@media (hover: hover) and (pointer: fine) {
+ .wk-header__nav a:hover {
+ color: var(--fg);
+ }
+}
+
+.wk-header__aktionen {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.wk-icon-btn {
+ display: inline-grid;
+ place-items: center;
+ width: 44px;
+ height: 44px;
+ border-radius: 999px;
+ color: var(--fg-muted);
+ transition: color var(--dur-state) var(--ease), background-color var(--dur-state) var(--ease);
+}
+@media (hover: hover) and (pointer: fine) {
+ .wk-icon-btn:hover {
+ color: var(--fg);
+ background: var(--bg-alt);
+ }
+}
+
+.wk-header__menuknopf {
+ display: none;
+ width: 44px;
+ height: 44px;
+ margin-left: auto;
+ margin-right: -10px;
+ place-items: center;
+ color: var(--fg);
+}
+
+.wk-burger {
+ position: relative;
+ display: block;
+ width: 20px;
+ height: 12px;
+}
+.wk-burger span {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ height: 1.5px;
+ border-radius: 2px;
+ background: currentColor;
+ transition: transform var(--dur-state) var(--ease), top var(--dur-state) var(--ease);
+}
+.wk-burger span:first-child {
+ top: 2px;
+}
+.wk-burger span:last-child {
+ top: 9px;
+}
+.wk-burger[data-offen="true"] span:first-child {
+ top: 5.5px;
+ transform: rotate(45deg);
+}
+.wk-burger[data-offen="true"] span:last-child {
+ top: 5.5px;
+ transform: rotate(-45deg);
+}
+
+.wk-menu,
+.wk-menu-hintergrund {
+ display: none;
+}
+
+@media (max-width: 899.98px) {
+ .wk-header__nav,
+ .wk-header__aktionen {
+ display: none;
+ }
+ .wk-header__menuknopf {
+ display: grid;
+ }
+
+ .wk-menu-hintergrund {
+ display: block;
+ position: fixed;
+ inset: var(--header-h) 0 0 0;
+ background: rgb(0 0 0 / 0.32);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity var(--dur-state) var(--ease);
+ }
+ .wk-menu-hintergrund[data-offen="true"] {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .wk-menu {
+ display: block;
+ position: fixed;
+ top: var(--header-h);
+ left: 0;
+ right: 0;
+ max-height: calc(100dvh - var(--header-h));
+ overflow-y: auto;
+ background: var(--bg);
+ border-bottom: 1px solid var(--line);
+ padding: 0.5rem var(--gutter) calc(1.5rem + env(safe-area-inset-bottom));
+ }
+ .wk-menu[hidden] {
+ display: none;
+ }
+ .wk-menu nav a {
+ display: flex;
+ align-items: center;
+ min-height: 56px;
+ font-size: 1.5rem;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ color: var(--fg);
+ border-bottom: 1px solid var(--line);
+ }
+ .wk-menu__fuss {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ margin-top: 1.5rem;
+ }
+ .wk-menu__fuss .wk-btn {
+ width: 100%;
+ }
+ .wk-menu-theme {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ min-height: 48px;
+ border-radius: 999px;
+ color: var(--fg);
+ box-shadow: inset 0 0 0 1px var(--line);
+ font-weight: 600;
+ }
+
+ @media (prefers-reduced-motion: no-preference) {
+ .wk-menu[data-offen="true"] {
+ animation: wk-menu-auf var(--dur-state) var(--ease);
+ }
+ }
+}
+
+@keyframes wk-menu-auf {
+ from {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+/* ── Footer ─────────────────────────────────────────────────────────────── */
+
+.wk-footer {
+ background: var(--bg-alt);
+ border-top: 1px solid var(--line);
+ color: var(--fg-muted);
+ font-size: var(--fs-small);
+ line-height: 1.5;
+ padding-block: 56px calc(40px + env(safe-area-inset-bottom));
+}
+.wk-footer__raster {
+ display: grid;
+ grid-template-columns: 1.4fr 1fr 1fr;
+ gap: 2.5rem;
+}
+.wk-footer__marke {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: var(--fg);
+ font-weight: 600;
+ font-size: 1rem;
+ margin-bottom: 0.75rem;
+}
+.wk-footer__marke svg {
+ border-radius: 6px;
+ box-shadow: inset 0 0 0 1px var(--line);
+}
+.wk-footer h2 {
+ font-size: var(--fs-small);
+ font-weight: 600;
+ letter-spacing: 0;
+ color: var(--fg);
+ margin-bottom: 0.75rem;
+}
+.wk-footer li a {
+ display: inline-flex;
+ align-items: center;
+ min-height: 32px;
+ color: var(--fg-muted);
+ transition: color var(--dur-state) var(--ease);
+}
+.wk-footer li a.wk-footer__pflicht {
+ color: var(--fg);
+ font-weight: 600;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+@media (hover: hover) and (pointer: fine) {
+ .wk-footer li a:hover {
+ color: var(--fg);
+ }
+}
+.wk-footer__unten {
+ margin-top: 3rem;
+ padding-top: 1.25rem;
+ border-top: 1px solid var(--line);
+}
+
+@media (max-width: 719.98px) {
+ .wk-footer__raster {
+ grid-template-columns: 1fr 1fr;
+ }
+ .wk-footer__info {
+ grid-column: 1 / -1;
+ }
+ .wk-footer li a {
+ min-height: 44px;
+ }
+}
diff --git a/src/data/projekte.ts b/src/data/projekte.ts
new file mode 100644
index 0000000..0038d59
--- /dev/null
+++ b/src/data/projekte.ts
@@ -0,0 +1,14 @@
+/**
+ * Referenzen für den Abschnitt „Projekte“ (Spezifikation 7.4).
+ * Nur Projekte, die online sind und deren Kunden zugestimmt haben (AGB § 13).
+ * Solange die Liste leer ist, gibt es weder Abschnitt noch Menüpunkt.
+ */
+export type Projekt = {
+ name: string;
+ beschreibung: string;
+ url: string;
+ /** eigener Screenshot unter public/, kein Stockfoto */
+ bild: string;
+};
+
+export const projekte: Projekt[] = [];
diff --git a/src/main.tsx b/src/main.tsx
index 36c22c5..b7abd54 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,5 +2,18 @@ import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./fonts.css";
import "./index.css";
+import "./styles/relaunch.css";
+
+// Entwurf: Schriftvariante vergleichen, ?schrift=a (Systemschrift) oder ?schrift=b (Instrument Sans, Standard).
+// Die Wahl gilt für den ganzen Besuch. Phase 2 entfernt den Umschalter.
+try {
+ const wahl = new URLSearchParams(window.location.search).get("schrift") ?? sessionStorage.getItem("schrift");
+ if (wahl === "a" || wahl === "b") {
+ sessionStorage.setItem("schrift", wahl);
+ document.documentElement.dataset.schrift = wahl;
+ }
+} catch {
+ // ohne sessionStorage bleibt die Standardschrift
+}
createRoot(document.getElementById("root")!).render(
);
diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx
index b08e83f..ae3f0d3 100644
--- a/src/pages/Index.tsx
+++ b/src/pages/Index.tsx
@@ -1,34 +1,28 @@
-import Header from "@/components/Header";
-import Hero from "@/components/Hero";
-import Partners from "@/components/Partners";
-import ProblemSection from "@/components/ProblemSection";
-import SolutionSection from "@/components/SolutionSection";
-import Values from "@/components/Values";
-import Services from "@/components/Services";
-import Process from "@/components/Process";
-import Contact from "@/components/Contact";
-import Footer from "@/components/Footer";
+import HeroKarte from "@/components/home/HeroKarte";
+import Kundenportal from "@/components/home/Kundenportal";
+import Leistungen from "@/components/home/Leistungen";
+import Preise from "@/components/home/Preise";
+import UeberUns from "@/components/home/UeberUns";
+import KontaktBuehne from "@/components/home/KontaktBuehne";
+import "@/components/home/home.css";
import { usePageMeta } from "@/hooks/use-page-meta";
+/** Startseite des Relaunchs (Spezifikation 6.2). Kopfleiste und Footer kommen aus dem gemeinsamen Layout. */
const Index = () => {
usePageMeta(
- "WEBklar – Webagentur für KMU",
- "WEBklar digitalisiert KMU: Website, Prozesse und Systeme aus einer Hand. Kostenlose Potenzialanalyse – Antwort spätestens am nächsten Werktag."
+ "WEBklar – Websites, Shops und Apps für kleine Betriebe",
+ "WEBklar baut Websites, Online-Shops und Web-Apps. Im Kundenportal sehen Sie jeden Schritt, mit Live-Vorschau Ihrer Website.",
);
return (
-
+ <>
+
+
+
+
+
+
+ >
);
};
diff --git a/src/routes.tsx b/src/routes.tsx
index ba1c8bd..23a9193 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -1,5 +1,6 @@
import { Routes, Route, Outlet } from "react-router-dom";
-import Footer from "@/components/Footer";
+import SiteHeader from "@/components/site/SiteHeader";
+import SiteFooter from "@/components/site/SiteFooter";
import Index from "./pages/Index";
import ContactPage from "./pages/Contact";
import AGBPage from "./pages/AGB";
@@ -10,21 +11,27 @@ import HostingAboPage from "./pages/HostingAbo";
import DatenschutzPage from "./pages/Datenschutz";
import NotFound from "./pages/NotFound";
-/** Gemeinsamer Rahmen aller Unterseiten: Seiteninhalt, darunter der Footer mit Impressum, Datenschutz und Kündigen. */
-const SubpageLayout = () => (
+/** Gemeinsamer Rahmen aller Seiten: Kopfleiste, Inhalt, Footer mit Impressum, Datenschutz und Kündigen. */
+const SiteLayout = () => (
<>
-
-
+
+ Zum Inhalt springen
+
+
+
+
+
+
>
);
/** Alle Seiten der Website; App.tsx rendert sie im BrowserRouter, die Tests im MemoryRouter. */
export const AppRoutes = () => (
- {/* Die Startseite bringt ihren Footer selbst mit; /datenschutz leitet nur weiter. */}
- } />
+ {/* /datenschutz leitet nur weiter */}
} />
- }>
+ }>
+ } />
} />
} />
} />
diff --git a/src/styles/relaunch.css b/src/styles/relaunch.css
new file mode 100644
index 0000000..7d04561
--- /dev/null
+++ b/src/styles/relaunch.css
@@ -0,0 +1,335 @@
+/*
+ * Relaunch 2026: Gestaltungs-Grundlage (Spezifikation Abschnitt 8).
+ * Eine Schriftfamilie, zwei Gewichte, neutrale Flächen und eine Akzentfarbe (Rot wie der Kartenpunkt).
+ * Wird nach index.css geladen und überschreibt dort die alten Token, damit auch die
+ * Rechtsseiten (shadcn-Klassen wie bg-background, text-muted-foreground) den neuen Look bekommen.
+ */
+
+/* Schrift B: Instrument Sans, selbst gehostet (SIL Open Font License, public/fonts/instrument-sans/LICENSE) */
+@font-face {
+ font-family: "Instrument Sans";
+ font-style: normal;
+ font-weight: 400 700;
+ font-display: swap;
+ src: url(/fonts/instrument-sans/instrument-sans-latin-ext-wght-normal.woff2) format("woff2");
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+@font-face {
+ font-family: "Instrument Sans";
+ font-style: normal;
+ font-weight: 400 700;
+ font-display: swap;
+ src: url(/fonts/instrument-sans/instrument-sans-latin-wght-normal.woff2) format("woff2");
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+:root {
+ /* Schrift: B (Instrument Sans) ist Standard im Entwurf, ?schrift=a schaltet auf die Systemschrift */
+ --font-system: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ --font-ui: "Instrument Sans", var(--font-system);
+
+ --fs-hero: clamp(2.5rem, 1.569rem + 3.81vw, 5rem);
+ --fs-h2: clamp(2rem, 1.444rem + 2.29vw, 3.5rem);
+ --fs-h3: clamp(1.25rem, 1.1rem + 0.5vw, 1.5rem);
+ --fs-lead: clamp(1.1875rem, 1.05rem + 0.6vw, 1.625rem);
+ --fs-body: 1.0625rem;
+ --fs-small: 0.875rem;
+ --measure-text: 46rem;
+ --container: 1100px;
+ --gutter: clamp(16px, 4vw, 32px);
+ --section-space: clamp(72px, 9vw, 160px);
+ --header-h: 60px;
+
+ --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
+ --dur-state: 200ms;
+ --dur-enter: 600ms;
+
+ /* Farben hell */
+ --bg: #ffffff;
+ --bg-alt: #f5f5f7;
+ --fg: #1d1d1f;
+ --fg-muted: #6e6e73;
+ --line: #d2d2d7;
+ --line-strong: #86868b;
+ --accent: #e5251c;
+ --accent-text: #c81e16;
+ --focus: #c81e16;
+
+ /* shadcn-Token auf die neuen Farben gelegt (HSL ohne hsl()) */
+ --background: 0 0% 100%;
+ --foreground: 240 3% 12%;
+ --card: 240 11% 96%;
+ --card-foreground: 240 3% 12%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 240 3% 12%;
+ --primary: 240 3% 12%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 240 11% 96%;
+ --secondary-foreground: 240 3% 12%;
+ --muted: 240 11% 96%;
+ --muted-foreground: 240 2% 44%;
+ --accent-hsl: 3 80% 43%;
+ --border: 240 6% 84%;
+ --input: 240 6% 84%;
+ --ring: 3 80% 43%;
+ --radius: 0.75rem;
+ --font-sans: var(--font-ui);
+ --font-display: var(--font-ui);
+}
+
+.dark {
+ --bg: #0b0b0c;
+ --bg-alt: #161618;
+ --fg: #f5f5f7;
+ --fg-muted: #a1a1a6;
+ --line: #2c2c2e;
+ --line-strong: #636366;
+ --accent: #e5251c;
+ --accent-text: #ff5a4f;
+ --focus: #ff5a4f;
+
+ --background: 240 4% 4%;
+ --foreground: 240 11% 96%;
+ --card: 240 4% 9%;
+ --card-foreground: 240 11% 96%;
+ --popover: 240 4% 9%;
+ --popover-foreground: 240 11% 96%;
+ --primary: 240 11% 96%;
+ --primary-foreground: 240 3% 12%;
+ --secondary: 240 4% 9%;
+ --secondary-foreground: 240 11% 96%;
+ --muted: 240 4% 9%;
+ --muted-foreground: 240 2% 64%;
+ --accent-hsl: 4 100% 65%;
+ --border: 240 2% 18%;
+ --input: 240 2% 18%;
+ --ring: 4 100% 65%;
+}
+
+html[data-schrift="a"] {
+ --font-ui: var(--font-system);
+}
+
+html {
+ scroll-behavior: smooth;
+ -webkit-text-size-adjust: 100%;
+}
+@media (prefers-reduced-motion: reduce) {
+ html {
+ scroll-behavior: auto;
+ }
+}
+
+body {
+ background: var(--bg);
+ color: var(--fg);
+ font-family: var(--font-ui);
+ font-size: var(--fs-body);
+ line-height: 1.47;
+ letter-spacing: normal;
+ font-feature-settings: "kern", "calt";
+ -webkit-font-smoothing: antialiased;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: var(--font-ui);
+ font-weight: 600;
+ letter-spacing: -0.015em;
+ text-transform: none;
+ text-wrap: balance;
+}
+
+p {
+ text-wrap: pretty;
+}
+
+:focus-visible {
+ outline: 2px solid var(--focus);
+ outline-offset: 3px;
+ border-radius: 6px;
+}
+
+section[id] {
+ scroll-margin-top: calc(var(--header-h) + 24px);
+}
+
+/* ── Bausteine ─────────────────────────────────────────────────────────── */
+
+.wk-container {
+ width: 100%;
+ max-width: calc(var(--container) + 2 * var(--gutter));
+ margin-inline: auto;
+ padding-inline: var(--gutter);
+}
+
+.wk-section {
+ padding-block: var(--section-space);
+ background: var(--bg);
+}
+.wk-section--alt {
+ background: var(--bg-alt);
+}
+
+.wk-eyebrow {
+ display: block;
+ font-size: var(--fs-small);
+ font-weight: 600;
+ color: var(--fg-muted);
+ letter-spacing: 0.005em;
+ margin-bottom: 1rem;
+}
+
+.wk-h1 {
+ font-size: var(--fs-hero);
+ line-height: 1.05;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+}
+
+.wk-h2 {
+ font-size: var(--fs-h2);
+ line-height: 1.07;
+ font-weight: 600;
+ letter-spacing: -0.015em;
+ max-width: 20ch;
+}
+
+.wk-h3 {
+ font-size: var(--fs-h3);
+ line-height: 1.2;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+}
+
+.wk-lead {
+ font-size: var(--fs-lead);
+ line-height: 1.38;
+ color: var(--fg-muted);
+ max-width: var(--measure-text);
+ text-wrap: pretty;
+}
+.wk-h2 + .wk-lead,
+.wk-h1 + .wk-lead {
+ margin-top: 1.2em;
+}
+
+.wk-muted {
+ color: var(--fg-muted);
+}
+.wk-small {
+ font-size: var(--fs-small);
+ line-height: 1.43;
+}
+.wk-num {
+ font-variant-numeric: tabular-nums;
+}
+
+.wk-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ min-height: 48px;
+ padding: 0 1.5rem;
+ border-radius: 999px;
+ font-size: 1.0625rem;
+ font-weight: 600;
+ line-height: 1;
+ white-space: nowrap;
+ transition:
+ background-color var(--dur-state) var(--ease),
+ color var(--dur-state) var(--ease),
+ box-shadow var(--dur-state) var(--ease),
+ opacity var(--dur-state) var(--ease);
+ -webkit-tap-highlight-color: transparent;
+}
+.wk-btn--primary {
+ background: var(--fg);
+ color: var(--bg);
+}
+.wk-btn--secondary {
+ background: transparent;
+ color: var(--fg);
+ box-shadow: inset 0 0 0 1px var(--line-strong);
+}
+.wk-btn--small {
+ min-height: 36px;
+ padding: 0 1rem;
+ font-size: 0.9375rem;
+}
+@media (hover: hover) and (pointer: fine) {
+ .wk-btn--primary:hover {
+ opacity: 0.86;
+ }
+ .wk-btn--secondary:hover {
+ box-shadow: inset 0 0 0 1px var(--fg);
+ }
+}
+
+.wk-link {
+ color: var(--fg);
+ font-weight: 600;
+ text-decoration: underline;
+ text-decoration-thickness: 1px;
+ text-underline-offset: 4px;
+ text-decoration-color: var(--line-strong);
+ transition: text-decoration-color var(--dur-state) var(--ease);
+}
+@media (hover: hover) and (pointer: fine) {
+ .wk-link:hover {
+ text-decoration-color: var(--fg);
+ }
+}
+
+.wk-visually-hidden {
+ position: absolute !important;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* Sanftes Erscheinen beim Scrollen: Grundzustand sichtbar, nur mit Unterstützung und ohne reduzierte Bewegung */
+@media (prefers-reduced-motion: no-preference) {
+ @supports (animation-timeline: view()) {
+ .wk-reveal {
+ animation: wk-reveal linear both;
+ animation-timeline: view();
+ animation-range: entry 0% cover 22%;
+ }
+ }
+}
+@keyframes wk-reveal {
+ from {
+ opacity: 0;
+ transform: translateY(28px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+.wk-skiplink {
+ position: absolute;
+ left: 1rem;
+ top: -100px;
+ z-index: 100;
+ padding: 0.75rem 1rem;
+ border-radius: 10px;
+ background: var(--fg);
+ color: var(--bg);
+ font-weight: 600;
+}
+.wk-skiplink:focus {
+ top: 0.75rem;
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 8855aa7..50e03ef 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -14,25 +14,9 @@ export default {
},
extend: {
fontFamily: {
- sans: [
- 'Inter',
- 'ui-sans-serif',
- 'system-ui',
- '-apple-system',
- 'BlinkMacSystemFont',
- 'Segoe UI',
- 'Roboto',
- 'Helvetica Neue',
- 'Arial',
- 'Noto Sans',
- 'sans-serif'
- ],
- display: [
- 'Space Grotesk',
- 'Inter',
- 'system-ui',
- 'sans-serif'
- ],
+ // Relaunch: eine Schriftfamilie für alles (src/styles/relaunch.css, --font-ui)
+ sans: ['var(--font-ui)'],
+ display: ['var(--font-ui)'],
serif: [
'Lora',
'ui-serif',