Design-System: Fokus vereinheitlichen, Formularfeld-Ring WCAG-fest machen

Letzter Teil. base.css hatte längst eine globale :focus-visible-Regel, trotzdem
existierten 15 Fokus-Regeln — die meisten davon Ballast.

Fünf Overrides wiederholten die Basisregel praktisch wortgleich (.back-to-top,
.ad-banner__dot, .faq__q, .table-scroll, .btn) und sind ersatzlos entfallen; sie
unterschieden sich höchstens um 1px Offset. Die Basisregel selbst läuft jetzt
über --focus-ring/--focus-offset, .position-card__head wechselt von Akzentrot
auf Weiß. Übrig bleiben zehn Regeln, davon deklarieren nur noch vier überhaupt
eine outline — jede mit Grund (inset-Offset bei .tab und .position-card__head,
geschichteter Ring beim Formularfeld). Die anderen sechs sind reine Zusatz-
effekte (transform, Farbe, Fläche) und lassen den Basis-Ring durch.

Der eigentliche Fund steckte im Formularfeld: es setzte outline: none und
ersetzte den Ring durch einen roten Rahmen mit 30%-Glow. Akzentrot erreicht auf
der Feldfläche aber nur 2,8:1 und verfehlt damit WCAG 1.4.11 (>=3:1) für
Fokus-Indikatoren — Tastaturnutzer hatten dort also den schwächsten Fokus der
ganzen Seite. Roter Rahmen und Glow bleiben als Marken-Signal, der weiße
Standard-Ring kommt zurück; outline-offset entspricht der Glow-Breite, beide
Ringe liegen bündig nebeneinander statt übereinander.

CLAUDE.md dokumentiert die neuen Regeln: Token-Disziplin mit Tabelle (Spacing,
Typo, Icons, Sperrung, Fokus, Trefferfläche) samt der bewussten em-Ausnahme,
das Button- und Tab-System inklusive der (0,2,0)-Falle durch die
CSS-Ladereihenfolge, und die Fokus-Regel. Dazu ein Punkt in der Checkliste.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSsVdooFLgPA8gPFp7HwZU
This commit is contained in:
2026-07-27 10:33:53 +02:00
parent eacf9449e0
commit 82cf21085d
5 changed files with 54 additions and 33 deletions

View File

