Commit Graph

119 Commits

Author SHA1 Message Date
fs
f21224ea84 Nav-Labels an die Seitentitel angeglichen, Dropdown-Breite nachgerechnet
Im Menü stand „Eltern-Kind-Turnen", die Seite heißt „Eltern-Kind- &
Kinderturnen" — das Kinderturnen fehlte also im Menü, obwohl Eltern genau
danach suchen. Beim Prüfen fielen zwei weitere Abweichungen auf, alle drei
sind jetzt gleich:

  Eltern-Kind-Turnen  → Eltern-Kind- & Kinderturnen
  Fitness und Pilates → Fitness & Pilates
  Trampolin           → Trampolinturnen

Punkte („…") braucht es dafür nicht: das Dropdown hat min-width und
white-space: nowrap, das Panel wächst also mit dem längsten Punkt statt zu
beschneiden. Ein abgeschnittener Menüpunkt wäre auch die schlechtere Lösung,
weil man dann nicht mehr liest, wohin der Link führt.

Es war aber knapper als es aussah. Gemessen mit dem längsten Punkt (187,6px):
ruhend 190,0px Platz, im Hover UND auf der aktiven Seite nur 186,0px, weil
padding-left dort von --space-s (16px) auf --space-m (20px) wächst. Das Panel
wäre beim Rübergehen um zwei Pixel gewachsen und auf dieser Seite dauerhaft
breiter gewesen — sichtbar als Flackern, weil es mittig sitzt und sich nach
beiden Seiten schiebt.

Behoben mit einem Wert: min-width 15rem → 15,5rem. Alle drei Panels bleiben
jetzt in jedem Zustand bei 248px, mit 8,4px Luft statt 1,6px Fehlbetrag. Auch
bei 1250px Viewport (der schmalsten Breite mit Desktop-Menü) liegt kein Panel
am Rand. Die Rechnung steht als Kommentar über der Regel, damit der nächste
längere Menüpunkt nachgemessen wird.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 22:31:58 +02:00
fs
592acf02b0 Turn-Anmeldung als eigene FAQ-Frage, Knöpfe in FAQ-Antworten möglich
Der Anmelde-Vorgang der Abteilung (erst Platz anfragen, dann Aufnahmeantrag)
stand nirgends auf der Seite. Auf der alten Seite gab es dafür einen Block
„ANMELDUNG" mit zwei Knöpfen.

Erst als eigene Sektion unter den Beiträgen gebaut (cta-band), auf Wunsch von
Felix wieder entfernt und stattdessen als eigene FAQ-Frage umgesetzt. Zwei
Sektionen mit denselben Knöpfen auf einer Seite schwächen beide Stellen, und
man weiß nicht mehr, welcher der richtige Weg ist. Deshalb ist auch
overview.cta ganz aus data/turnen.json raus statt unbenutzt liegen zu bleiben —
genau so ein toter Block hat heute schon einmal in die Irre geführt (die Notiz
„NOCH unbenutzt" stand monatelang im Docblock von turnen.php).

faq.php bekommt dafür ein optionales ctas-Feld pro Eintrag, gerendert über die
bestehende cta-Komponente, also keine neue Knopf-Variante. .faq__actions
bricht um, weil zwei Knöpfe auf einem schmalen Handy nicht nebeneinander passen.

Die Antwort nennt beide Wege bewusst AUCH in Worten: faq_schema() liest nur q
und a, Knöpfe landen nie im FAQPage-Schema. Stünde in der Antwort nur „so
geht's" plus zwei Knöpfe, hätte Google eine Antwort ohne Ziel. Nachgeprüft:
sechs Fragen im Schema, der neue Antworttext ist HTML- und Markup-frei.

Gemessen bei 390px: kein Überlauf, Knöpfe 130x44 und 193x44 (Trefferfläche
erfüllt). Die fünf Turn-Unterseiten hatten als einzige von 105 PHP-Dateien
keinen Zeilenumbruch am Dateiende, das ist mitkorrigiert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 22:31:57 +02:00
fs
63a045835e Austritt Turnen: Kündigung darf formlos per Mail kommen
Rückmeldung der Turnabteilung. Der Austritt-Block auf /turnen nannte bisher nur
die Schriftform und die Frist zum 30. September, nicht aber, wohin die Kündigung
geht.

Die Adresse steht wie überall ausschließlich in club.json (email_turnen), der
Satz in turnen.json markiert ihre Stelle nur mit {mail} — dasselbe Muster wie
der Material-Aufruf auf /historie. Ersetzt wird nach inline_links_html(), damit
der Fließtext escaped bleibt und nur der Platzhalter zu einem mailto-Link wird.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWoqDYuANFbJDGFQAgaRdT
2026-07-31 22:26:37 +02:00
fs
06a18a2d19 301-Weiterleitungen der alten CMS-Adressen
Der Aufnahmeantrag der Turnabteilung ging als alte URL in den Einladungen an
die neuen Familien raus und lief seit dem Umzug in einen 404, ebenso der Link
auf die Turn-Seite. Beide leiten jetzt weiter, dazu alle uebrigen Seiten der
alten Navigation unter /08/de/.

Die PDF-Regel matcht am Umlaut vorbei: der Dateiname kommt je nach Mailprogramm
als NFC, als NFD (so stand er in der alten Datenbank) oder roh als UTF-8 an.
Die Sammelregel wirft den alten Query-String weg, sonst haengt mod_rewrite
?group_filter_id=14 ans neue Ziel.

Erledigt damit den offenen Punkt F3 aus docs/deploy.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWoqDYuANFbJDGFQAgaRdT
2026-07-31 22:11:08 +02:00
fs
e7572b34d6 Trampolin: Schnupper-Einladung, Hinweise bekommen zwei Tonlagen
Info von Eva aus der Trampolin-Gruppe: neue Kinder können ab September
(Schulbeginn) gerne schnuppern kommen. Die Info stand nirgends.

Rot wäre dafür falsch gewesen. Die Farbe bekommt deshalb eine Bedeutung:
--stop (gefüllt rot) heißt „geht nicht", --info (Glas-Fläche, Rot nur im Icon)
heißt „geht". Eine Einladung in Alarmrot liest sich wie eine Absage, und wenn
alles rot ist, stumpft Rot ab und der echte Wartelisten-Stopp bei „Tiger" und
„Hühner" verliert seine Wirkung. --info ist ab jetzt der Default.

Damit steht der Hinweis an drei Stellen, also greift die Bedingung aus dem
letzten Commit („kommt ein dritter Ort dazu, gehören die gemeinsamen
Eigenschaften in eine geteilte Klasse"): neu app/components/notice.php als
einzige Render-Stelle statt dreimal dasselbe Markup. Die beiden bestehenden
Klassen schrumpfen auf das, was sie wirklich beitragen —
.turnen-dash__notice nur noch grid-area, .contact__notice nur noch die
Formularbreite. Die tote .turnen-dash__notice-icon-Regel ist raus, der
CSS-Abschnitt neu sortiert (notice als eigener Block hinter turnen-dash statt
mitten hinein).

Text, Tonlage und Icon stehen wie gehabt in data/turnen.json bei der
Disziplin, nicht im Template. Kinderturnen trägt jetzt explizit
notice_tone: stop, Trampolin nichts und ist damit neutral.

Das Grid-Template für --notice + --single gab es schon, an beiden
Breakpoints — die Trampolin-Seite hat eine Gruppe und wäre sonst
auseinandergefallen.

Gemessen bei 390px: kein Überlauf auf allen drei Seiten, Trampolin-Fläche
rgba(0,0,0,0.22) (Glas), die beiden anderen #e20612. Zwölf Seiten geprüft,
alle 200. preflight ohne neue Meldungen.

OFFEN, mit Eva oder Tina zu klären: der zweite Satz („Schreib uns davor
einfach kurz eine Nachricht") ist von mir, Eva hat ihn nicht gesagt. Wenn die
Kinder einfach zum Training kommen dürfen, gehört er weg. Und die Einladung
steht bisher nur auf /turnen/trampolin, nicht auf der Übersicht /turnen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 22:04:41 +02:00
fs
921a55635c Wartelisten-Hinweis für Tiger und Hühner, über dem Anfrageformular
Die Turnabteilung (Maxi) beantwortet laufend Anfragen nach einem Wartelistenplatz
für „Tiger" und „Hühner", den es nicht gibt. Auf der alten Seite stand der Hinweis
direkt über dem Anfrageformular, hier fehlte er.

Schlimmer: an der Stelle stand das GEGENTEIL. Der bisherige Hinweis lautete
"Aktuell sind alle Kinderturngruppen belegt. Bei Interesse setzen wir sie gerne
auf die Warteliste." — für alle Kindergruppen, also auch für die beiden, die
keine Liste führen. Der Satz hat einen Teil der Anfragen vermutlich erst erzeugt.
Er ist nicht gelöscht, sondern auf die übrigen Gruppen eingegrenzt und aufs Du
umgestellt (Haus-Stil; vorher stand dort klein "sie").

Der Text steht genau EINMAL, in data/turnen.json unter
disciplines.eltern-kind-turnen.notice + notice_title. Zwei Stellen lesen ihn:
die rote Kachel auf /turnen/eltern-kind-turnen (gab es schon) und neu ein Block
direkt über dem Formular auf /turnen. Ändert die Abteilung die Lage, ist das eine
Zeile in einer Datei.

Umsetzung:
- notice_title ist neu, weil eine rote Fläche ohne Überschrift wie eine
  Fehlermeldung liest. Abel statt Coolvetica, der Schnitt ist bei ~19px in
  Versalien unleserlich (CLAUDE.md, Design).
- Der Block steht AUSSERHALB des <form> und damit außerhalb der
  role="status"-Region: ein dauerhafter Hinweis, keine Rückmeldung zum Absenden.
  In der Live-Region läse ihn ein Screenreader bei jeder Formularmeldung erneut
  vor. Kein role="alert", er steht schon beim Laden da.
- Überschrift als h3 unter der h2 des Intro-Blocks, damit die Folge nicht springt.
- .contact__notice übernimmt bewusst die Optik von .turnen-dash__notice, damit
  beide Stellen als derselbe Hinweis erkennbar sind. Weiß auf --clr-accent
  erreicht 4,92:1 und trägt damit Fließtext (WCAG 1.4.3). Kommt ein dritter Ort
  dazu, gehören die gemeinsamen Eigenschaften in eine geteilte .notice-Klasse;
  für zwei wäre das verfrüht.

Gemessen: Hinweisbreite gleich Formularbreite (720px bei 1280, 347px bei 390),
kein Überlauf. preflight ohne neue Meldungen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:54:10 +02:00
fs
25f36e5a53 Karten-Innenabstände fluid: mehr Platz auf schmalen Handys
Auf schmalen Bildschirmen stapeln sich Container- und Kartenabstand so, dass für
den Inhalt kaum Breite bleibt. Gemessen bei 309px: Container 11,5 + Karte 32 je
Seite, das Eingabefeld hatte 220px, also 71% der Bildschirmbreite.

Kein harter Breakpoint ("ab 310px vollflächig"), sondern fluide Abstände — ein
Umbruch bei 310px sähe bei 311px wieder falsch aus. Das Muster gab es im Projekt
schon: fee-table, membership-card, legal__card und matchcenter-empty nutzen
clamp()-Abstände. Fest waren genau die drei, über die es stolperte: .form (32px),
.group-card (32px) und .group-tabs__panel (40px).

Zwei neue Token in tokens.css als einzige Quelle, --pad-card (20…32px) und
--pad-card-lg (24…40px). Die OBERGRENZEN sind die bisherigen festen Werte, am
Desktop ändert sich also nichts — nachgemessen: 32px bei 1269px Viewport, wie
vorher. Es wird nur nach unten schmaler.

Ergebnis: Feldbreite bei 309px von 71% auf 76%, bei 379px von 74% auf 80%.
Kein Überlauf auf /turnen, den Disziplinseiten und /kontakt bei 320px und 1280px.

Die vier bereits fluiden Karten behalten ihre eigenen clamp()-Werte, damit ihr
Desktop-Bild unangetastet bleibt; sie in die Token einzusammeln ist eine
Aufräumung für später.

VORBESTEHEND und nicht von dieser Änderung verursacht (per git stash gegengeprüft,
mit und ohne Änderung identisch): /mitglied-werden läuft bei 320px um 12px über,
/turnen bei 300px um 13px. Die 320px sind eine echte Gerätebreite (iPhone SE),
das gehört als Nächstes angeschaut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:34:29 +02:00
fs
6a9244d847 Turn-Gruppenkarten: fr-Spuren begrenzen (Textumbruch bei 300px)
Bei sehr schmalen Breiten stand der Text über den Kartenrand hinaus. Gemessen bei
300px Viewport (clientWidth 289): Dokument 317px, also 28px Überlauf; die einzelne
Grid-Spur des Panels rechnete sich auf 267,7px aus, obwohl die Karte nur 193px
Inhaltsfläche hat.

Ursache: `grid-template-columns: 1.6fr 1fr` bzw. `1fr` — eine fr-Spur hat als
automatisches Minimum min-content und kann deshalb nicht schmaler werden als das
längste Wort darin. Bei deutschen Komposita ("sportartenübergreifende",
"Grundlagenausbildung") sprengt das die Karte. Jetzt minmax(0, …) in beiden
Spurdefinitionen von .group-tabs__panel, und dieselbe Korrektur bei
.group-list--stacked, wo das Muster latent drinsteckte.

Zwei Umwege, die ich mir hätte sparen können: min-width: 0 am Grid-KIND reicht
nicht, die SPUR muss das Schrumpfen erlauben. Und overflow-wrap: break-word
verkleinert die min-content-Breite nicht (nur `anywhere` täte das) — beides bleibt
trotzdem drin, zusammen mit hyphens: auto, weil die Seite lang="de" ist und der
Browser die Komposita dann sauber trennt statt hart umzubrechen.

Geprüft: alle sechs Turn-Seiten bei 300px, 320px und 390px ohne Überlauf. Desktop
unverändert zweispaltig (648,6 / 405,4 px, Verhältnis 1,6:1).

OFFEN und bewusst nicht mitgefixt: /turnen läuft bei 300px noch um 13px über, ab
320px nicht mehr. Es kommt aus dem Kontaktformular, nicht aus den Beitragskarten
oder Gruppenkarten — die min-content-Breite des Formulars in der Grid-Spur von
.container. .container global anzufassen ist mir für 13px auf einer Breite, die
kein Gerät hat, zu riskant (das schmalste verbreitete Display ist 320px).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:27:56 +02:00
fs
8c15b2584b Flamingos: Chip gekürzt und Ursache des Mobil-Überlaufs behoben
Rückmeldung der Turnabteilung mit Screenshot: auf dem iPhone war im
Flamingos-Panel der Fließtext rechts abgeschnitten.

Gemessen bei 390px mit aktivem Flamingos-Tab (die anderen Panels sind
ausgeblendet, ihre Chips haben Breite 0 — deshalb fiel es beim Prüfen der Seite
nicht auf): Dokument 442px bei 379px Bildschirm, also 63px Überlauf; der Chip
393px breit in einer 345px-Karte.

Ursache war nicht die Textlänge allein, sondern .group-card__level mit
`white-space: nowrap` UND `flex-shrink: 0`: der Chip konnte seinen Text nicht
brechen und auch nicht schrumpfen, seine Mindestbreite spannte die ganze Karte
auf. Von vier level-Chips war nur dieser betroffen (47 Zeichen gegen 17 bis 25).

Zwei Änderungen:
- Inhalt wie gewünscht gekürzt: "Turnen · fortgeschritten · Alter nach Absprache"
  → "Turnen · fortgeschritten".
- Sicherheitsnetz im CSS: nowrap raus, max-width: 100% rein. Ein Chip trägt
  Information, deshalb umbrechen statt mit … kürzen. flex-shrink: 0 bleibt, es
  hält den Chip neben einem kurzen Gruppennamen in Form.

Gegenprobe, damit der Fix nicht nur am kürzeren Text hängt: mit dem LANGEN Text
und dem CSS-Fix bleibt das Dokument bei 379px, der Chip bricht auf zwei Zeilen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:17:24 +02:00
fs
c4ffe4a1f4 Turn-Anfrageformular: Feld „Geburtsdatum des Kindes" wieder da
Auf der alten Seite gab es das Feld, beim Neubau ist es untergegangen. Gegen
2024.tsv08kulmbach.de geprüft: dort hieß es input_alter_kind, war type="date",
OPTIONAL und trug das Label „Geburtsdatum des Kindes" — genau so wieder
eingebaut. Die Abteilung braucht es, weil die Gruppen nach Alter eingeteilt sind.

Nur in der Turn-Variante. In der allgemeinen wäre es sinnlos und würde ohne Not
personenbezogene Daten Minderjähriger erheben. Dafür drei Stellen:
- contact_variant(): Schalter ask_birthdate (true nur für turnen)
- contact-form.php: Feld definiert, bedingt gerendert, direkt nach der
  Gruppenauswahl wie auf der alten Seite
- contact-submit.php: Validierung UND Rücksetzen auf leer, wenn die Variante das
  Feld nicht anbietet — sonst könnte ein manipulierter POST es an der allgemeinen
  Variante vorbei einschmuggeln

Validierung serverseitig, nicht nur per type=date: echtes Datum, nicht in der
Zukunft, nicht vor 1900. createFromFormat mit Rückvergleich, weil PHP sonst
Scheindaten wie 2019-02-31 stillschweigend auf den 3. März verschiebt.
Geprüft mit ausreichend altem Token (die Time-Trap verlangt 3 s, meine ersten
Versuche liefen deshalb als Bot durch): 2019-02-31 abgewiesen, 2030-01-01
abgewiesen, gültiges Datum passiert. Ohne JS bleibt der Wert bei HTTP 422
erhalten (value="2019-03-12" im Re-Render nachgewiesen).

Kein neues Markup: form-field kann type=date und min/max längst, die Prop für
den Hilfetext heißt hint (nicht help, das war mein erster Fehlgriff). Docblock
der Komponente um date und min/max ergänzt.

Datenschutzerklärung mitgezogen, wie die Projektregel es für neue Formularfelder
verlangt — mit Zweckbindung (Zuordnung zur Altersgruppe) und dem ausdrücklichen
Hinweis, dass es keine Pflichtangabe ist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 21:10:47 +02:00
fs
75c06dbd04 Beitragsblock: Sektions-Kopf nach Hausmuster, Preise größer
Drei Dinge, zwei aus deinem Feedback und eines von mir.

1) Der Einleitungstext klebte ohne Abstand an der Überschrift und lief über die
   volle Breite, über 120 Zeichen je Zeile ("schaut weird aus"). Ursache: ich
   hatte <p class="lead"> geschrieben — eine Klasse, die es im Projekt gar nicht
   gibt, der Absatz war also völlig unformatiert. Jetzt das etablierte Muster
   (.membership-signup__head, instagram, partners): zentrierter Kopf, 60ch breit,
   Luft nach unten. Anmerkung im Code: dasselbe Kopf-Muster steht damit zum
   vierten Mal fast gleich in components.css — ein gemeinsames .section__head
   wäre die Aufräumung, aber ein eigener Umbau.

2) Preise von 28px auf 34px. Sie tragen die Hervorhebung allein, seit die Farbe
   weg ist, und sind die Information, auf die man in einer Beitragstabelle sieht.

3) Selbst aufgefallen: die Überschrift "Austritt" lag in .prose und rendert dort
   mit 21px Abel — exakt Fließtextgröße, also gar keine Auszeichnung. In
   Impressum und Artikeln ist das richtig, deshalb .prose nicht angetastet,
   sondern eigene Klassen für den Hinweis: 28px Coolvetica (die Untergrenze für
   Versalien) mit derselben dezenten Sperrung wie die Kartentitel.

Außerdem: data/turnen.json ist wieder in seiner ursprünglichen Formatierung.
Mein JSON-Schreiber hatte im vorigen Commit die kompakten Arrays aufgeklappt
("widths": [640, 1000, 1600] wurde vierzeilig) — inhaltlich identisch, aber 371
Diff-Zeilen für eine Umbenennung. Jetzt textuell ersetzt statt neu geschrieben:
17 Diff-Zeilen, Inhalt per Vergleich identisch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 20:55:48 +02:00
fs
6bc6b0d599 Step Aerobic entfernt, Seite heißt jetzt Fitness & Pilates
Rückmeldung der Turnabteilung: Step Aerobic findet nicht mehr statt.

Wichtig war der Befund vor der Änderung: die Seite bündelt DREI Angebote —
Pilates, Fitness & Stretching und Step-Aerobic. Ein pauschales "die Card raus"
hätte Pilates und Fitness mitgenommen. Entfernt ist deshalb nur die eine Gruppe
(Donnerstag 19:30, Heike Wahler); Pilates und Fitness & Stretching bei Karola
Lorz laufen unverändert weiter. Heike Wahler stand nur in dieser Gruppe und
erscheint damit nicht mehr auf der Website — das ist die einzige Nebenwirkung,
die jemandem auffallen könnte.

Umbenannt in "Fitness & Pilates" an allen zehn Fundstellen: navigation.json,
turnen.json (Disziplin-Schlüssel, Hero-Titel, drei Übersichtstexte),
ansprechpartner.json, news.json, turnen.php (Karte, Alt-Text, Meta) und die
Seiten-Datei selbst.

URL mitgezogen wie besprochen: turnen/step-aerobic-fitness-pilates →
turnen/fitness-pilates, samt Seiten-Datei und den drei Bilddateien (git mv,
Historie bleibt). Der alte Slug war in der Sitemap und ist indexiert, deshalb
eine 301 in public/.htaccess — VOR dem Front Controller, sonst würde index.php
den Pfad als unbekannten Slug abfangen und 404 rendern.

Geprüft: /turnen/fitness-pilates liefert 200 mit nur noch zwei Gruppen,
Sitemap führt ausschließlich den neuen Slug, Navigation, Übersichtskarte und der
News-Artikel verweisen darauf, keine Referenz auf die alten Bildnamen mehr.
Der alte Slug gibt lokal 404, weil php -S keine .htaccess liest; die 301 greift
auf Apache.

Nicht geändert: das Wort "Step-Aerobic" in der CLAUDE.md-Sprachregel. Dort ist
es ein Beispiel für orthografisch nötige Bindestriche und bleibt als solches
richtig, auch ohne den Kurs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 20:50:47 +02:00
fs
9244149bac Sperrung der Kartentitel: neuer Token --ls-heading statt --ls-caps
--ls-caps (0.08em) war zu viel: bei 34px sind das 2,7px und die Überschriften
wirkten auseinandergezogen. Gewollt war ein Hauch Luft, etwa 0,4px (Felix).

Dafür gibt es keinen Token, und rohe Werte verbietet die Token-Disziplin — also
--ls-heading: 0.012em, was bei 34px genau 0,408px ergibt (nachgemessen). Der Fall
trifft jede Coolvetica-Überschrift in h3-Größe, nicht nur diese Karten: der
Schnitt ist condensed, bei 60px trägt er ohne Sperrung, bei 34px kleben die
Versalien. --ls-caps bleibt für kleine Labels, wofür es gedacht ist.

Klarstellung zum vorigen Commit: die Titel waren und bleiben Coolvetica. Der
Verweis auf Abel in meiner Begründung war ein Zitat der CLAUDE.md-Regel für
kleine Auszeichnungen, keine Änderung an der Schriftart.

CLAUDE.md entsprechend berichtigt, inklusive der Warnung, hier nicht --ls-caps
zu nehmen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 20:46:25 +02:00
fs
97676e91ee Beitragskarten: Titel gesperrt, Preise weiß statt rot
Zwei Punkte aus dem Gegenlesen, beide gemessen statt geschätzt.

1) Die Kartentitel waren schwer lesbar. Gemessen war letter-spacing "normal",
   also gar keins gesetzt — nicht zu eng eingestellt. Coolvetica ist ein
   Condensed-Schnitt: bei 60px (Abschnitts-h2) trägt das, bei 34px in einer
   schmalen Kartenspalte kleben die Versalien. Jetzt var(--ls-caps), der
   dokumentierte Token für gesperrte Uppercase-Auszeichnungen.

