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>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-01 14:20:18 +00:00
1 parent 11fd118aa7
commit 822481f2e4
19 files changed
+1149 -50

No files matched your search

+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.
+10
View File
@@ -0,0 +1,10 @@
/** Platzhalter: wird im Relaunch-Workflow gebaut (Spezifikation Abschnitt 7). */
const HeroKarte = () => (
<section className="wk-section">
<div className="wk-container">
<h2 className="wk-h2">HeroKarte</h2>
</div>
</section>
);
export default HeroKarte;
+32
View File
@@ -0,0 +1,32 @@
import { Link } from "react-router-dom";
/** Spezifikation 7.7: ein Satz, ein Button, E-Mail und Telefon. */
const KontaktBuehne = () => (
<section id="kontakt" className="wk-section wk-section--alt wk-kontakt" aria-labelledby="kontakt-titel">
<div className="wk-container wk-kontakt__inhalt">
<h2 id="kontakt-titel" className="wk-h2 wk-reveal">
Erzählen Sie uns, was Sie vorhaben.
</h2>
<p className="wk-lead wk-reveal">
Zwei Sätze genügen. Wir melden uns spätestens am nächsten Werktag. Im ersten Gespräch klären wir in
15&nbsp;Minuten, ob und wie wir helfen können – kostenlos und unverbindlich.
</p>
<div className="wk-kontakt__aktion wk-reveal">
<Link to="/kontakt" className="wk-btn wk-btn--primary">
Projekt besprechen
</Link>
</div>
<p className="wk-kontakt__wege">
<a href="mailto:support@webklar.com" className="wk-link">
support@webklar.com
</a>
<span aria-hidden>·</span>
<a href="tel:+4917623726355" className="wk-link wk-num">
0176 23726355
</a>
</p>
</div>
</section>
);
export default KontaktBuehne;
+10
View File
@@ -0,0 +1,10 @@
/** Platzhalter: wird im Relaunch-Workflow gebaut (Spezifikation Abschnitt 7). */
const Kundenportal = () => (
<section id="kundenportal" className="wk-section">
<div className="wk-container">
<h2 className="wk-h2">Kundenportal</h2>
</div>
</section>
);
export default Kundenportal;
+10
View File
@@ -0,0 +1,10 @@
/** Platzhalter: wird im Relaunch-Workflow gebaut (Spezifikation Abschnitt 7). */
const Leistungen = () => (
<section id="leistungen" className="wk-section">
<div className="wk-container">
<h2 className="wk-h2">Leistungen</h2>
</div>
</section>
);
export default Leistungen;
+10
View File
@@ -0,0 +1,10 @@
/** Platzhalter: wird im Relaunch-Workflow gebaut (Spezifikation Abschnitt 7). */
const Preise = () => (
<section id="preise" className="wk-section">
<div className="wk-container">
<h2 className="wk-h2">Preise</h2>
</div>
</section>
);
export default Preise;
+25
View File
@@ -0,0 +1,25 @@
import { Link } from "react-router-dom";
/** Spezifikation 7.6: wer dahinter steckt. Keine Fotos, bis echte Teamfotos vorliegen. */
const UeberUns = () => (
<section id="ueber-uns" className="wk-section" aria-labelledby="ueber-uns-titel">
<div className="wk-container wk-ueber-uns">
<h2 id="ueber-uns-titel" className="wk-h2 wk-reveal">
Wer hinter WEBklar steckt.
</h2>
<div className="wk-reveal">
<p className="wk-lead">
Wir sind ein kleines Team aus Winnweiler in der Pfalz. Sie sprechen bei uns direkt mit den Leuten, die Ihre
Website bauen.
</p>
<p className="wk-ueber-uns__link">
<Link to="/ueber-uns" className="wk-link">
Mehr über uns
</Link>
</p>
</div>
</div>
</section>
);
export default UeberUns;
+46
View File
@@ -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;
}
+78
View File
@@ -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 }) => (
<li>
{link.to ? (
<Link to={link.to}>{link.label}</Link>
) : (
<a href={link.href} className={link.pflicht ? "wk-footer__pflicht" : undefined}>
{link.label}
</a>
)}
</li>
);
const SiteFooter = () => (
<footer className="wk-footer">
<div className="wk-container">
<div className="wk-footer__raster">
<div className="wk-footer__info">
<Link to="/" className="wk-footer__marke">
<Logo width={24} height={24} />
<span>WEBklar</span>
</Link>
<p>
WEBklar GbR
<br />
Am Schwimmbad 10
<br />
67722 Winnweiler
</p>
</div>
<nav aria-label="Seiten">
<h2>Seiten</h2>
<ul>
{SEITEN.map((link) => (
<Eintrag key={link.label} link={link} />
))}
</ul>
</nav>
<nav aria-label="Rechtliches">
<h2>Rechtliches</h2>
<ul>
{RECHTLICHES.map((link) => (
<Eintrag key={link.label} link={link} />
))}
</ul>
</nav>
</div>
<p className="wk-footer__unten">© {new Date().getFullYear()} WEBklar GbR</p>
</div>
</footer>
);
export default SiteFooter;
+155
View File
@@ -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 (
<button
type="button"
className={mitText ? "wk-menu-theme" : "wk-icon-btn"}
onClick={() => setTheme(dunkel ? "light" : "dark")}
aria-label={dunkel ? "Hellen Modus einschalten" : "Dunklen Modus einschalten"}
>
{dunkel ? <Sun size={18} aria-hidden /> : <Moon size={18} aria-hidden />}
{mitText && <span>{dunkel ? "Heller Modus" : "Dunkler Modus"}</span>}
</button>
);
};
/** 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<HTMLButtonElement>(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 (
<header className={`wk-header${gescrollt || offen ? " wk-header--linie" : ""}`}>
<div className="wk-container wk-header__zeile">
<Link to="/" className="wk-header__marke" aria-label="WEBklar – zur Startseite">
<Logo width={28} height={28} />
<span>WEBklar</span>
</Link>
<nav className="wk-header__nav" aria-label="Hauptnavigation">
<ul>
{NAVIGATION.map((eintrag) => (
<li key={eintrag.to}>
<Link to={eintrag.to} onClick={() => zumAnker(eintrag.to)}>
{eintrag.label}
</Link>
</li>
))}
</ul>
</nav>
<div className="wk-header__aktionen">
<ThemaSchalter />
<Link to="/kontakt" className="wk-btn wk-btn--primary wk-btn--small">
Projekt besprechen
</Link>
</div>
<button
ref={knopfRef}
type="button"
className="wk-header__menuknopf"
aria-expanded={offen}
aria-controls={panelId}
aria-label={offen ? "Menü schließen" : "Menü öffnen"}
onClick={() => setOffen((o) => !o)}
>
<span className="wk-burger" data-offen={offen} aria-hidden>
<span />
<span />
</span>
</button>
</div>
{/* Handy-Menü: Abdunkelung schließt per Tipp daneben */}
<div className="wk-menu-hintergrund" data-offen={offen} onClick={() => setOffen(false)} aria-hidden />
<div id={panelId} className="wk-menu" data-offen={offen} hidden={!offen}>
<nav aria-label="Menü">
<ul>
{NAVIGATION.map((eintrag) => (
<li key={eintrag.to}>
<Link
to={eintrag.to}
onClick={() => {
setOffen(false);
zumAnker(eintrag.to);
}}
>
{eintrag.label}
</Link>
</li>
))}
</ul>
</nav>
<div className="wk-menu__fuss">
<Link to="/kontakt" className="wk-btn wk-btn--primary" onClick={() => setOffen(false)}>
Projekt besprechen
</Link>
<ThemaSchalter mitText />
</div>
</div>
</header>
);
};
export default SiteHeader;
+283
View File
@@ -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;
}
}
+14
View File
@@ -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[] = [];
+13
View File
@@ -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(<App />);
+18 -24
View File
@@ -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 (
<div className="min-h-screen bg-background">
<Header />
<Hero />
<Partners />
<ProblemSection />
<SolutionSection />
<Values />
<Services />
<Process />
<Contact />
<Footer />
</div>
<>
<HeroKarte />
<Kundenportal />
<Leistungen />
<Preise />
<UeberUns />
<KontaktBuehne />
</>
);
};
+14 -7
View File
@@ -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 = () => (
<>
<a href="#inhalt" className="wk-skiplink">
Zum Inhalt springen
</a>
<SiteHeader />
<main id="inhalt">
<Outlet />
<Footer />
</main>
<SiteFooter />
</>
);
/** Alle Seiten der Website; App.tsx rendert sie im BrowserRouter, die Tests im MemoryRouter. */
export const AppRoutes = () => (
<Routes>
{/* Die Startseite bringt ihren Footer selbst mit; /datenschutz leitet nur weiter. */}
<Route path="/" element={<Index />} />
{/* /datenschutz leitet nur weiter */}
<Route path="/datenschutz" element={<DatenschutzPage />} />
<Route element={<SubpageLayout />}>
<Route element={<SiteLayout />}>
<Route path="/" element={<Index />} />
<Route path="/kontakt" element={<ContactPage />} />
<Route path="/agb" element={<AGBPage />} />
<Route path="/impressum" element={<ImpressumPage />} />
+335
View File
@@ -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;
}
+3 -19
View File
@@ -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',