Stadionzeitung: Ergebnis- und Gegnerseite neu gestaltet

Ergebnisse laufen als Anzeigetafel (Wappen und Name je Seite, Ergebnis gross
in der Mitte) statt als Listenzeilen. Verworfen und im CSS begruendet: Tore
am rechten Blattrand, getoente Karten je Spiel, roter Strich unter der
Mannschaftszeile, graue Verlierer-Ziffern. Vereinsnamen einzeilig, sonst
ausgepunktet.

Die Gegnerseite bekommt ein symmetrisches Duell, eine aus den Tabellenwerten
abgeleitete Aufmacher-Zeile, den Vergleich als Balkenpaar aus der Mitte und
einen Tabellenausschnitt ueber die geteilte league-table-Komponente. Die
Balkenlaengen kommen aus Klassen in 5-Prozent-Stufen, nie aus einem
style-Attribut (die CSP verwirft inline styles stillschweigend).

Das Mitglied-werden-Poster sagt jetzt "Werde Teil unseres Vereins".

Alle geaenderten Seiten auf A5-Hoehe nachgemessen, kein Ueberstand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hv9SGDu3m3diiPrW9Fmj3v
This commit is contained in:
2026-09-19 10:54:05 +02:00
parent 8f864368f0
commit 0ee75aa93b
5 changed files with 423 additions and 114 deletions

View File

