diff --git a/CLAUDE.md b/CLAUDE.md index 3a361b5..952a054 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,6 +38,7 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern. | Startseiten-Inhalte | `data/home.json` | Texte in `pages/home.php` | | Partner/Sponsoren | `data/partners.json` | — | | Instagram-Cache | `data/instagram.json` + `public/assets/img/instagram/` | **maschinenverwaltet von `bin/instagram-sync.php` — nie von Hand editieren** | +| Icons | `public/assets/icons/` (Bootstrap Icons, lokal) via `icon()`-Helper | Inline-SVG-Pfade von Hand ins Markup, Emoji/Unicode-Glyphen, gezeichnete CSS-Icons, Icon-Font/CDN | Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pages/*.php`) im `$meta`-Array. @@ -50,7 +51,8 @@ Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pag prüfen ob eine Komponente existiert und erweitert werden kann. Wiederkehrende Inhalte auf neuen Seiten → als Komponente extrahieren. - **Helpers** (`app/helpers.php`): `e()` (Escaping — IMMER für dynamische Ausgaben), `url()`, `asset()` - (Versionierung via filemtime), `json_load('name')` (liest `data/name.json`, static cache), `component()`. + (Versionierung via filemtime), `json_load('name')` (liest `data/name.json`, static cache), `component()`, + `icon('name', 'klasse', 'label?')` (Inline-SVG aus `public/assets/icons/`, siehe Abschnitt Icons). - **CSS:** 6 Dateien als `` in fester Reihenfolge: tokens → reset → base → layout → components → utilities. Kein `@import`, kein Inline-Style. Komponenten-Styles in `components.css` mit Banner-Kommentaren (`/* === hero === */`). @@ -61,6 +63,17 @@ Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pag ## Formulare +- **Felder ausschließlich über `component('form-field', ['field' => …])`** (`app/components/form-field.php`) — + nie Feld-Markup von Hand. Die Komponente liefert Label, Pflichtfeld-Stern, Hilfetext und das + Fehler-Element `#-error` (per `aria-describedby` verknüpft) in einheitlichem Markup. +- **Jedes neue Formular braucht:** (1) Status-Region `role="status" aria-live="polite" tabindex="-1" + data-form-status` als erstes Form-Kind, (2) Hinweiszeile „Pflichtfelder sind mit * markiert“, + (3) Honeypot-Feld `website` (visually-hidden) + Hidden-Field `ft` mit `form_token()`, + (4) serverseitige Action unter `app/actions/` mit Whitelist-Validierung + PRG-Redirect. +- `form.js` übernimmt automatisch Inline-Feldfehler (Browser-Validierungsmeldung, `aria-invalid`, + Fokus aufs erste ungültige Feld) — Konvention dafür ist die `#-error`-ID aus der Komponente. +- A11y-Vorgaben: Feldränder mit `--clr-glass-border-strong` (≥3:1 Kontrast, WCAG 1.4.11), Checkbox + min. 24px (WCAG 2.5.8), Status-Region nie per `display:none` verstecken (fliegt aus dem Accessibility-Tree). - Versand via PHPMailer über Brevo SMTP (`smtp-relay.brevo.com:587`, STARTTLS), Config aus `config/config.php`. - Spam-Schutz ohne externe Dienste: Honeypot-Feld + HMAC-signierter Timestamp (Time-Trap, `app_secret`) + serverseitige Validierung mit Whitelists. POST-Redirect-GET; `form.js` macht optional fetch. @@ -74,6 +87,13 @@ Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pag - 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. +## Icons + +- **Bootstrap Icons** (MIT), lokal & self-hosted unter `public/assets/icons/.svg` — die erlaubte Icon-Quelle. Kein Icon-Font, kein CDN, keine gezeichneten CSS-/Unicode-Icons. +- Ausgabe **nur** über den Helper: `icon('pause-fill')` (dekorativ, `aria-hidden`), `icon('pause-fill', 'meine-klasse')` (mit Klasse), `icon('envelope', '', 'E-Mail')` (semantisch, `role="img"` + `aria-label`). Inline-SVG, currentColor erbt die Schriftfarbe. +- Größe = `font-size` des Elements (Icon ist `1em`), Farbe = `currentColor`. Basisklasse `.icon` in `utilities.css`. +- Nur tatsächlich genutzte Icons werden abgelegt (kein Komplett-Set). Neues Icon: `php bin/icons-add.php ` (Name = Dateiname auf icons.getbootstrap.com ohne `.svg`). + ## Checkliste für jede neue Seite/Komponente - [ ] Semantisches HTML (Landmarks, Headings-Hierarchie ohne Sprünge) diff --git a/public/assets/css/components.css b/public/assets/css/components.css index ffc393b..12ee397 100644 --- a/public/assets/css/components.css +++ b/public/assets/css/components.css @@ -331,7 +331,7 @@ width: 100%; padding: 10px 12px; background: var(--clr-glass-bg); - border: 1px solid var(--clr-glass-border); + border: 1px solid var(--clr-glass-border-strong); border-radius: var(--radius-btn); color: var(--clr-text); transition: border-color var(--transition), background var(--transition), box-shadow var(--transition); @@ -340,7 +340,7 @@ .form__field input:hover, .form__field select:hover, .form__field textarea:hover { - border-color: rgb(255 255 255 / 0.4); + border-color: rgb(255 255 255 / 0.55); } /* Eigener, deutlich sichtbarer Fokus-Ring statt des globalen Outlines */ @@ -416,19 +416,25 @@ } .form__field--checkbox input { - width: 1.25rem; - height: 1.25rem; - margin-top: 0.2rem; + /* 24px — Mindest-Zielgröße (WCAG 2.5.8) */ + width: 1.5rem; + height: 1.5rem; + margin-top: 0.1rem; accent-color: var(--clr-accent); } .form__field--checkbox label { - font-size: var(--fs-400); - color: var(--clr-text-muted); + font-size: var(--fs-500); } +/* Leer: unsichtbar, aber im Accessibility-Tree — display:none würde die + aria-live-Region abmelden und Screenreader verpassen die erste Meldung. */ .form__status:empty { - display: none; + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); } .form__success { @@ -601,25 +607,21 @@ outline-offset: 3px; } -/* Pause-Symbol (zwei Balken) im Abspiel-Zustand … */ -.ad-banner__toggle::before { - content: ""; - display: block; - width: 4px; - height: 13px; - border-left: 4px solid currentColor; - border-right: 4px solid currentColor; +.ad-banner__toggle-icon { + font-size: 1.15rem; } -/* … Play-Dreieck, wenn pausiert. */ -.ad-banner__toggle[aria-pressed="true"]::before { - width: 0; - height: 0; - margin-left: 3px; - border: 0; - border-left: 13px solid currentColor; - border-top: 8px solid transparent; - border-bottom: 8px solid transparent; +/* Pause-Icon im Abspiel-Zustand, Play-Icon wenn pausiert. */ +.ad-banner__toggle .ad-banner__toggle-icon--play { + display: none; +} + +.ad-banner__toggle[aria-pressed="true"] .ad-banner__toggle-icon--pause { + display: none; +} + +.ad-banner__toggle[aria-pressed="true"] .ad-banner__toggle-icon--play { + display: inline-block; } /* Indikatorleiste — die aktive Leiste füllt sich über --banner-interval. */ diff --git a/public/assets/css/tokens.css b/public/assets/css/tokens.css index 2eb6626..3d91882 100644 --- a/public/assets/css/tokens.css +++ b/public/assets/css/tokens.css @@ -18,6 +18,7 @@ --clr-text-faint: #8c8c8c; /* 4.5:1 auf --clr-bg (AA) */ --clr-glass-bg: rgb(255 255 255 / 0.11); --clr-glass-border: rgb(255 255 255 / 0.22); + --clr-glass-border-strong: rgb(255 255 255 / 0.4); /* Formularfelder: ≥3:1 Kontrast (WCAG 1.4.11) */ --clr-error: #ff8a8a; /* Formularfehler, lesbar auf dunkel */ --clr-success: #8ce99a;