# Gestaltung — was eine Seite gut macht

Dies ist der wichtigste Text im ganzen Vertrag. Er sagt dir nicht, *wie* deine Seite
aussehen soll — das ist deine Entscheidung. Er sagt dir, welche Regeln eine gute Seite
von einer mittelmäßigen trennen, damit du deine Entscheidungen auf Wissen stützt statt
auf Gefühl.

Stand September 2026. Alles hier ist in allen aktuellen Browsern verfügbar.

---

## 1 · Zuerst das Wichtigste: Handy vor Rechner

**Die meisten Menschen sehen diese Seite auf einem Telefon.** Entwirf für 390 Pixel
Breite und lass die Seite nach oben wachsen — nie umgekehrt. Ein Entwurf, der am großen
Bildschirm entsteht und danach „responsiv gemacht" wird, hat am Telefon immer
Kompromisse.

Konkret heißt das:

```css
/* richtig: Grundzustand ist schmal, Breite kommt dazu */
.karten { display: grid; gap: 14px; grid-template-columns: 1fr; }
@media (min-width: 720px)  { .karten { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1040px) { .karten { grid-template-columns: repeat(3, 1fr); } }
```

**Trefflächen mindestens 44 × 44 Pixel.** Das ist Apples Richtwert, Material nennt 48dp,
und WCAG 2.2 fordert als Untergrenze 24 × 24 CSS-Pixel (Kriterium 2.5.8). Nimm 44 — ein
Knopf, den man dreimal antippen muss, ist kaputt, egal wie gut er aussieht.

**Kein waagerechtes Scrollen der Seite.** Breite Inhalte — Tabellen, Diagramme,
Codeblöcke — scrollen in ihrem *eigenen* Behälter mit `overflow-x: auto`. Prüfe es:
`document.documentElement.scrollWidth` darf nie größer sein als `clientWidth`.

> **Die Falle, in die fast jeder tappt:** Eine Grid-Spalte mit `1fr` hat
> `min-width: auto`. Ein `<pre>` mit `white-space: pre` darin drückt die Spalte über die
> Seitenbreite hinaus, obwohl es selbst scrollen könnte. Lösung: `minmax(0, 1fr)` statt
> `1fr`, oder `min-width: 0` auf dem Kind.

---

## 2 · Typografie: eine Skala, flüssig

Feste Pixelgrößen mit Sprüngen an Haltepunkten sind veraltet. Nimm `clamp()`:

```css
h1 { font-size: clamp(2rem, 1.2rem + 4vw, 4.5rem); }
h2 { font-size: clamp(1.5rem, 1.1rem + 2vw, 2.6rem); }
p  { font-size: clamp(1rem, 0.96rem + 0.2vw, 1.15rem); }
```

Das mittlere Argument ist die flüssige Größe. Die Form `Xrem + Yvw` statt reinem `vw`
ist wichtig: reines `vw` skaliert auch beim Hineinzoomen mit und bricht damit die
Zugänglichkeit.

**Weitere Regeln, die tragen:**

| Regel | Warum |
|---|---|
| Zeilenlänge 60–75 Zeichen (`max-width: 65ch`) | Längere Zeilen verliert das Auge beim Zeilensprung |
| Zeilenhöhe 1,5–1,7 im Fließtext, 1,1–1,25 in großen Überschriften | Große Schrift braucht relativ weniger Luft |
| Höchstens zwei Schriftfamilien | Eine für Text, eine monospaced für Zahlen und Code |
| Zahlen immer `font-variant-numeric: tabular-nums` | Sonst zappeln Tabellenspalten |
| Überschriften mit `letter-spacing: -0.02em` | Große Grade wirken sonst locker gesetzt |