2) Die Preise standen in Akzentrot. Das ist kein Geschmacksfehler, sondern ein
   Kontrastfehler: #e20612 auf dem Kartengrund (rgba(0 0 0 / 0.22) über #222)
   erreicht 3,50:1 und verfehlt die 4,5:1 für Fließtext. Weiß liegt bei 17,22:1.
   CLAUDE.md verbietet Rot für Fließtext auf dunklem Grund ohnehin. Hervorgehoben
   bleibt der Preis durch Coolvetica und 28px gegen 21px Fließtext.

Die Preisfarbe wirkt auch auf /mitglied-werden, wo dieselbe Komponente steht —
dort ist der Grund dasselbe dunkle Glas (geprüft), die Änderung verbessert also
beide Seiten.

Beide Erkenntnisse als Regel in CLAUDE.md, mit den gemessenen Zahlen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 20:44:17 +02:00
fs
a975bed33c Turn-Beiträge sichtbar machen (standen nirgends auf der Website)
Die Turnabteilung verlinkt die Beiträge in Einladungen für neue Familien und
nimmt zum September auf — die Angaben waren aber auf der ganzen Website nicht
zu finden. Nachgeprüft: 0 Treffer auf /turnen und allen fünf Disziplin-Seiten,
/mitglied-werden führt nur die Fußball-Beiträge.