@@ -54,7 +54,7 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern.
| Partner/Sponsoren | `data/partners.json` | — |
| Instagram-Beiträge | `data/instagram.json` + `public/assets/img/instagram/` | Aufgenommen ausschließlich mit **`php bin/instagram-add.php <post-url>`** (holt Bild, Text und Datum) — Einträge nie von Hand tippen, Bilder nie von Hand skalieren. **Kein Cron**, chat-gepflegt wie `events.json`. Eintrag und Bild müssen immer zusammen existieren, `bin/preflight.php` prüft beide Richtungen |
| Matchcenter (Spiele/Ergebnisse/Tabellen) | `data/matchcenter.json` + `public/assets/img/crests/` | **maschinenverwaltet von `bin/matchcenter-sync.php` — nie von Hand editieren**. Team-/Wettbewerbs-IDs in `config.php` (`matchcenter.teams`) |
| Stadionzeitung (Ausgaben-Archiv) | `data/stadionzeitung.json` + `public/assets/img/stadionzeitung/` | **maschinenverwaltet von `bin/stadionzeitung-add.php` + `bin/stadionzeitung-publish.php` (Status/Snapshot) — nie von Hand editieren; einzige Chat-Pflege-Ausnahmen sind `vorwort`, `interview`, `aufruf`, `veranstaltung` (Slug-Verweis auf `veranstaltungen.json`) und `cover` (`teaser` einer Sonderausgabe).** Der Ausgaben-Ordner wird bei jedem Lauf komplett geleert und neu gefüllt |
| Stadionzeitung (Ausgaben-Archiv) | `data/stadionzeitung.json` + `public/assets/img/stadionzeitung/` | **maschinenverwaltet von `bin/stadionzeitung-add.php` + `bin/stadionzeitung-publish.php` (Status/Snapshot) — nie von Hand editieren; einzige Chat-Pflege-Ausnahmen sind `vorwort`, `interview`, `aufruf`, `veranstaltung` (Slug-Verweis auf `veranstaltungen.json`), `cover` (`teaser` einer Sonderausgabe), `opponent_short` (gedruckter Anzeigename des Gegners) und `gegner_teams` (weitere Mannschaften mit Gegnerseite).** Der Ausgaben-Ordner wird bei jedem Lauf komplett geleert und neu gefüllt |
| Sponsoren-Anzeigen (Stadionzeitung) | `data/anzeigen.json` + `public/assets/img/anzeigen/` | Dauerhafter Bestand, **nicht** pro Ausgabe: Motive ändern sich selten, das Heft erscheint oft. `sponsor`, `alt` (optional), `aktiv`, `bis` (optional, Enddatum gegen das Ausgabedatum), `platzierung` (optional, fester Platz auf einer Live-Seite statt im Streu-Pool) und die **Reihenfolge** (= Reihenfolge im Heft) im Chat gepflegt; `base`/`widths` schreibt ausschließlich `bin/anzeige-add.php`. Anzeigen nie in den Ausgaben-Ordner legen |
| Termine/Events (`/termine`) | `data/events.json` (Vereins-Termine **ohne** eigene Detailseite) — Spieltermine kommen weiterhin nur aus `data/matchcenter.json`, Feste mit eigener Seite nur aus `data/veranstaltungen.json`; die Termine-Seite mergt alle drei | Von Claude im Chat gepflegt wie `teams.json`/`partners.json` — **keine** Automatisierung, kein Bin-Skript. Spieltermine nie hier duplizieren. Ein Fest, das eine Detailseite hat, steht **nur** in `veranstaltungen.json` (`bin/preflight.php` prüft das) |
| Veranstaltungen (`/veranstaltungen` + Detailseiten) | `data/veranstaltungen.json` + `public/assets/img/veranstaltungen/` | Termin, Texte, Programm, Turnierpläne (`turniere[]`, nur fürs Heft), Abschnitte von Claude im Chat gepflegt — **außer `plakate` und `galerie`: die schreibt ausschließlich `bin/veranstaltung-bilder.php`**. Adresse nie in die Texte schreiben (steht in `club.json`) |
@@ -795,6 +795,49 @@ keinen farbigen Rahmen über das Spielerfoto** — der Kicker bleibt „Saison
die Ausgabe nicht, in die die Felder gehören. `bin/preflight.php` prüft, dass der Slug und jeder
Turnier-Key wirklich existieren — ein Tippfehler ergäbe sonst eine **leere** A5-Seite ohne Fehler.
**Gegnerseiten ohne Fest, und der Anzeigename des Gegners (17.09.2026).** Zwei Felder, beide
chat-gepflegt und von `bin/stadionzeitung-add.php` über erneute Läufe durchgetragen wie `vorwort`:
- **`gegner_teams`** (Liste von Team-Keys) gibt weiteren Mannschaften eine Gegnerseite, ohne dass es
eine Veranstaltung braucht. Der Sportfest-Weg blieb unverändert daneben stehen (Heimspiele im
Festzeitraum); dieser hier nimmt das nächste Spiel im Fenster **zwei Tage vor bis drei Tage nach**
dem Ausgabedatum, **auch auswärts** — am normalen Spielwochenende fährt eine der drei Mannschaften
fast immer weg, und ihr Gegner ist trotzdem eine Seite wert. `stadionzeitung_gegner_fixtures()`
bleibt die eine entscheidende Stelle. Findet eine benannte Mannschaft kein Spiel im Fenster,
entsteht keine Seite und das Skript **warnt** — ein Tippfehler im Team-Key sähe sonst aus wie ein
spielfreies Wochenende. Auf der Gegnerseite steht seitdem **immer unsere Mannschaft links**
(Klasse `--own`), bei einem Auswärtsspiel läuft die Zeile „… bei …" statt „… gegen …"; vorher war
die linke Spalte fest die Heimmannschaft und die Hervorhebung lag beim Gegner.
- **`opponent_short`** ist der gedruckte Anzeigename des Gegners, gelesen über
**`stadionzeitung_opponent_display()`** (Cover, Archivzeile, Ausgaben-Kopf, Titel, Meta, Alt-Text
der Rückseite). **`opponent` selbst muss die BFV-Schreibweise behalten** — die Gegnerseite sucht
damit ihre Tabellenzeile und ihre Begegnung in `matchcenter.json` über einen exakten
Namensvergleich. Bei Spielgemeinschaften ist das eine Zeile wie „(SG1) TSV Wirsberg I/ FC
Kupferberg I", die auf einem Cover nichts zu suchen hat; `opponent_short` trägt dann
„Wirsberg/Kupferberg". Fehlt das Feld, bleibt es beim vollen Namen.
Nebenbei löst das oft die Seitenzahl: drei Gegnerseiten statt einer sind **zwei Seiten mehr**, und
ein Heft, das bei 42 stand, steht damit auf 44.
**Gegner- und Ergebnisseite neu gestaltet (19.09.2026).** Beide Seiten waren inhaltlich richtig und
gestalterisch leer, der Umbau lief über mehrere verworfene Entwürfe. Was davon bleibt:
- **Ergebnisse als Anzeigetafel** (Vorbild kicker/Sofascore): Wappen und Name je Seite, das Ergebnis
groß in der Mitte. Vorher waren es Listenzeilen, und eine Zeile stellt Heim und Gast untereinander
statt gegenüber. Verworfen und nicht wieder einzubauen: Tore am rechten Blattrand (halbe
Seitenbreite Leere zwischen Name und Ziffer), eine getönte Karte je Spiel („altbacken"), ein roter
Strich unter der Mannschaftszeile (lauter als die Ergebnisse) und graue Verlierer-Ziffern (auf
Papier schlechter lesbar). Vereinsnamen laufen **einzeilig** und werden sonst ausgepunktet.
- **Gegnerseite**: Duell symmetrisch und leise (beide Wappen gleich groß, der Name des Gegners steht
nur in der Überschrift laut da), darunter eine abgeleitete Aufmacher-Zeile („Sieben Siege aus acht
Spielen", aus den Tabellenwerten gebaut), der Vergleich als **Balkenpaar aus der Mitte** und zum
Schluss ein **Tabellenausschnitt** über die geteilte `league-table`-Komponente. Der Tabellenplatz
steht bewusst dort und nicht als Balken (klein ist besser, der Balken läge falsch herum) und auch
nicht als lose Ziffer im Kopf (war gebaut, wirkte unruhig).
- **Balkenlängen kommen aus Klassen in 5-Prozent-Stufen**, nie aus einem `style`-Attribut — die CSP
verwirft inline styles stillschweigend, die Balken wären erst nach dem Deploy unsichtbar.
- Die Gegnerseite druckt **`opponent_display`** (Kurzname der Ausgabe, wenn die Seite den Gegner der
Ausgabe zeigt); `opponent` bleibt die BFV-Schreibweise für den Namensvergleich.
**Cover (automatisch generiert, Phase 3):** einzige fotografische Stadionzeitungs-Seite —
`app/components/stadionzeitung-cover.php`. Echtes Magazin-Cover statt Website-Baustein: **kein**
Stadionfoto mehr als Hintergrund, stattdessen ein ruhiger Verlauf in Vereinsfarben, auf dem das

View File

@@ -40,19 +40,28 @@ $matchcenterLink = club_domain('matchcenter');
<section class="stadionzeitung-ergebnisse__team">
<h3 class="stadionzeitung-ergebnisse__team-name"><?= e(trim(implode(' · ', array_filter([(string) ($group['team_name'] ?? ''), (string) ($group['league'] ?? '')])))) ?></h3>
<?php foreach ($group['matches'] ?? [] as $match): ?>
<div class="stadionzeitung-ergebnisse__match">
<p class="stadionzeitung-ergebnisse__meta">
<?= e(trim((string) ($match['date_display'] ?? ''))) ?><?php if (($match['competition'] ?? 'Liga') !== 'Liga'): ?> · <?= e((string) $match['competition']) ?><?php endif; ?>
</p>
<?php foreach ([['club' => $match['home'] ?? '', 'crest' => $match['home_crest'] ?? null, 'goals' => $match['goals_home'] ?? null],
['club' => $match['away'] ?? '', 'crest' => $match['away_crest'] ?? null, 'goals' => $match['goals_away'] ?? null]] as $side): ?>
<div class="stadionzeitung-ergebnisse__row<?= str_contains((string) $side['club'], 'TSV 08 Kulmbach') ? ' stadionzeitung-ergebnisse__row--own' : '' ?>">
<?php
$sides = [
['club' => $match['home'] ?? '', 'crest' => $match['home_crest'] ?? null, 'goals' => $match['goals_home'] ?? null],
['club' => $match['away'] ?? '', 'crest' => $match['away_crest'] ?? null, 'goals' => $match['goals_away'] ?? null],
];
?>
<article class="stadionzeitung-ergebnisse__match">
<?php foreach ($sides as $i => $side): ?>
<?php if ($i === 1): ?>
<div class="stadionzeitung-ergebnisse__score">
<p class="stadionzeitung-ergebnisse__result">
<span class="stadionzeitung-ergebnisse__goals"><?= e((string) $sides[0]['goals']) ?></span><span class="stadionzeitung-ergebnisse__colon">:</span><span class="stadionzeitung-ergebnisse__goals"><?= e((string) $sides[1]['goals']) ?></span>
</p>
<p class="stadionzeitung-ergebnisse__meta"><?= e(trim((string) ($match['date_display'] ?? ''))) ?><?php if (($match['competition'] ?? 'Liga') !== 'Liga'): ?> · <?= e((string) $match['competition']) ?><?php endif; ?></p>
</div>
<?php endif; ?>
<div class="stadionzeitung-ergebnisse__side<?= str_contains((string) $side['club'], 'TSV 08 Kulmbach') ? ' stadionzeitung-ergebnisse__side--own' : '' ?>">
<?php component('crest', ['src' => $side['crest'], 'name' => (string) $side['club']]); ?>
<span class="stadionzeitung-ergebnisse__club"><?= e((string) $side['club']) ?></span>
<span class="stadionzeitung-ergebnisse__goals"><?= e((string) $side['goals']) ?></span>
</div>
<?php endforeach; ?>
</div>
</article>
<?php endforeach; ?>
</section>
<?php endforeach; ?>

View File

@@ -18,15 +18,29 @@ $teamName = trim((string) ($info['team_name'] ?? ''));
$fixture = is_array($info['fixture'] ?? null) ? $info['fixture'] : [];
$own = is_array($info['own'] ?? null) ? $info['own'] : null;
$opponent = is_array($info['opponent_stats'] ?? null) ? $info['opponent_stats'] : null;
$opponentName = (string) ($info['opponent'] ?? 'Unser Gegner');
// Gedruckt wird der Anzeigename (Kurzname der Ausgabe, wenn gepflegt) — die
// BFV-Schreibweise „(SG1) TSV Wirsberg I/ FC Kupferberg I" ist ein Suchschlüssel,
// keine Überschrift.
$ausschnitt = is_array($info['table_excerpt'] ?? null) ? $info['table_excerpt'] : [];
$opponentName = (string) ($info['opponent_display'] ?? $info['opponent'] ?? 'Unser Gegner');
// Links steht IMMER unsere Mannschaft (Klasse --own hebt sie hervor), rechts der
// Gegner. Bei einem Heimspiel ist das ohnehin die Reihenfolge Heim/Gast; fährt die
// Mannschaft aber auswärts, trüge die Hervorhebung sonst der gegnerische Verein.
$istHeim = !empty($fixture['is_home']);
$ownName = (string) (($istHeim ? ($fixture['home'] ?? null) : ($fixture['away'] ?? null)) ?? 'TSV 08 Kulmbach');
$ownCrest = ($istHeim ? ($fixture['home_crest'] ?? null) : ($fixture['away_crest'] ?? null)) ?? $own['crest'] ?? null;
$gegnerLabel = $opponentName;
$gegnerCrest = ($istHeim ? ($fixture['away_crest'] ?? null) : ($fixture['home_crest'] ?? null)) ?? $opponent['crest'] ?? null;
$kickoff = (string) ($fixture['kickoff'] ?? '');
$kickoffDate = $kickoff !== '' ? substr($kickoff, 0, 10) : '';
$kickoffTime = $kickoff !== '' ? substr($kickoff, 11, 5) : '';
// Der Tabellenplatz steht NICHT in der Balkenliste: klein ist dort besser,
// ein Balken läge genau falsch herum. Er gehört nach oben neben die Wappen.
// „Spiele“ fällt ebenfalls raus (für beide gleich, steht in der Kopfzeile).
$rows = [
['label' => 'Tabellenplatz', 'key' => 'rank'],
['label' => 'Spiele', 'key' => 'played'],
['label' => 'Siege', 'key' => 'won'],
['label' => 'Unentschieden', 'key' => 'drawn'],
['label' => 'Niederlagen', 'key' => 'lost'],
@@ -41,6 +55,57 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
$value = (int) $row[$key];
return $signed && $value > 0 ? '+' . $value : (string) $value;
};
// Balkenlänge als KLASSE, nie als style-Attribut: die CSP verwirft inline
// styles stillschweigend (siehe CLAUDE.md), der Balken wäre in Produktion
// unsichtbar. Deshalb 5-Prozent-Stufen, die components.css ausdefiniert.
$barStep = static function (?array $row, ?array $other, string $key): int {
if ($row === null) {
return 0;
}
$value = abs((int) ($row[$key] ?? 0));
$scale = max($value, $other === null ? 0 : abs((int) ($other[$key] ?? 0)));
if ($scale <= 0) {
return 0;
}
return (int) round($value / $scale * 20);
};
// Ein Satz statt einer Zahlenkolonne: derselbe Bestand, nur in Sprache. Die
// erste zutreffende Regel gewinnt, damit immer genau eine Aussage dasteht.
$zahlwort = static function (int $n): string {
$woerter = [0 => 'Kein', 1 => 'Ein', 2 => 'Zwei', 3 => 'Drei', 4 => 'Vier', 5 => 'Fünf',
6 => 'Sechs', 7 => 'Sieben', 8 => 'Acht', 9 => 'Neun', 10 => 'Zehn', 11 => 'Elf',
12 => 'Zwölf', 13 => 'Dreizehn', 14 => 'Vierzehn', 15 => 'Fünfzehn'];
return $woerter[$n] ?? (string) $n;
};
$aufmacher = '';
if ($opponent !== null) {
$played = (int) ($opponent['played'] ?? 0);
$won = (int) ($opponent['won'] ?? 0);
$lost = (int) ($opponent['lost'] ?? 0);
$rank = (int) ($opponent['rank'] ?? 0);
$points = (int) ($opponent['points'] ?? 0);
$diff = (int) ($opponent['goal_diff'] ?? 0);
if ($played > 0 && $lost === 0) {
$aufmacher = 'Noch ohne Niederlage';
} elseif ($played > 0 && $won === 0) {
$aufmacher = 'Noch ohne Sieg';
} elseif ($played > 0 && $won / $played >= 0.7) {
$aufmacher = $zahlwort($won) . ' Siege aus ' . mb_strtolower($zahlwort($played)) . ' Spielen';
} elseif ($rank === 1) {
$aufmacher = 'Tabellenführer mit ' . $points . ' Punkten';
} elseif ($played > 0 && $diff <= -8) {
$aufmacher = $zahlwort(abs($diff)) . ' Tore mehr kassiert als geschossen';
} elseif ($played > 0 && $diff >= 8) {
$aufmacher = $zahlwort($diff) . ' Tore mehr geschossen als kassiert';
} elseif ($played > 0) {
$aufmacher = $points . ' Punkte aus ' . $played . ' Spielen';
}
}
$spieltagInfo = $opponent !== null && (int) ($opponent['played'] ?? 0) > 0
? 'Stand nach ' . (int) $opponent['played'] . ' Spieltagen'
: '';
?>
<div class="stadionzeitung-gegner">
<header class="stadionzeitung-page__head">
@@ -53,25 +118,26 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
<div class="stadionzeitung-gegner__duell">
<div class="stadionzeitung-gegner__club stadionzeitung-gegner__club--own">
<?php component('crest', ['src' => $fixture['home_crest'] ?? $own['crest'] ?? null, 'name' => $fixture['home'] ?? 'TSV 08 Kulmbach']); ?>
<strong><?= e((string) ($fixture['home'] ?? 'TSV 08 Kulmbach')) ?></strong>
<div class="stadionzeitung-gegner__club">
<?php component('crest', ['src' => $ownCrest, 'name' => $ownName]); ?>
<span class="stadionzeitung-gegner__club-name"><?= e($ownName) ?></span>
</div>
<div class="stadionzeitung-gegner__match">
<span>gegen</span>
<div class="stadionzeitung-gegner__vs">
<p class="stadionzeitung-gegner__vs-wort"><?= $istHeim ? 'gegen' : 'bei' ?></p>
<?php if ($kickoffDate !== ''): ?>
<strong><?= e(german_date($kickoffDate)) ?></strong>
<?php endif; ?>
<?php if ($kickoffTime !== ''): ?>
<span><?= e($kickoffTime) ?> Uhr</span>
<p class="stadionzeitung-gegner__anstoss"><?= e(german_date($kickoffDate)) ?><?php if ($kickoffTime !== ''): ?><br><?= e($kickoffTime) ?> Uhr<?php endif; ?></p>
<?php endif; ?>
</div>
<div class="stadionzeitung-gegner__club">
<?php component('crest', ['src' => $fixture['away_crest'] ?? $opponent['crest'] ?? null, 'name' => $fixture['away'] ?? $opponentName]); ?>
<strong><?= e((string) ($fixture['away'] ?? $opponentName)) ?></strong>
<?php component('crest', ['src' => $gegnerCrest, 'name' => $gegnerLabel]); ?>
<span class="stadionzeitung-gegner__club-name"><?= e($gegnerLabel) ?></span>
</div>
</div>
<?php if ($aufmacher !== ''): ?>
<p class="stadionzeitung-gegner__aufmacher"><?= e($aufmacher) ?></p>
<?php endif; ?>
<?php if ($own !== null && $opponent !== null): ?>
<div class="stadionzeitung-gegner__vergleich" role="table" aria-label="Tabellenvergleich">
<div class="stadionzeitung-gegner__vergleich-head" role="row">
@@ -79,18 +145,33 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
Markup — das Kürzel des Marktschorgast-Gegners wäre in jeder Ausgabe
mitgedruckt worden. Die Überschrift nennt den Gegner ohnehin. */ ?>
<span role="columnheader">Wir</span>
<span role="columnheader">Vergleich</span>
<span role="columnheader"><?= e($spieltagInfo !== '' ? $spieltagInfo : 'Vergleich') ?></span>
<span role="columnheader">Gegner</span>
</div>
<?php foreach ($rows as $row): ?>
<div class="stadionzeitung-gegner__vergleich-row" role="row">
<strong role="cell"><?= e($format($own, (string) $row['key'], (bool) ($row['signed'] ?? false))) ?></strong>
<span role="rowheader"><?= e((string) $row['label']) ?></span>
<strong role="cell"><?= e($format($opponent, (string) $row['key'], (bool) ($row['signed'] ?? false))) ?></strong>
<strong class="stadionzeitung-gegner__wert" role="cell"><?= e($format($own, (string) $row['key'], (bool) ($row['signed'] ?? false))) ?></strong>
<span class="stadionzeitung-gegner__balken stadionzeitung-gegner__balken--own" aria-hidden="true"><span class="stadionzeitung-gegner__balken-fuellung stadionzeitung-gegner__balken-fuellung--s<?= $barStep($own, $opponent, (string) $row['key']) ?>"></span></span>
<span class="stadionzeitung-gegner__label" role="rowheader"><?= e((string) $row['label']) ?></span>
<span class="stadionzeitung-gegner__balken" aria-hidden="true"><span class="stadionzeitung-gegner__balken-fuellung stadionzeitung-gegner__balken-fuellung--s<?= $barStep($opponent, $own, (string) $row['key']) ?>"></span></span>
<strong class="stadionzeitung-gegner__wert" role="cell"><?= e($format($opponent, (string) $row['key'], (bool) ($row['signed'] ?? false))) ?></strong>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="stadionzeitung-gegner__empty">Die aktuellen Vergleichsdaten waren beim letzten Abruf nicht verfügbar.</p>
<?php endif; ?>
<?php /* Der Tabellenplatz stand kurz als zwei lose Ziffern oben im Duell und
wirkte dort unruhig (Felix, 19.09.2026). Hier zeigt ihn der Ausschnitt
im Zusammenhang: wer steht zwischen uns und ihnen. Gerendert mit der
geteilten league-table-Komponente, nie mit einer zweiten Tabelle. */ ?>
<?php if ($ausschnitt !== []): ?>
<div class="stadionzeitung-gegner__ausschnitt">
<p class="stadionzeitung-gegner__ausschnitt-titel">Die Tabelle dazwischen</p>
<?php foreach ($ausschnitt as $block): ?>
<?php component('league-table', ['table' => $block, 'caption' => 'Tabellenausschnitt ' . (string) ($info['league'] ?? '')]); ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
/**
* Mitglied-werden-Seite der Stadionzeitung (Live-Seite) — Poster statt Liste:
* riesige zentrierte Coolvetica-Headline („Werde Teil des Nullachters", das
* riesige zentrierte Coolvetica-Headline („Werde Teil unseres Vereins", das
* letzte Wort in Vereinsrot), darunter Claim-Satz und Abteilungszeile,
* unten der geteilte CTA-Fuß mit QR auf /mitglied-werden. Fester Baustein
* jeder Ausgabe (bin/stadionzeitung-add.php plant sie nach den
@@ -25,8 +25,8 @@ $mitgliedLink = club_domain('mitglied-werden');
<img class="stadionzeitung-mitglied__logo" src="<?= e(asset('img/logo.svg')) ?>" alt="" width="75" height="97">
<h2 class="stadionzeitung-mitglied__headline">
<span class="stadionzeitung-mitglied__line">Werde Teil</span>
<span class="stadionzeitung-mitglied__line">des</span>
<span class="stadionzeitung-mitglied__line stadionzeitung-mitglied__line--accent">Nullachters</span>
<span class="stadionzeitung-mitglied__line">unseres</span>
<span class="stadionzeitung-mitglied__line stadionzeitung-mitglied__line--accent">Vereins</span>
</h2>
<p class="stadionzeitung-mitglied__claim">Seit 1908 in Kulmbach zuhause. Egal ob auf dem Platz, in der Halle oder am Spielfeldrand: bei uns ist Platz für dich.</p>
<p class="stadionzeitung-mitglied__departments">Fußball · Jugend · Turnen</p>

View File

@@ -7261,74 +7261,136 @@ a.news-feature__link:hover .news-feature__more {
}
/* === stadionzeitung-ergebnisse (Live-Seite) === */
/* Ergebnis-Rückblick: zwei Zeilen pro Spiel (Heim über Gast, wie das
„Nächste Spiele"-Band) — lange Vereinsnamen passen auf der schmalen
A5-Seite nie in eine Zeile. Eigene Mannschaft über Gewicht der Body-Schrift
markiert (Abel hat echte Schnitte — anders als Coolvetica erlaubt, siehe
CLAUDE.md „nie über font-weight" bei Coolvetica). */
/* Ergebnis-Rückblick als Anzeigetafel: Wappen und Vereinsname je Seite, das
Ergebnis groß in der Mitte (Vorbild kicker/Sofascore). Vierter Entwurf,
19.09.2026 — die drei Vorgänger scheiterten alle an derselben Ursache:
sie waren Listenzeilen, und eine Zeile stellt Heim und Gast untereinander
statt gegenüber, obwohl eine Begegnung etwas Symmetrisches ist. Mit den
Toren rechts am Blattrand lag halbe Seitenbreite Leere zwischen Name und
Ziffer, eine getönte Karte dagegen sah „altbacken" aus, und mit den Toren
links fehlte der Seite die Hierarchie.
Die Mittelachse löst zusätzlich das Namensproblem: lange Namen wie
„(SG1) TSV Melkendorf III/ TSV 08 Kulmbach II" umbrechen in ihrer Spalte
über mehrere Zeilen, ohne dass irgendetwas verrutscht — deshalb steht der
Name UNTER dem Wappen und mittig, nicht daneben.
Flächen gibt es keine, nur Linien: Haarlinie zwischen den Begegnungen,
roter Strich unter der Mannschaftszeile, sonst weißes Blatt. */
/* Trennung der Mannschaften: Haarlinie plus reichlich Abstand auf beiden
Seiten. Die Linie liegt am Abschnitt, nicht an der letzten Begegnung darin —
sonst wanderte sie bei mehreren Spielen pro Mannschaft mitten in den Block. */
.stadionzeitung-ergebnisse__team {
margin-bottom: 4.2cqi;
}
.stadionzeitung-ergebnisse__team-name {
margin-bottom: 1.7cqi;
font-family: var(--ff-body);
font-weight: var(--fw-bold);
font-size: 2.7cqi;
text-transform: uppercase;
letter-spacing: var(--ls-caps);
color: var(--clr-text-muted);
}
.stadionzeitung-ergebnisse__match {
padding-bottom: 1.7cqi;
margin-bottom: 1.7cqi;
padding-bottom: 4.6cqi;
margin-bottom: 4.6cqi;
border-bottom: 1px solid var(--clr-glass-border);
}
.stadionzeitung-ergebnisse__match:last-child {
.stadionzeitung-ergebnisse__team:last-of-type {
padding-bottom: 0;
margin-bottom: 0;
border-bottom: 0;
}
.stadionzeitung-ergebnisse__meta {
font-size: 2.5cqi;
/* Mannschaftszeile bewusst leise: die Gliederung macht der Abstand zwischen
den Mannschaften, keine Linie (ein roter Strich darunter war gebaut und am
19.09.2026 wieder entfernt — er zog mehr Aufmerksamkeit als die Ergebnisse
darunter). */
.stadionzeitung-ergebnisse__team-name {
margin-bottom: 1.6cqi;
font-family: var(--ff-body);
font-size: 2.4cqi;
text-align: center;
text-transform: uppercase;
letter-spacing: var(--ls-caps);
color: var(--clr-text-faint);
}
/* Das Zeilen-Layout gehört auf die ZEILE, nicht auf das Wappen darin (stand
hier mal als `.stadionzeitung-ergebnisse__row .crest` — dann wurde das Wappen
selbst zum Grid, lag als Block-Element auf eigener Zeile, und Vereinsname und
Tore rutschten als Inline-Spans darunter zusammen). `--crest-size` bleibt
trotzdem hier: Custom Properties erben, das Wappen bleibt eine dumme
Komponente und der Kontext gibt ihm seine Größe — so machen es alle anderen
Wappen-Kontexte im Projekt auch. */
.stadionzeitung-ergebnisse__row {
--crest-size: 5.8cqi;
.stadionzeitung-ergebnisse__match {
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 1.7cqi;
padding-block: 0.6cqi;
gap: 2.1cqi;
padding-block: 1.4cqi;
border-bottom: 1px solid var(--clr-glass-border);
}
.stadionzeitung-ergebnisse__match:last-child {
padding-bottom: 0;
border-bottom: 0;
}
/* Beide Seiten sind gleich gebaut und gleich breit (1fr), damit die Mittelachse
wirklich in der Mitte liegt — auch wenn eine Spielgemeinschaft drei Zeilen
Namen mitbringt und der Gegner eine. `--crest-size` setzt der Kontext, die
crest-Komponente bleibt dumm (gleiches Muster wie in allen anderen
Wappen-Kontexten). */
.stadionzeitung-ergebnisse__side {
--crest-size: 8.5cqi;
display: grid;
justify-items: center;
gap: 1.2cqi;
text-align: center;
}
/* Einzeilig, notfalls ausgepunktet: ein umbrechender Name schiebt die
Anzeigetafel auseinander und die Zeilenpaare stehen dann ungleich hoch
nebeneinander. Die volle Schreibweise steht ohnehin im Matchcenter. */
.stadionzeitung-ergebnisse__club {
min-width: 0;
font-size: 3.75cqi;
line-height: 1.2;
max-width: 100%;
overflow: hidden;
font-size: 3.2cqi;
line-height: 1.15;
white-space: nowrap;
text-overflow: ellipsis;
}
.stadionzeitung-ergebnisse__row--own .stadionzeitung-ergebnisse__club {
.stadionzeitung-ergebnisse__side--own .stadionzeitung-ergebnisse__club {
font-weight: var(--fw-bold);
}
.stadionzeitung-ergebnisse__goals {
.stadionzeitung-ergebnisse__score {
display: grid;
justify-items: center;
gap: 0.8cqi;
}
.stadionzeitung-ergebnisse__result {
display: flex;
align-items: baseline;
gap: 0.8cqi;
font-family: var(--ff-heading);
font-size: 5.8cqi;
font-size: 7.4cqi;
line-height: 1;
}
/* Das Ergebnis läuft durchgehend in voller Textfarbe: eine zurückgenommene
Verlierer-Ziffer war gebaut und am 19.09.2026 verworfen (Felix) — auf dem
gedruckten Blatt ist eine graue Ziffer schlechter lesbar, und wer gewonnen
hat, sagt die Zahl selbst. */
.stadionzeitung-ergebnisse__meta {
font-size: 2.5cqi;
text-transform: uppercase;
letter-spacing: var(--ls-caps);
color: var(--clr-text-faint);
white-space: nowrap;
}
/* === stadionzeitung-gegner (Live-Seite) === */
/* Gegnerseite als Duell-Aufmacher plus Balkenvergleich (19.09.2026). Vorher
war das eine rosa getönte Karte mit der Begegnung und darunter eine
Zahlenkolonne aus sieben Zeilen: inhaltlich richtig, gestalterisch ohne
Blickfang, und die untere Blatthälfte blieb leer.
Drei Entscheidungen tragen den Aufbau:
- Das GEGNERWAPPEN ist der Bildanker (deutlich größer als unseres). Die
Seite handelt von ihm, nicht von der Begegnung.
- Der Tabellenplatz steht oben bei den Wappen, NICHT als Balken: dort ist
klein besser, ein Balken läge genau falsch herum.
- Der Vergleich läuft als Balkenpaar aus der Mitte (Vorbild Sofascore).
Die Länge kommt aus einer KLASSE in 5-Prozent-Stufen, nie aus einem
style-Attribut: die CSP verwirft inline styles stillschweigend, der
Balken wäre in Produktion unsichtbar (siehe CLAUDE.md). */
.stadionzeitung-gegner {
min-height: 100%;
}
@@ -7336,90 +7398,204 @@ a.news-feature__link:hover .news-feature__more {
.stadionzeitung-gegner__duell {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
align-items: start;
gap: 2.5cqi;
margin-bottom: 5cqi;
padding: 3.3cqi;
border: 1px solid var(--clr-glass-border);
border-radius: var(--radius-btn);
background: color-mix(in srgb, var(--clr-accent) 5%, #fff);
margin-top: 2.5cqi;
}
/* Beide Seiten sind gleich gebaut und gleich groß. Der Gegner war hier kurz
mit doppelt so großem Wappen und seinem Namen in Coolvetica gesetzt — damit
stand er zweimal laut auf derselben Seite (Überschrift und Duell) und das
Blatt wirkte unruhig (Felix, 19.09.2026). Die Überschrift nennt ihn, das
Duell zeigt nur noch, wer gegen wen und auf welchem Platz. */
.stadionzeitung-gegner__club {
--crest-size: 12cqi;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.7cqi;
--crest-size: 11cqi;
display: grid;
grid-template-rows: auto minmax(7.4cqi, auto);
justify-items: center;
gap: 1.2cqi;
min-width: 0;
text-align: center;
}
.stadionzeitung-gegner__club strong {
.stadionzeitung-gegner__club-name {
font-size: 3.2cqi;
line-height: 1.15;
}
.stadionzeitung-gegner__club--own strong {
color: var(--clr-accent);
}
.stadionzeitung-gegner__match {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5cqi;
font-size: 2.5cqi;
color: var(--clr-text-muted);
/* Mittelspalte: „gegen" und der Anstoß stehen zusammen, damit im Duell nur
noch drei Dinge auf einer Höhe liegen (wir, wann, sie). Zwei lose
Platz-Ziffern und eine freistehende Datumszeile standen hier kurz und
machten den Kopf unruhig (Felix, 19.09.2026) — der Tabellenplatz steht
jetzt im Ausschnitt unter den Balken. */
.stadionzeitung-gegner__vs {
display: grid;
justify-items: center;
gap: 1.2cqi;
padding-top: 3.3cqi;
text-align: center;
white-space: nowrap;
}
.stadionzeitung-gegner__match strong {
font-size: 2.9cqi;
color: var(--clr-text);
.stadionzeitung-gegner__vs-wort {
font-size: 2.7cqi;
text-transform: uppercase;
letter-spacing: var(--ls-caps);
color: var(--clr-text-faint);
}
.stadionzeitung-gegner__anstoss {
font-size: 2.7cqi;
line-height: 1.4;
color: var(--clr-text-muted);
}
/* Im Ausschnitt entfällt die Zonen-Legende: sie erklärt Auf- und Abstieg,
und das ist die Aufgabe der vollständigen Tabellenseite weiter hinten im
Heft. Hier geht es nur um die Nachbarschaft der beiden Mannschaften. */
.stadionzeitung-gegner__ausschnitt {
margin-top: 3.3cqi;
}
.stadionzeitung-gegner__ausschnitt .league-table__legend {
display: none;
}
/* Der Ausschnitt läuft eine Spur enger als die vollständige Tabellenseite:
er teilt sich das Blatt mit Duell, Aufmacher und Balken. Gemessen: mit den
Maßen der Tabellenseite stand die Seite 16px über der A5-Kante. */
.stadionzeitung-gegner__ausschnitt .league-table th,
.stadionzeitung-gegner__ausschnitt .league-table td {
padding: 0.7cqi 0.9cqi;
}
.stadionzeitung-gegner__ausschnitt .league-table tbody .crest {
--crest-size: 4cqi;
}
.stadionzeitung-gegner__ausschnitt-titel {
margin-bottom: 1.4cqi;
font-size: 2.4cqi;
text-transform: uppercase;
letter-spacing: var(--ls-caps);
color: var(--clr-text-faint);
}
/* Zwei Blöcke (wenn die Mannschaften weit auseinanderliegen) bekommen Luft
dazwischen, sonst sähe es aus wie eine durchgehende Tabelle mit Sprung. */
.stadionzeitung-gegner__ausschnitt .table-scroll-frame + .table-scroll-frame {
margin-top: 2.1cqi;
}
/* Die Tabelle in einem Satz: derselbe Bestand, nur in Sprache. Coolvetica
trägt hier, weil die Zeile kurz ist und nie umbricht. */
.stadionzeitung-gegner__aufmacher {
margin-top: 3.3cqi;
padding-top: 2.5cqi;
border-top: 0.5cqi solid var(--clr-accent);
font-family: var(--ff-heading);
font-size: 5.8cqi;
line-height: 1;
letter-spacing: var(--ls-heading);
text-transform: uppercase;
}
.stadionzeitung-gegner__vergleich {
border-block: 1px solid var(--clr-glass-border);
margin-top: 2.5cqi;
}
.stadionzeitung-gegner__vergleich-head,
.stadionzeitung-gegner__vergleich-row {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
grid-template-columns: 6cqi minmax(0, 1fr) 24cqi minmax(0, 1fr) 6cqi;
align-items: center;
gap: 1.7cqi;
text-align: center;
}
.stadionzeitung-gegner__vergleich-head {
padding-block: 1.7cqi;
font-size: 2.5cqi;
font-weight: var(--fw-bold);
padding-bottom: 1.4cqi;
margin-bottom: 1.4cqi;
border-bottom: 1px solid var(--clr-glass-border);
font-size: 2.4cqi;
text-transform: uppercase;
letter-spacing: var(--ls-caps);
color: var(--clr-text-muted);
color: var(--clr-text-faint);
}
/* Die beiden äußeren Spaltenköpfe sitzen über den Zahlen, der mittlere über
dem Label — deshalb spannt er über die drei inneren Spalten. */
.stadionzeitung-gegner__vergleich-head span:nth-child(2) {
grid-column: 2 / 5;
}
.stadionzeitung-gegner__vergleich-row {
min-height: 6.5cqi;
border-top: 1px solid var(--clr-glass-border);
min-height: 5.8cqi;
}
.stadionzeitung-gegner__vergleich-row span {
.stadionzeitung-gegner__wert {
font-family: var(--ff-heading);
font-size: 4.4cqi;
line-height: 1;
}
.stadionzeitung-gegner__label {
font-size: 2.8cqi;
color: var(--clr-text-muted);
}
.stadionzeitung-gegner__vergleich-row strong {
font-family: var(--ff-heading);
font-size: 4.8cqi;
line-height: 1;
/* Balkenpaar: der linke wächst aus der Mitte nach links, der rechte nach
rechts. Die Spur bleibt sichtbar (helle Fläche), damit auch ein Wert von
null als Zeile lesbar ist. */
.stadionzeitung-gegner__balken {
display: flex;
justify-content: flex-start;
block-size: 2.2cqi;
border-radius: 1.1cqi;
background: var(--clr-glass-border);
overflow: hidden;
}
.stadionzeitung-gegner__vergleich-row strong:first-child {
color: var(--clr-accent);
.stadionzeitung-gegner__balken--own {
justify-content: flex-end;
}
.stadionzeitung-gegner__balken-fuellung {
display: block;
block-size: 100%;
border-radius: 1.1cqi;
background: var(--clr-text);
}
/* Unsere Seite rot, die des Gegners schwarz: die einzige Farbe der Seite
steht damit bei uns, ohne dass Rot Fließtext tragen müsste. */
.stadionzeitung-gegner__balken--own .stadionzeitung-gegner__balken-fuellung {
background: var(--clr-accent);
}
/* 5-Prozent-Stufen, siehe Kommentar oben (kein style-Attribut). */
.stadionzeitung-gegner__balken-fuellung--s0 { inline-size: 0%; }
.stadionzeitung-gegner__balken-fuellung--s1 { inline-size: 5%; }
.stadionzeitung-gegner__balken-fuellung--s2 { inline-size: 10%; }
.stadionzeitung-gegner__balken-fuellung--s3 { inline-size: 15%; }
.stadionzeitung-gegner__balken-fuellung--s4 { inline-size: 20%; }
.stadionzeitung-gegner__balken-fuellung--s5 { inline-size: 25%; }
.stadionzeitung-gegner__balken-fuellung--s6 { inline-size: 30%; }
.stadionzeitung-gegner__balken-fuellung--s7 { inline-size: 35%; }
.stadionzeitung-gegner__balken-fuellung--s8 { inline-size: 40%; }
.stadionzeitung-gegner__balken-fuellung--s9 { inline-size: 45%; }
.stadionzeitung-gegner__balken-fuellung--s10 { inline-size: 50%; }
.stadionzeitung-gegner__balken-fuellung--s11 { inline-size: 55%; }
.stadionzeitung-gegner__balken-fuellung--s12 { inline-size: 60%; }
.stadionzeitung-gegner__balken-fuellung--s13 { inline-size: 65%; }
.stadionzeitung-gegner__balken-fuellung--s14 { inline-size: 70%; }
.stadionzeitung-gegner__balken-fuellung--s15 { inline-size: 75%; }
.stadionzeitung-gegner__balken-fuellung--s16 { inline-size: 80%; }
.stadionzeitung-gegner__balken-fuellung--s17 { inline-size: 85%; }
.stadionzeitung-gegner__balken-fuellung--s18 { inline-size: 90%; }
.stadionzeitung-gegner__balken-fuellung--s19 { inline-size: 95%; }
.stadionzeitung-gegner__balken-fuellung--s20 { inline-size: 100%; }
.stadionzeitung-gegner__empty {
padding: 4.2cqi;
border: 1px solid var(--clr-glass-border);