**Schriften nur von Google Fonts**, mit `display=swap` und Vorverbindung:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=…&display=swap">
```

Immer eine echte Ersatzkette angeben: `font-family: "Deine Schrift", ui-sans-serif,
system-ui, sans-serif`. Lädt die Schrift nicht, darf die Seite nicht zerfallen.

---

## 3 · Farbe: Token zuerst, Kontrast eingebaut

Definiere deine Palette **einmal** als CSS-Variablen, dann benutze nur noch die Namen.

```css
:root{
  --grund:   #0a0818;
  --flaeche: rgba(21,17,50,.46);
  --linie:   rgba(236,233,255,.10);
  --text:    #f2f0ff;
  --text-2:  #a9a2d6;
  --akzent:  #c98bff;
}
```

**Kontrast ist keine Nachprüfung, sondern Teil der Definition.** Prüfe beim Festlegen:

- Fließtext gegen seinen Grund: mindestens **4,5 : 1**
- Große Schrift (ab 24 px, oder 19 px fett): mindestens **3 : 1**
- Bedienelemente und Diagrammlinien gegen ihren Grund: mindestens **3 : 1**

### Der Fehler, den du garantiert machen wirst

Diese Seite liegt über einem **bewegten** Hintergrund mit hellen Stellen. Jeder Textblock
braucht eine eigene Fläche. Ein Rahmen genügt nicht — dahinter läuft der Hintergrund
weiter durch.

```css
/* falsch: sieht im Entwurf gut aus, ist es aber nur manchmal */
.hinweis{ border: 1px dashed var(--linie); padding: 16px; }