Es war keine verlorene Seite, sondern eine Lücke im Code: app/pages/turnen.php
rendert nur Hero, Karten und Formular. In data/turnen.json lagen ungenutzt
cards_intro, fees (drei Tabellen), austritt und faq (fünf Fragen). Der Docblock
behauptete, die Beiträge lägen auf den Unterseiten — sie lagen dort nicht, und
in der Git-Historie wurden sie nie gerendert.

Inhalte 1:1 wie auf der alten Seite (gegengeprüft an 2024.tsv08kulmbach.de,
Tab "Allgemeines"): Zusatzbeitrag 24/48 €, Neumitglieder 84/94/184 €,
Förderstufen 120/180/240 €, Austritt zum 30. September. Nichts umformuliert,
die Abteilung entscheidet über Änderungen.

Struktur jetzt Hero → Karten → Beiträge (#beitraege) → FAQ → Formular. Der
Anker ist die Adresse für die Einladungen. Nur bestehende Komponenten benutzt
(fee-table wie auf /mitglied-werden, faq, team-cards mit intro); neu ist allein
der Wrapper .fee-tables. Die FAQ füllt zusätzlich das FAQPage-Schema, weil
page_schema() sie als zweiten Parameter bekommt.

Feinschliff nach Sichtprüfung: Kartentitel von --fs-650 auf --fs-675, weil 28px
Coolvetica neben 21px Fließtext zu klein wirkten; Karten gleich hoch (der
Grid-Default stretch statt align-items: start, gemessen vorher 252/377/471px)
mit dem Hinweis am Kartenfuß; und minmax(min(22rem, 100%), 1fr) statt
minmax(22rem, 1fr), weil die Karten am Handy 352px breit in einem 347px-Raster
standen und überliefen.

NOCH offen: overview.cta ist weiter unbenutzt. Der Block verlinkt "Zur
Turn-Übersicht" und gehört auf die Disziplin-Unterseiten, nicht auf die
Übersicht — dort wäre es ein Selbstlink.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 20:40:23 +02:00
fs
e0b2184372 Swipe-Viewer: alle Seiten wieder einheitlich A5 (kaputter Kommentar)
Am Handy waren die Seitenhöhen beim Durchwischen unterschiedlich: 350 bis 559px
je Inhalt, statt einheitlich 515. Nur Cover, Vorwort, Termine und Sportheim
trafen A5 — die vier haben eine eigene aspect-ratio.

Ursache war ein Fehler von mir aus dem vorigen Commit: der ergänzte Kommentar
landete HINTER dem schließenden */ des bestehenden. Damit stand roher Text vor
der Regel, und der CSS-Parser verwarf den kompletten Block
".stadionzeitung-viewer.is-enhanced .stadionzeitung-page, … { aspect-ratio }".
Der Browser meldete aspect-ratio: auto, obwohl die Regel im ausgelieferten CSS
stand und der Selektor per matches() passte — genau das Bild, das ein
verworfener Block erzeugt. Ein Testlauf mit aspect-ratio: 1/3 änderte nichts,
was den Verdacht bestätigte. php -l prüft CSS nicht, es gab also keinen
Hinweis.

Dass es vor der align-items-Änderung einheitlich aussah, war Zufall: der
Flex-Default stretch streckte alle Seiten auf dieselbe Zeilenhöhe (559) und
verdeckte damit, dass die A5-Regel nie griff.

Zusätzlich sind die Bild-Seiten (Anzeigen) jetzt in derselben A5-Regel. Sie
leiteten ihre Höhe aus dem eigenen Bildverhältnis ab; drei Motive sind
1440x2037 statt 1440x2043 und waren dadurch 513 statt 515px hoch, was beim
Wischen als Sprung auffiel. object-fit: cover fängt die 0,4% Abweichung ab,
wie die Druckfassung es ohnehin tut.

Gemessen: 40 von 40 Seiten 363x515 bei 390px Viewport. Zwei Seiten brauchen
dort internen Scroll (Ergebnisse +44px, Vorschau +11px), weil lange
Vereinsnamen umbrechen — Textumbruch skaliert nicht mit den cqi-Maßen. Im
Druck ist beides unauffällig, per PDF-Sichtprüfung an der Ergebnisse-Seite
bestätigt: Überschrift, drei Ergebnisblöcke und CTA-Kasten vollständig.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 19:38:23 +02:00
fs
c7b23b9f39 Archiv-Zeile: Titel größer und selbsterklärend
Titel lautet jetzt "Stadionzeitung zum Heimspiel gegen <Gegner>" statt nur
"Gegen <Gegner>". Damit ist die Zeile für sich verständlich, auch als Link-Text
für Screenreader. Das "zum" ist eingefügt, damit der Satz grammatisch trägt.

Größe von --fs-650 auf --fs-690, den Token für Karten-Überschriften (28px auf
32 bis 44px). Die alte Begründung für die kleinere Stufe war, dass lange
Gegner-Bezeichnungen in die 280px-Karte passen mussten — die Karte ist jetzt
eine Zeile über die volle Breite, der Grund ist weg. Nach unten bleibt
--fs-650 die harte Grenze für Coolvetica in Versalien.

Geprüft bei 1280px (Titel auf einer Zeile, füllt die Zeile) und bei echten
390px (bricht auf zwei Zeilen, liest sich sauber).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 18:32:32 +02:00
fs
f6efdfb4b4 Stadionzeitungs-Archiv: Liste über die volle Breite statt Regal
Cover links, Angaben rechts daneben — dasselbe Muster wie .veranstaltung-card,
und aus demselben Grund: der Bestand ist klein.

Vorher war es ein horizontales Regal (grid-auto-flow: column, 280px-Spalten,
Scroll-Snap). Der Kommentar dort nannte genau die richtige Gefahr, "bei 1-3
Karten wie eine leere Fläche mit einer verlorenen Karte", löste sie aber nicht:
mit einer veröffentlichten Ausgabe stand eine 280px-Karte allein links auf einer
1280px breiten Seite. Eine Zeile über die volle Breite füllt die Fläche bei jeder
Anzahl.

Zwei Dinge sind dabei anders als bei den Veranstaltungen, beide begründet:
- Die Medienspalte ist schmaler (clamp bis 230px statt 26%), weil das Medium
  hier die live gerenderte Cover-Seite mit festem A5-Verhältnis ist. Ein <img>
  liesse sich per object-fit beschneiden, das Cover nicht — es bestimmt die
  Zeilenhöhe. 230px Cover ergeben 326px Zeile.
- Das margin-top: auto am CTA ist weg. Es stammte aus der hochformatigen Karte,
  wo der Link an den Kartenfuss gehörte; in der breiten Zeile riss es die drei
  kurzen Zeilen auf 150px auseinander.

Neu in der Zeile: die Coverzeile des Interviews als Teaser (interview.teaser,
dieselbe Quelle wie "Im Heft" auf dem Cover). Sie füllt die Breite mit echtem
Inhalt statt Leerfläche und sagt, warum man die Ausgabe öffnen sollte; ohne
Interview entfällt sie, wie .veranstaltung-card__text.

Geprüft per Screenshot bei 1280px und bei echten 390px (iframe-Wrapper, weil
headless Chrome sonst eine Mindestbreite erzwingt): mobil steht das Cover über
den Angaben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 18:30:16 +02:00
fs
e24b63147d CLAUDE.md: Falz-Steg und die drei Seiten mit eigener aspect-ratio
Nachtrag zum vorigen Commit — die Doku-Ergänzung war durchgefallen, weil meine
Textsuche an einem Zeilenumbruch scheiterte. Hält beide Fallen fest: Steg nur
für alle Bögen oder für keinen, und die min-height-Absicherung gehört für
Vorwort, Sportheim und Termine in einen gemeinsamen Selektor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 18:20:48 +02:00
fs
24a3d6234c Druckbogen: Falz-Steg raus, drei Seiten füllen ihre Fläche
Zwei Befunde aus dem Gegenlesen des Hefts, beide gemessen.

1) Die Seiten waren unterschiedlich breit: Umschlagbogen 448,5px,
   Innenblätter 441px. Ursache war ein Falz-Steg von 2%, den nur die
   Innenbögen hatten (:nth-child(n+3)). Der Gedanke war richtig — randabfallende
   Anzeigen stoßen sonst mit 0mm an die Blattmitte — die Umsetzung aber
   uneinheitlich, und das fiel als "das Programm ist schmaler als das Vorwort
   drüber" auf. Entfernt zugunsten von "alle Bögen gleich"; die 9,9mm
   Innenabstand der Seiten halten den Inhalt aus dem Knick. Soll er zurück,
   dann für alle Bögen.