@@ -138,10 +138,51 @@ Sichtbare FAQ + FAQPage-Schema aus derselben `data/*.json`-Quelle, damit Inhalt
- Headings: Coolvetica, uppercase, line-height ~0.85. Body: Abel. Beide self-hosted als woff2. - Headings: Coolvetica, uppercase, line-height ~0.85. Body: Abel. Beide self-hosted als woff2.
- **Coolvetica nie kleiner als `--fs-650` (28px)** — der schmale Condensed-Schnitt wird in Uppercase - **Coolvetica nie kleiner als `--fs-650` (28px)** — der schmale Condensed-Schnitt wird in Uppercase
darunter unleserlich (mehrfach bestätigt: Footer-Headings, Timeline-Nav). Für kleinere Auszeichnungen darunter unleserlich (mehrfach bestätigt: Footer-Headings, Timeline-Nav). Für kleinere Auszeichnungen
Abel verwenden, ggf. mit `letter-spacing` + uppercase. Abel verwenden, ggf. mit `letter-spacing` + uppercase. `base.css` setzt das durch: `h1``h3` tragen
Coolvetica, **`h4``h6` laufen über `--ff-body` + uppercase + `--ls-caps`**.
- Rot nie für Fließtext auf dunklem Grund (Kontrast!) — nur als Button-/Akzent-Fläche mit weißem Text. - Rot nie für Fließtext auf dunklem Grund (Kontrast!) — nur als Button-/Akzent-Fläche mit weißem Text.
- Alle Werte als CSS Custom Properties in `tokens.css`; Breakpoints: 1249 (Burger) / 1024 / 992 / 768 / 600 / 500. - Alle Werte als CSS Custom Properties in `tokens.css`; Breakpoints: 1249 (Burger) / 1024 / 992 / 768 / 600 / 500.
### Token-Disziplin (gilt für jede CSS-Änderung)
Keine rohen Werte in `base/layout/components/utilities.css` — es gibt für alles eine Stufe:
| Was | Token | Anmerkung |
|---|---|---|
| `padding` / `margin` / `gap` | `--space-4xs … --space-7xl` (14 Stufen, 4px-Raster) | `--space-4xs` (2px) **nur** für optische Korrekturen (Icon-/Baseline-Nudges), nie fürs Layout |
| `font-size` (Text) | `--fs-300 … --fs-1000` | `--fs-550`/`--fs-690` sind die fluiden Zwischenstufen (Lead, Karten-Überschrift) |
| `font-size` (Icons) | `--icon-s/m/l/xl` (rem), `--icon-inline-sm/--icon-inline/--icon-inline-lg` (em) | em-Stufen für Icons, die mit ihrer Textzeile mitwachsen sollen |
| `letter-spacing` | `--ls-tight/caps/wide/wider` | |
| Fokus | `--focus-ring` / `--focus-offset` | |
| Trefferfläche | `--tap-min` (44px) | |
**Ausnahmen, bewusst roh:** `em`-Werte, die mit der `font-size` skalieren sollen (Button-Padding
`--btn-pad-*`, Badge-Innenabstände) — die gehören nicht auf ein festes px-Raster.
### Buttons & Trefferflächen
- Varianten in `utilities.css`: `.btn` (primary) · `.btn--outline` · `.btn--ghost` (tertiär, ohne Fläche)
· `.btn--icon` (quadratisch, nur Icon + `aria-label`) · `.btn--sm` / `.btn--lg`.
`.btn` ist `inline-flex` mit `gap` — Icon + Label brauchen **kein** Zusatz-Markup und keinen eigenen Abstand.
- Tabs (Filterleiste wie Panel-Tabs) laufen über die gemeinsame **`.tab`**-Basis in `components.css`;
aktiv wird über `aria-pressed` **oder** `aria-selected` erkannt. Nie eine dritte Tab-Optik bauen.
- **Jedes Control ≥ `--tap-min` (44px, WCAG 2.5.5).** Soll es optisch kleiner bleiben, streckt
`.tap-target` die Trefferfläche unsichtbar per `::after` (bei `.btn--sm` automatisch). Voraussetzung:
kein `overflow: hidden`, `::after` frei. In Reihen (Nav, Footer) stattdessen `min-height` verwenden —
überlappende Pseudo-Flächen würden sonst den Nachbarn verdecken.
- Ausnahme: die Formular-Checkbox bleibt bei 24px (WCAG 2.5.8). `<input>` ist ein Replaced Element,
`::after` greift dort nicht zuverlässig; das klickbare `<label>` liefert die große Fläche.
- **Komponenten-Deltas zu `.btn` brauchen (0,2,0)** — z. B. `.btn.ad-banner__nav-btn`. `utilities.css`
lädt nach `components.css`, bei gleicher Spezifität gewinnt sonst die `.btn`-Basis.
### Fokus
`base.css` setzt **eine** globale `:focus-visible`-Regel mit `--focus-ring` (2px weiß, 15,9:1 auf
`--clr-bg`). Komponenten überschreiben sie **nur** mit Begründung — zulässig sind ein negativer
`outline-offset` (Ring soll inset liegen, z. B. `.tab`) und zusätzliche Effekte ohne eigene `outline`.
**Nie `outline: none`** und nie Akzentrot als alleiniger Indikator: Rot erreicht auf der Feldfläche nur
2,8:1 und verfehlt WCAG 1.4.11 (≥3:1).
## Icons ## Icons
- **Bootstrap Icons** (MIT), lokal & self-hosted unter `public/assets/icons/<name>.svg` — die erlaubte Icon-Quelle. Kein Icon-Font, kein CDN, keine gezeichneten CSS-/Unicode-Icons. - **Bootstrap Icons** (MIT), lokal & self-hosted unter `public/assets/icons/<name>.svg` — die erlaubte Icon-Quelle. Kein Icon-Font, kein CDN, keine gezeichneten CSS-/Unicode-Icons.
@@ -155,6 +196,8 @@ Sichtbare FAQ + FAQPage-Schema aus derselben `data/*.json`-Quelle, damit Inhalt
- [ ] Alt-Texte für Bilder; dekorative Bilder `alt=""` - [ ] Alt-Texte für Bilder; dekorative Bilder `alt=""`
- [ ] Tastatur-bedienbar, `:focus-visible` sichtbar, sinnvolle Tab-Reihenfolge - [ ] Tastatur-bedienbar, `:focus-visible` sichtbar, sinnvolle Tab-Reihenfolge
- [ ] Kontrast ≥ 4.5:1 (Fließtext) / 3:1 (große Schrift, UI) - [ ] Kontrast ≥ 4.5:1 (Fließtext) / 3:1 (große Schrift, UI)
- [ ] Keine rohen Werte: Spacing, `font-size`, `letter-spacing`, Fokus und Trefferflächen über die
Token aus `tokens.css` (siehe „Token-Disziplin"). Interaktive Elemente ≥ `--tap-min`
- [ ] `prefers-reduced-motion` respektiert (globaler Kill-Switch in reset.css) - [ ] `prefers-reduced-motion` respektiert (globaler Kill-Switch in reset.css)
- [ ] `$meta` gesetzt: title, description (+ og_image falls abweichend) - [ ] `$meta` gesetzt: title, description (+ og_image falls abweichend)
- [ ] Bilder: `srcset`/`sizes`, `width`/`height`, `loading="lazy"` (above-the-fold: eager + fetchpriority). - [ ] Bilder: `srcset`/`sizes`, `width`/`height`, `loading="lazy"` (above-the-fold: eager + fetchpriority).

View File

@@ -80,7 +80,6 @@ h4 { font-size: var(--fs-650); }
h5 { font-size: var(--fs-600); } h5 { font-size: var(--fs-600); }
h6 { font-size: var(--fs-500); } h6 { font-size: var(--fs-500); }
a { a {
color: inherit; color: inherit;
text-decoration-thickness: 1px; text-decoration-thickness: 1px;
@@ -92,8 +91,8 @@ a:hover {
} }
:focus-visible { :focus-visible {
outline: 2px solid var(--clr-text); outline: var(--focus-ring);
outline-offset: 3px; outline-offset: var(--focus-offset);
border-radius: 2px; border-radius: 2px;
} }

View File

@@ -1106,7 +1106,7 @@
} }
.position-card__head:focus-visible { .position-card__head:focus-visible {
outline: 2px solid var(--clr-accent); outline: var(--focus-ring);
outline-offset: -2px; outline-offset: -2px;
} }
@@ -1342,11 +1342,16 @@
border-color: var(--clr-field-border-hover); border-color: var(--clr-field-border-hover);
} }
/* Markenroter Fokus-Ring (Akzent nur als Rahmen/Glow, nie als Text). */ /* Roter Rahmen + Glow als Marken-Signal, dazu der weiße Standard-Ring.
Der rote Rahmen allein reichte nicht: Akzentrot kommt auf der Feldfläche nur
auf 2,8:1 und verfehlt damit WCAG 1.4.11 (>=3:1) für Fokus-Indikatoren.
outline-offset entspricht der Glow-Breite, beide Ringe liegen also bündig
nebeneinander statt übereinander. */
.form__field input:focus-visible, .form__field input:focus-visible,
.form__field select:focus-visible, .form__field select:focus-visible,
.form__field textarea:focus-visible { .form__field textarea:focus-visible {
outline: none; outline: var(--focus-ring);
outline-offset: var(--focus-offset);
border-color: var(--clr-accent); border-color: var(--clr-accent);
box-shadow: 0 0 0 3px rgb(var(--clr-accent-rgb) / 0.3); box-shadow: 0 0 0 3px rgb(var(--clr-accent-rgb) / 0.3);
} }
@@ -1818,11 +1823,6 @@
background: rgb(255 255 255 / 0.35); background: rgb(255 255 255 / 0.35);
} }
.ad-banner__dot:focus-visible {
outline: 2px solid var(--clr-text);
outline-offset: 3px;
}
.ad-banner__progress { .ad-banner__progress {
background: var(--clr-accent); background: var(--clr-accent);
transform: scaleX(0); transform: scaleX(0);
@@ -2524,11 +2524,6 @@
color: var(--clr-text-muted); color: var(--clr-text-muted);
} }
.faq__q:focus-visible {
outline: 2px solid var(--clr-text);
outline-offset: 2px;
}
.faq__chevron { .faq__chevron {
flex: none; flex: none;
font-size: var(--icon-s); font-size: var(--icon-s);
@@ -3695,7 +3690,6 @@
font-size: var(--fs-600); font-size: var(--fs-600);
} }
/* Band „Nächste Spiele" — abgesetzte Fläche (.section--sunken) mit roter Oberkante, /* Band „Nächste Spiele" — abgesetzte Fläche (.section--sunken) mit roter Oberkante,
damit der Bereich nicht als Hero-Überhang gelesen wird. */ damit der Bereich nicht als Hero-Überhang gelesen wird. */
.next-matches { .next-matches {
@@ -3942,11 +3936,6 @@ a.match-row__link:hover {
} }
} }
.table-scroll:focus-visible {
outline: 2px solid var(--clr-text);
outline-offset: 2px;
}
.league-table { .league-table {
width: 100%; width: 100%;
min-width: 520px; min-width: 520px;

View File

@@ -537,11 +537,6 @@
border-color: var(--clr-accent); border-color: var(--clr-accent);
} }
.back-to-top:focus-visible {
outline: 2px solid var(--clr-text);
outline-offset: 3px;
}
@media print { @media print {
.back-to-top { .back-to-top {
display: none; display: none;

View File

@@ -56,11 +56,6 @@
color: var(--clr-text); color: var(--clr-text);
} }
.btn:focus-visible {
outline: var(--focus-ring);
outline-offset: 2px;
}
.btn:active { .btn:active {
transform: translateY(1px); transform: translateY(1px);
} }