/* richtig */
.hinweis{ border: 1px dashed var(--linie); padding: 16px;
          background: rgba(8,7,14,.90);
          backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
```

Warum das so tückisch ist: Der Hintergrund bewegt sich. Wenn du nachsiehst, steht die
helle Stelle vielleicht gerade woanders — und der Text sieht lesbar aus. Zwei Sekunden
später ist er weg.

**Das betrifft auch kurze Beschriftungen.** Eine Kapitelüberschrift in 11 px verschwindet
über einer hellen Stelle genauso wie ein ganzer Absatz.

Der Validator prüft das (`npm run pruefe`): Er liest aus deinem `<style>`, welche Klassen
eine Fläche setzen, läuft dann durch das Markup und meldet jeden Textblock ab 22 Zeichen,
über dem in der ganzen Kette kein `background` steht.

> Diese Prüfung existiert, weil das Modell, das diese Regeln geschrieben hat, sie beim
> ersten eigenen Versuch selbst verletzt hat — an vier Stellen. Eine Vorschrift, an die
> man sich erinnern muss, ist keine.

**Bei Diagrammen:**

- Höchstens drei kategorische Farben. Bei mehr brauchst du Form *und* Farbe.
- Ein Farbverlauf für Größe heißt: **ein** Farbton, hell nach dunkel. Kein Regenbogen.
- Farbe folgt dem Datensatz, nie seinem Rang. Ein Filter darf die übrigen nicht umfärben.
- Zahlen und Achsen tragen Textfarben, nie die Serienfarbe.
- Ab zwei Serien immer eine Legende.
- **Nie zwei y-Achsen.** Zwei Größen unterschiedlicher Skala werden zwei Diagramme.

---

## 4 · Ladeverhalten: die drei Zahlen, an denen gemessen wird

| Größe | Gut | Was sie kaputt macht |
|---|---|---|
| **LCP** — größte Inhaltsdarstellung | unter 2,5 s | Große Bilder ohne `loading`, Schriften ohne `swap` |
| **INP** — Reaktion auf Eingaben | unter 200 ms | Langlaufendes JavaScript im Hauptthread |
| **CLS** — Layoutverschiebung | unter 0,1 | **Bilder ohne `width`/`height`** |

Die dritte ist die, die auf dieser Seite am leichtesten schiefgeht. **Jedes `<img>`,
`<video>` und `<iframe>` bekommt `width` und `height`** — dann reserviert der Browser
den Platz, bevor die Datei da ist, und nichts springt.

```html
<img src="../../media/schuss.jpg" alt="Kampfszene mit Aktionsmenü"
     width="1100" height="760" loading="lazy" decoding="async">
```

Videos unterhalb des ersten Bildschirms: `preload="none"` und ein `poster`. Drei lange
Videos, die beim Aufruf gleichzeitig laden, kosten mehr als die ganze restliche Seite.

---

## 5 · Bewegung: sparsam und abschaltbar

Bewegung lenkt. Setze sie ein, wo sie etwas erklärt — ein Wert, der hochzählt; ein
Diagramm, das sich aufbaut — nicht als Dekoration.

**Immer** respektieren:

```css
@media (prefers-reduced-motion: reduce){
  *{ animation: none !important; transition: none !important; }
}
```

Animiere nur `transform` und `opacity`. Alles andere zwingt den Browser zum Neuberechnen
des Layouts in jedem Bild.

---

## 6 · Bedienbarkeit ohne Maus

Am Telefon gibt es kein Schweben. Alles, was nur bei `:hover` sichtbar wird, existiert
dort nicht. Wenn ein Element anklickbar ist, muss man das **ohne Mauszeiger** sehen.

```css
/* Sichtbarer Fokus ist Pflicht, nicht Zierde */
a:focus-visible, button:focus-visible{
  outline: 2px solid var(--akzent);
  outline-offset: 3px;
}
```

Der Fokusrahmen braucht mindestens 2 Pixel Stärke und 3 : 1 Kontrast (WCAG 2.2, 2.4.13).
Und er darf nicht unter einer klebenden Kopfleiste verschwinden (2.4.11) — dagegen hilft
`scroll-margin-top` an den Sprungzielen.

**Verschachtele niemals zwei `<a>` ineinander.** Der Browser reißt das Element
auseinander. Willst du eine ganze Karte klickbar machen und trotzdem einen zweiten Link
darin haben:

```css
.karte{ position: relative; }
.karte .haupt-link::after{ content:""; position:absolute; inset:0; }
.karte .neben-link{ position: relative; z-index: 1; }
```

---

## 7 · Semantik: die Struktur trägt die Bedeutung

- `<h1>` genau einmal. Danach `<h2>`, `<h3>` ohne Sprünge.
- Jeder Abschnitt ist ein `<section>` mit seiner Pflicht-`id`.
- Tabellendaten in `<table>`, nicht in `<div>`-Gittern.
- `<button>` für Aktionen, `<a>` für Ziele. Ein `<div onclick>` ist für die Tastatur
  unsichtbar.
- `alt` an jedem Bild — und zwar was zu sehen ist, nicht der Dateiname.
- `lang="de"` am `<html>`.

---

## 8 · Was auf dieser Seite verboten ist

| Verboten | Grund |
|---|---|
| Blindtext, Platzhalterbilder | Es sind echte Daten da |
| Fremde Skripte oder Stile (außer Google Fonts) | Die Seite muss offline funktionieren |
| `alert()`, `confirm()`, `prompt()` | Blockiert und wirkt kaputt |
| `position: fixed` über dem Inhalt | Verdeckt am Telefon die halbe Seite |
| Inhalt bei `opacity: 0`, der auf einen Observer wartet | Bleibt unsichtbar, wenn das Skript nicht läuft |
| `!important` außer in der Bewegungsregel | Macht die eigene Seite unwartbar |
| Erfundene Zahlen | Zerstört die gesamte Messreihe |
| Mehr als etwa 1 400 Zeilen | Darunter leidet die Kohärenz, und du hast nur einen Versuch |

---

## 9 · Der Hintergrund gehört dir nicht

Der bewegte Hintergrund kommt aus `../../assets/bg.js` und ist auf allen Seiten
derselbe — er verbindet die Modellseiten mit der Startseite. Du bindest ihn über den
Kopf- und Fußblock ein und fasst ihn nicht an.

Was du tust: **deine Flächen darauf abstimmen.** Der Hintergrund ist dunkel, violett bis
orange, in Bewegung. Deine Flächen brauchen genug Deckkraft, dass Zahlen scharf bleiben.
`backdrop-filter: blur(14px)` mit einer halbdurchsichtigen Fläche ist ein guter
Ausgangspunkt — aber prüfe den Kontrast über der *hellsten* Stelle des Hintergrunds,
nicht über der dunkelsten.

---

## 10 · Bevor du fertig bist

Geh diese Liste durch. Sie fängt die Fehler, die am häufigsten passieren:

- [ ] Alle zehn `id`-Abschnitte da, in der richtigen Reihenfolge
- [ ] Kopf- und Fußblock zeichengenau übernommen
- [ ] `<svg id="modell-marke">` vorhanden, quadratisch, unter 4 KB
- [ ] **Jede Zahl steht so in `run.json`** — nachgezählt, nicht geschätzt
- [ ] Wo `null` steht, steht auf der Seite „nicht gemessen"
- [ ] Jedes `<img>`, `<video>`, `<iframe>` hat `width` und `height`
- [ ] Bei 390 px Breite kein waagerechtes Scrollen
- [ ] Alle Trefflächen mindestens 44 px hoch
- [ ] Sichtbarer Fokus auf allen Links und Knöpfen
- [ ] `prefers-reduced-motion` beachtet
- [ ] Kein fremdes Skript, kein fremder Stil außer Google Fonts
- [ ] Kontrast im Fließtext mindestens 4,5 : 1 über der hellsten Hintergrundstelle
- [ ] **Jeder Textblock hat eine eigene Fläche** — kein Text direkt auf dem Hintergrund