2) Unter dem Foto der Sportheim-Seite blieb ein weißer Streifen: die Seite war
   625,7px hoch in einer 636,4px hohen Fläche. Sie hat wie Vorwort und Termine
   eine eigene aspect-ratio plus negativen Margin, aber die min-height-
   Absicherung hatte nur das Vorwort bekommen. Die drei stehen jetzt in EINER
   Regel, damit eine vierte nicht wieder abdriftet.

Nachgemessen: Sportheim 448,5 x 636,4 statt 441 x 625,7, Foto bündig 0/0/0,
alle Slots ohne Padding, im gedruckten PDF Bogen 11 unten 0,0mm. Sichtprüfung
an Bogen 11: Foto läuft bis an linke und untere Kante.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 18:20:14 +02:00
fs
f678608140 Druckfassung: alle Bögen randabfallend, unterer Weißstreifen weg
Im Druck war oben fast kein Rand und unten viel — im Chrome-Export etwa 3 zu
8 mm. Der Rand war nicht die Ursache: der Blatt-Inhalt war über eine
aspect-ratio bemaßt und nur 199,1 mm hoch statt 210 mm. Die fehlenden 10,9 mm
sammelten sich immer unten, weil der Inhalt oben ausgerichtet ist. Gemessen an
Blatt 4 des erzeugten PDF:

  margin 5mm  ->  oben 4,8 / unten 5,8 mm   (Rest 0,6 fiel kaum auf)
  margin 0    ->  oben 0,0 / unten 10,9 mm  (der ganze Rest unten)

Ein Rand hätte den Fehler also nur kaschiert. Randabfallend ist die
geometrisch saubere Lösung, weil zwei A5 exakt ein A4 quer sind
(2 x 148,5 = 297 mm, Überschuss null) — es bleibt nichts zu verteilen.

Umgesetzt durch Auflösen einer Ausnahme statt neuer Sonderregeln: die zwei
Umschlagbögen liefen längst randabfallend (@page umschlag, Fläche 210mm), das
gilt jetzt für alle Bögen. Damit fällt auch der Bruch weg, dass der Umschlag
anders gedruckt wurde als die Innenblätter.

Nachgemessen: oben und unten je 0,0 mm auf Blatt 1, 4 und 6. Verbleibendes Weiß
steckt in den Anzeigenmotiven selbst (viele Sponsoren gestalten auf weißem
Grund, 100% weiße Randpixel) und ist nicht unser Layout. Sichtprüfung an
Blatt 4: Sportfest-Plakat läuft bis an alle vier Kanten, nichts beschnitten.

Bewusst in Kauf genommen (Felix): der Bürodrucker beschneidet auf sein
Hardware-Minimum von 4 bis 5 mm und liefert den Rand selbst. Unser Fließtext
verliert dabei nichts, die Live-Seiten haben 9,9 mm Innenabstand. Ein
Falz-Abstand entfällt, die Seiten stoßen mittig aneinander.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 18:09:59 +02:00
8be0441341 Hutschdorf-Ausgabe eingefroren 2026-07-31 17:50:30 +02:00
fs
de5132c2ec CLAUDE.md: Bild-Einfrieren beim Veröffentlichen dokumentieren
Nachtrag zu 7223122 — der Doku-Teil war beim Commit durchgefallen, weil meine
Textsuche in CLAUDE.md nicht griff. Enthält auch den Hinweis, dass veröffentlicht
werden muss, wo der frische Sync-Stand liegt: auf dem Server. data/matchcenter.json
ist gitignored, lokal ist er also älter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:48:27 +02:00
fs
7223122220 Veröffentlichen friert auch die Bilder ein
Der Snapshot fror die Daten ein, verwies aber weiter auf Ordner, die die
Sync-Skripte verwalten: matchcenter-sync räumt Wappen weg, die der BFV nicht mehr
führt (eine Saison), instagram-sync ältere Posts (Wochen). Am 31.07.2026
nachgewiesen — ein Lauf löschte 18 Wappen und 3 Bilder. Eine archivierte Ausgabe
wäre also mit der Zeit von selbst zerfallen, und "gedruckt = fixiert" war damit
nur die halbe Wahrheit.

bin/stadionzeitung-publish.php kopiert jetzt alles, worauf der Snapshot zeigt
(img/crests, img/instagram, img/news), als fix-* in den Ausgaben-Ordner und
schreibt die Pfade um. Varianten-Sets {base, widths} werden über alle Breiten
kopiert und nur umgeschrieben, wenn alle da sind — ein halbes srcset wäre
schlimmer als der alte Pfad; genau daran fiel img/news/neue-website beim ersten
Versuch auf. --entwurf löscht die fix-*-Dateien wieder, sie gehören zum Snapshot.
Flache Dateinamen statt Unterordner, weil die Aufräum-Logik in
stadionzeitung-add.php nur Dateien entfernt und ein Unterordner als Waise bliebe.

bin/preflight.php warnt, wenn eine veröffentlichte Ausgabe noch auf die
flüchtigen Ordner zeigt — sonst verfiele sie still.

Geprüft am Hutschdorf-Heft: 39 Dateien eingefroren, Snapshot verweist nur noch
auf den Ausgaben-Ordner, 88 Bilder in Viewer und Druckfassung erreichbar, keins
fehlt, preflight-Warnung verschwindet. Der Testlauf wurde danach zurückgenommen:
der lokale Matchcenter-Stand ist einen Tag alt, eingefroren wird auf dem Server
nach dem letzten Sync vor dem Druck.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:47:44 +02:00
fs
344727bed0 Vorwort-Seite: Rasenbild bis zur Blattkante
Unter dem einblendenden Rasenbild blieb ein weißer Streifen, responsiv wurde er
größer. Gemessen bei 390px Viewport: Seite 559px hoch, Vorwort 515px — 44px
Lücke.

Ursache lag nicht am Vorwort, sondern an der Seite: .stadionzeitung-viewer__page
ist display:flex, und der Flex-Default align-items:stretch streckt die Seite auf
die Höhe der Flex-Zeile und übersteuert ihre aspect-ratio. Die Seite war damit
gar kein A5 mehr (559/363 statt 210/148) — das betraf ALLE Seiten, sichtbar wurde
es nur beim Vorwort, weil dort ein Bild die Blattkante erreichen muss.

align-items: center behebt es an der Wurzel: alle Seiten sind wieder exakt A5,
Lücke 0px. Zusätzlich bekommt das Vorwort min-height: calc(100% + 2x Padding) als
Absicherung — die aspect-ratio trägt weiter, wenn die Seite keine definite Höhe
hat (No-JS-Stapel, Druck), die min-height füllt sie, wenn sie eine hat.

Gemessen vorher/nachher, Desktop-Viewer per Screenshot gegengeprüft.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:43:52 +02:00
fs
a63822f3d4 Druckfassung-Button aus dem Viewer entfernen
Die Druckfassung ist Werkzeug für die Heft-Produktion, kein Besucher-Angebot.
Als sichtbarer Knopf neben Blättern und Vollbild wurde er versehentlich geklickt
und riss Leute aus dem Lesen (Felix, 31.07.2026). Die Route bleibt unverändert
erreichbar, nur über die URL: ?druck=1 an die Ausgabe hängen.

Mit dem Knopf fallen drei CSS-Stellen weg, die es ohne ihn nicht mehr braucht:
die Basisregel, die Mobil-Ausblendung (dort wäre er eine Sackgasse gewesen) und
der Eintrag in der Ausblend-Liste der Druckansicht. In CLAUDE.md vermerkt,
inklusive der Bitte, ihn nicht als Link wieder einzubauen.

Geprüft: Viewer enthält kein druck=1 und kein viewer__print mehr, Vollbild und
Blättern intakt, ?druck=1 antwortet weiter mit 200 und montiert die Blätter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:36:15 +02:00
fs
827a717a02 Stadionzeitungs-Cover: definite Breite im Swipe-Viewer
Das Cover fehlte am iPhone, Chrome am Desktop zeigte es. Es hatte im Viewer eine
eigene Regel mit "max-height: 72vh; width: auto" — und width:auto ist bei
inline-size-Containment genau die Falle, die für .stadionzeitung-page schon
behoben war: der Inhalt zählt für die eigene Breite per Definition nicht mit.
Sichtbar wird der Unterschied, weil .stadionzeitung-viewer__page display:flex ist:
auf einem Flex-Kind heißt width:auto "richte dich nach dem Inhalt". Chrome
rettet sich über aspect-ratio plus gestreckte Höhe, Safari nicht.

Cover und Seite stehen jetzt in EINER Regel mit derselben definiten Breite, damit
sie nicht wieder auseinanderlaufen; nur overflow-y bleibt den Seiten vorbehalten.
Gemessen bei 390px Viewport: Cover 363x559, identisch zu den Folgeseiten.

EHRLICHE GRENZE: Chrome zeigt vor und nach der Änderung dieselben Maße, der
Fehler ist im Desktop-Browser nicht reproduzierbar. Die Ursache ist damit
begründet, aber nicht bewiesen — Gegenprobe nur am echten Gerät. In CLAUDE.md
vermerkt, damit das nächste Mal niemand im Desktop-Browser "verifiziert".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:31:35 +02:00
fs
05c64c8f12 Takt über public/cron.php: KAS-Cronjobs können nur URLs
Korrektur meiner eigenen Annahme von heute früh. Der Tarif hat SSH und
Cronjobs, aber das KAS-Formular kennt nur ein Feld "Protokoll / Pfad" mit
https:// — ein `php bin/…` ist dort nicht eintragbar. Der Endpoint, den ich
vorhin stillgelegt hatte, ist damit wieder der einzige Takt-Geber, und zwar in
einer besseren Form als geplant: Aufrufer ist der Hoster selbst, der Schlüssel
verlässt den Webspace nie, kein Dritter ist beteiligt.

Dritter Job 'logrotate' in der Whitelist, denn ohne ihn liefe die Logpflege nie
automatisch und die 2-MB-Grenze samt 90-Tage-Frist wäre toter Code. Er passt in
denselben Rahmen wie die Syncs: kein Parameter aus der URL, keine Verbindung nach
außen, schreibt nur in storage/logs. bin/log-rotate.php trägt dafür denselben
Riegel wie die Syncs (CRON_HTTP) und require_once. Eine vierte Ausnahme braucht
wieder denselben Aufwand an Begründung.

Geprüft: falscher Schlüssel 403, unbekannter Job 403, gültiger Aufruf 204 mit
leerem Body, sofortige Wiederholung 204 plus WARN, Logdateien unversehrt.

Zeitraster angepasst, weil das KAS Wochentage nicht mit Halbstunden kombiniert:
Matchcenter stündlich statt 2x/Tag plus Wochenende, Instagram als zwei
Tageseinträge, Logpflege täglich nachts. Die Abwägung steht in
config.example.php und docs/deploy.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:16:12 +02:00
fs
964ffb932b config.example: Cron-Doku auf den echten Stand
Zwei Korrekturen an Stellen, die beim Anlegen der Cronjobs in die Irre geführt
hätten: (1) der Abschnitt behauptete noch, der Tarif habe keine Cronjobs —
seit 31.07.2026 ist Weg A unser Weg, cron.key ist leer und public/cron.php
stillgelegt. (2) Die Cron-Zeilen leiteten die Fehlerausgabe in die Kanal-Logs,
deren Format `[Zeit] LEVEL Meldung` eine rohe PHP-Warnung bricht (bin/logs.php
wertet Kanäle nach Level aus). Sie geht jetzt nach php-errors.log, das
bin/logs.php bereits als "eigenes Format" behandelt. Dazu die echten Pfade
dieses Webspace statt Platzhalter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:09:35 +02:00
fs
913c058c93 send_mail: Erfolg als INFO ins mail-Log
Bisher schrieb nur der Fehlerfall. Damit war ein funktionierender Versand von
einem stummen nicht zu unterscheiden: beim Deploy am 31.07.2026 zeigte
bin/logs.php gar keinen mail-Kanal, was "nichts abgeschickt" und "einwandfrei
versandt" gleichzeitig bedeuten konnte.

Geloggt wird nur der Kontext (welches Formular) — die Werte sind feste Labels
wie "Kontaktanfrage" oder "Sportheimbuchung", nie Empfänger, Absender oder
Inhalt. Das verbietet die Logging-Regel, und für "läuft der Versand?" braucht es
davon auch nichts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:07:51 +02:00
fs
f1ba364922 config.example: transiente SMTP-Auth-Fehler und Rückfallweg dokumentieren
Auf dem Server scheiterte die Anmeldung mit "Could not authenticate", lief
Minuten später ohne jede Änderung fehlerfrei und wurde parallel vom
Entwicklungsrechner durchgehend angenommen. Es war also vorübergehend, nicht
falsch konfiguriert — vermutlich der Brute-Force-Schutz des Mailservers.

Notiert, damit das nicht als Konfigurationsfehler behandelt wird, plus der
nachgewiesene Rückfallweg (localhost:25 ohne Anmeldung, der lokale Relay).
Bewusst nicht der Standard: mit localhost liefen Dev und Produktion auseinander.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:05:02 +02:00
fs
748f029afb smtp-test: verdeckte Passwort-Eingabe und Zugangsweg-Sonde
Auf dem Server scheiterte der Login mit "Could not authenticate", während
dieselben Zugangsdaten vom Entwicklungsrechner angenommen werden. Beide Namen
(w01ca75d.kasserver.com und der Webserver) zeigen auf dieselbe IP — vom Webspace
aus verbindet sich PHP also mit der eigenen Maschine, und die kann Anmeldungen
anders behandeln als Verbindungen von außen.

--probe testet vier Zugangswege (587/STARTTLS, 465/SSL, localhost:25 mit und
ohne Anmeldung) und nennt den ersten, der trägt. Bewusst nur vier: jeder
Fehlversuch ist ein fehlgeschlagener Login, zu viele sperren ein Postfach.

--frage fragt das Passwort verdeckt ab, damit ein neu gesetztes Postfach-Passwort
geprüft werden kann, BEVOR es in eine Datei auf zwei Rechnern wandert.
Die Fehlerausgabe nennt bei "Could not authenticate" jetzt die drei Ursachen,
die dahinter stecken können.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 17:03:00 +02:00
fs
2066b43a57 Cover-Spielerfoto: Varianten statt 12-MB-Original (iOS Safari)
Das Cover blieb am iPhone leer, während Chrome am Desktop es zeigte. Ursache
war kein fehlendes Bild: das freigestellte Spielerfoto hatte 2584x5093 Pixel und
12,3 MB. iOS Safari verwirft Bilder, deren dekodierte Größe sein Budget
übersteigt — hier gut 53 MB, und auf einer Ausgabe liegen 86 Bilder.

Dargestellt wird das Foto mit 98cqi Höhe, bei 480px Seitenbreite also etwa
238x470 CSS-Pixel. Es gibt jetzt 320/640/1000 als PNG mit erhaltener
Transparenz (0,2 / 0,8 / 1,9 MB); sizes nennt die echte Breite, sonst zöge ein
Handy mit DPR 3 die größte Variante. Das Original ist gelöscht.

Dafür kann resize_image_variants() nun Alpha erhalten ($alpha = true schreibt
PNG statt JPG) und die img-Komponente eine Endung ($image['ext']). Der Default
bleibt JPG, weil bestehende Aufrufer PNG-Quellen absichtlich zu JPG verkleinern.

bin/stadionzeitung-add.php erzeugt die Varianten künftig selbst. Dort stand
"unverändert kopieren, Felix liefert bereits ein web-taugliches Foto" — genau
diese Annahme ist gerissen.

Außerdem: smtp.password stand in der Produktiv-Config noch auf dem Platzhalter
POSTFACH_PASSWORT (von preflight auf dem Server gefunden, einziger harter
Fehler). Übernommen wurde das Passwort aus der Dev-Config, mit dem der Versand
am 27.07.2026 verifiziert wurde; Host, Postfach und Empfänger sind identisch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:58:43 +02:00
fs
a3cfd4941d Banner-Hintergründe CSP-fest: <img> statt Inline-Variable
Beim ersten Deploy blieben die Slides des Banner-Sliders schwarz. Ursache war
kein fehlendes Bild, sondern die CSP: public/.htaccess setzt
default-src 'self' ohne style-src, der Browser verwirft damit jedes
style-Attribut — und die Komponente lieferte Bild und Akzentfarbe genau so
(style="--banner-bg: url(…); --banner-accent: #6cbbbc"). Lokal war das
unsichtbar, weil php -S keine .htaccess liest.

Der Hintergrund ist jetzt ein <img class="ad-banner__bg"> über die bestehende
img-Komponente (CSP erlaubt img-src) und bringt srcset mit: das Handy lädt die
640er statt der 1600er, die erste Slide lädt eager mit fetchpriority. Der
dunkle Leseverlauf liegt auf ::before statt im background-image.

Die Akzentfarbe kann als beliebiger Hex-Wert nicht ohne Inline-Style in die
Seite. Sie steht deshalb als Token in tokens.css (wo Farben laut CLAUDE.md
hingehören), components.css bildet den Namen darauf ab, und banners.json nennt
nur noch "accent": "breadcrumb".

bin/preflight.php bricht ab, wenn unter app/ wieder ein style-Attribut
auftaucht — der Fehler wäre sonst erneut erst in Produktion sichtbar. Beim
Bauen dieser Wache zweimal selbst hineingetappt: ein Muster mit [^>]* bricht an
jedem schließenden PHP-Tag ab, und die Zeichenfolge dieses Tags beendet den
PHP-Modus auch in einem //-Kommentar (preflight gab ab dort seinen eigenen
Quelltext aus, php -l meldete nichts).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:50:43 +02:00
fs
e8296a465b Deploy-Anleitung: Verzeichnisschutz ehrlich als Empfehlung einordnen
Stand vorher "nicht weglassen". Tatsächlich mindern die Canonical-Tags auf
www.tsv08kulmbach.de das Indexierungsrisiko so weit, dass der Schutz eine
Empfehlung ist und kein Blocker. Am 31.07.2026 bewusst weggelassen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:39:39 +02:00
1213fa4ca1 Sync-Stand vom Server: Wappen und Instagram-Bilder aktualisiert 2026-07-31 16:36:23 +02:00
fs
8ed5707d24 Deploy-Anleitung: Git-Identität auf dem Server als eigener Schritt
Fehlte, und ohne sie bricht jeder Commit auf dem Server mit "Author identity
unknown" ab — womit der Update-Ablauf aus F1 nicht funktioniert. Dazu der
Umgang mit einem abgewiesenen Push (pull --rebase).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:36:03 +02:00
fs
3cf8c2688b Deploy-Anleitung: Platzhalter ohne spitze Klammern
<host> liest die zsh als Eingabe-Umleitung, der scp brach mit "no such file or
directory: host" ab. Platzhalter heißen jetzt SERVER.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:28:58 +02:00
fs
dd18906348 Deploy-Anleitung: absolute Pfade, ~ ist auf diesem Webspace /
Das SSH-Konto hat die Systemwurzel als Home. cd ~ führt aus dem Webspace heraus,
~/tsv08kulmbach-website existiert nicht. Betraf das scp-Ziel und jede cd-Zeile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:21:09 +02:00
fs
4f1bfce8b2 Deploy-Anleitung: echte Server-Pfade statt Platzhalter
B1 auf dem Webspace durchgeführt: PHP-CLI 8.3.29 unter /usr/bin/php, Composer
2.9.8 global, Git 2.43. Der Ordnername tsv08kulmbach.de ist von der alten
CMS-Seite belegt (die der Rückweg beim Umschalten ist), der neue heißt deshalb
tsv08kulmbach-website.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:19:22 +02:00
fs
f0ec8614b2 Deploy-Anleitung + Cron-Endpoint stilllegen
Der Tarif hat jetzt SSH und Shell-Cronjobs. Damit fällt die Begründung für
public/cron.php weg: der Endpoint war ausschließlich der Ersatz dafür.
cron.key steht in der Produktiv-Config auf '' (403 auf jeden Aufruf), der Code
bleibt als Rückweg samt Begründung stehen.

docs/deploy.md beschreibt den Weg in kleinen Schritten. Kern: die neue Seite
läuft erst auf einer per Verzeichnisschutz geschlossenen Test-Subdomain mit
DERSELBEN Produktiv-Config, die später live geht — kein Config-Wechsel beim
Umschalten. Möglich, weil die Host-Weiterleitung in public/.htaccess nur beim
exakten Host ohne www greift. Die alte CMS-Seite bleibt bis zuletzt online und
ist der Rückweg.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:13:14 +02:00
fs
a495250db8 CLAUDE.md: uploads-Regel dokumentieren
Der Satz „Weil uploads/ gitignored ist" stimmte nicht mehr. Ersetzt durch die
Regel, die den Ordner im Repo tragfähig macht (Server pusht zuerst) und den
dauerhaften Preis (Historie schrumpft nie).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:07:13 +02:00
fs
aee8a3a901 Dateiablage /dateien versionieren (Vereinsmaterial sichern)
483 Dateien, 174 MB: Fotos und Videos vom Sportfest 2025, die Fundstücke der
Chronik 2028 samt ihren .quelle.txt-Belegen, das Logo. Damit ist das Material
gesichert und beim Entwickeln lokal verfügbar.

Der Ordner ist ein Schreibziel für andere, deshalb hat die Reihenfolge Bedeutung:
auf dem Server committen und pushen, dann lokal pullen, dann entwickeln. Und da
Git nichts vergisst, schrumpft das Repo nie wieder — große Videos sparsam.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:05:19 +02:00
fs
e6f72a0e4b Wappen und Instagram-Bilder versionieren
Eine veröffentlichte Stadionzeitung friert Verweise auf genau diese Dateien
dauerhaft ein: 37 der 86 Bilder im Hutschdorf-Heft lagen in gitignorierten
Ordnern (33 Wappen, 4 Instagram-Fotos). Auf einem frischen Klon wären sie
kaputt, und der Instagram-Sync räumt alte Posts irgendwann weg, womit eine
archivierte Ausgabe nachträglich zerfiele.

Die JSON-Caches bleiben draußen, die baut der Sync neu. Maschinenverwaltet und
flüchtig ist nicht dasselbe wie maschinenverwaltet und für immer referenziert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:02:56 +02:00
fs
2f8619e78d Gedruckte Adressen aus club.website statt base_url
QR-Codes und Adresszeilen trugen `http://localhost:8000` ins Druckmaterial, weil
sie über abs_url() liefen. Nach dem Produktivgang hätten alle zehn Codes neu
erzeugt werden müssen — und ein gedruckter Code mit localhost darin ist Altpapier.

Die öffentliche Domain ist Vereinsidentität, keine Umgebungseinstellung: neues
Feld `club.website` in data/club.json plus club_url()/club_domain(). base_url
bleibt umgebungsabhängig, weil Canonical, OG, Sitemap und JSON-LD zur
ausliefernden Adresse passen müssen.

Umgestellt: Cover-QR und die acht geteilten CTA-QRs, die Internet-Zeile im
Heft-Impressum, die Cover-Zeile neben dem QR, die acht Linkzeilen unter den
CTA-QRs der Live-Seiten (die hatte ich beim ersten Durchgang übersehen), das
Web-Feld im Website-Impressum und bin/qr.php.

Nebenbei: club_domain() ohne Pfad hängte einen Wurzel-Slash an
(„tsv08kulmbach.de/"), und der Kopf-Kommentar von stadionzeitung-add.php
beschrieb noch die alten vier Anzeigen-Gruppen.

Nicht verifizierbar und deshalb dokumentiert: die vendorte QR-Bibliothek
arbeitet nicht reproduzierbar (gleiche URL, andere Pixel — anderes Maskenmuster,
beides gültig). Ein PNG-Vergleich beweist nichts, nachweisbar ist nur der
Code-Pfad. Jeden Druck-QR einmal scannen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 15:40:59 +02:00
fs
93b67bc9d3 CLAUDE.md: Anzeigen-Bestand, Verteilungsregel, Veranstaltungen, Lightbox
Neue Single-Source-Zeilen für data/anzeigen.json und data/veranstaltungen.json,
Vorlage der Stadionzeitung als Blöcke mit Lücken statt vier Anzeigen-Gruppen,
Doppelseiten-Regel jetzt maschinell. Dazu die Fallen, in die wir gelaufen sind:
Kontext-Regeln setzen Custom Properties und kein Layout (… .crest), --header-space
statt --header-h, [hidden] gegen .lightbox, QR-Codes tragen die base_url fest
eingebacken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
fs
a101b0c3ec Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.

Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.

News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.

Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
  Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
  (Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
  Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
  unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
  umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.

Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
fs
3fb480dd78 Inhalte und Bilder: Stadionzeitung, Veranstaltungen, News, Anzeigen-Bestand
Neue Datenquellen:
- data/veranstaltungen.json  Feste mit eigener Detailseite (dritte Termin-Quelle)
- data/anzeigen.json         Sponsoren-Anzeigen als dauerhafter Bestand statt
                             pro Ausgabe abgeliefert
- data/stadionzeitung.json   Ausgaben-Archiv (maschinenverwaltet)
- data/news.json             redaktionelle Meldungen mit Artikelseiten

Bilder einmal statt pro Ausgabe: public/assets/img/anzeigen/ hält die 19
Sponsorenmotive plus drei Eigenanzeigen; vorher lag jede Anzeige als Kopie
unter jeder Stadionzeitungs-Ausgabe.

Zuerst die Inhalte, dann der Code: so ist der Baum in jedem Schritt gültig.
Daten ohne die zugehörigen Seiten sind harmlos, Seiten ohne Daten nicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:54:37 +02:00
fs
82cf21085d 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
2026-07-27 10:33:53 +02:00
fs
eacf9449e0 Design-System: Button-Varianten konsolidieren, 44px-Trefferflächen
Dritter Teil. .btn hatte nur primary + outline + sm/lg, daneben lebten mehrere
Controls als Eigenbau — teils zeichengleich dupliziert.

Tabs: .group-tabs__tab und .filter-bar__tab waren bis auf flex-shrink und 4px
Innenabstand identisch. Beide laufen jetzt über eine gemeinsame .tab-Basis, die
aria-pressed (Filter) und aria-selected (Tabs) gleich behandelt; von den
BEM-Klassen bleibt nur das jeweilige Delta. Innenabstand vereinheitlicht auf
--space-m, die Gruppen-Tabs verlieren dadurch 4px pro Seite.

Icon-Buttons: .ad-banner__nav-btn und .ad-banner__toggle waren vollständige
Duplikate voneinander. Sie tragen jetzt .btn .btn--icon und behalten nur die
Glas-Optik als Delta. Der Selektor schreibt .btn mit — utilities.css lädt nach
components.css, bei gleicher Spezifität gewänne sonst die rote .btn-Fläche.

Trefferflächen auf 44px (WCAG 2.5.5): .btn bekommt min-height und wird
inline-flex, damit Icon und Label ohne Zusatz-Markup zentriert liegen.
Bezeichnend auch hier — .department-choice__btn hatte inline-flex, align-items
und exakt denselben gap bereits selbst deklariert und gibt sie jetzt ab.
Controls, die optisch klein bleiben sollen, strecken die Fläche stattdessen
unsichtbar per Pseudo-Element (.tap-target, automatisch auch für .btn--sm) —
gewertet wird die Trefferfläche, nicht die gezeichnete.

Der Banner-Indikator brauchte einen anderen Weg: er trug overflow: hidden, um
den Fortschrittsbalken zu beschneiden, was jede Erweiterung wieder abgeschnitten
hätte. Der sichtbare 6px-Balken liegt jetzt im ::before, der Fortschritt ist
selbst rund — overflow entfällt, der Button ist 48x44px.

Haupt-Nav-Links, Dropdown-Trigger und Footer-Social-Links bekommen min-height
statt einer Pseudo-Erweiterung: sie stehen in Reihen, wo überlappende
Trefferflächen den Nachbarn verdecken würden. In der 60px hohen Kopfleiste ist
die Höhe ohnehin nicht sichtbar.

Bewusst unverändert: die Formular-Checkbox bleibt bei 24px (WCAG 2.5.8).
<input> ist ein Replaced Element, dort greift ::after nicht zuverlässig — und
das zugehörige <label> ist klickbar und liefert die große Fläche.

.btn--ghost ist angelegt, hat aber aktuell keinen Abnehmer: die vermuteten
nackten Links sind bei näherem Hinsehen Bildkarten (.instagram__link),
Wrapper (.split__cta) oder eigenständige Karten-Affordanzen (.team-card__cta),
keine tertiären Buttons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSsVdooFLgPA8gPFp7HwZU
2026-07-27 10:28:02 +02:00