Sportfest-Ausgabe der Stadionzeitung zum Heimspiel gegen ATS Wartenfels
Neue Live-Seiten, geschaltet vom chat-gepflegten Feld `veranstaltung` (Slug aus data/veranstaltungen.json) — Programm und Turnierpläne kommen damit aus derselben Quelle wie die Website, nicht als Kopie ins Heft: - Programm-Seite (programm[], rendert zugleich auf /veranstaltungen/sportfest-2026) - vier Turnierplan-Seiten (turniere[]) mit leerer Ergebnisspalte und leerer Abschlusstabelle zum Eintragen am Platz - Gegnerseite je Mannschaft mit Heimspiel am Fest statt nur der tragenden: stadionzeitung_gegner_fixtures() ist die einzige Stelle, die das entscheidet, Seitenplanung und Snapshot lesen beide dort. `gegner` ist deshalb eine Map je Team-Key; die Vorschau überspringt jedes Spiel mit eigener Gegnerseite - Danke-Seite mit Partner-Logo-Wand aus data/partners.json und QR auf /partner-werden, dazu das Anzeigen-Feld `platzierung` für Motive mit festem Platz auf einer Live-Seite (Emons über der Logo-Wand). Vorher klebte der Dank als leere Fläche unten an einem Anzeigen-Bild - Cover einer Sonderausgabe: das Fest als Titelgeschichte statt einer der drei Begegnungen, dazu das Hauptplakat schräg gelegt an Stelle des Spielerfotos Ausgabe 2026-09-04-ats-wartenfels als ENTWURF angelegt, 48 Seiten (12 Bögen), Tabellen auf gerader Seite, längster Anzeigenblock zwei. Veröffentlicht wird auf dem Server, dort liegt der frische Matchcenter-Stand. Nebenbei behoben: bin/anzeige-add.php räumte beim Anlegen alle Motive weg, deren Slug mit demselben Text beginnt (`emons` löschte `emons-partnerdank` und `emons-stuebinger`). Muster jetzt auf die Breiten-Endung eingeschränkt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KubsajzuV6N6QsSd3xKNjC
110
CLAUDE.md
@@ -54,10 +54,10 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern.
|
||||
| Partner/Sponsoren | `data/partners.json` | — |
|
||||
| Instagram-Cache | `data/instagram.json` + `public/assets/img/instagram/` | **maschinenverwaltet von `bin/instagram-sync.php` — nie von Hand editieren** |
|
||||
| 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` und `interview`.** 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` 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 |
|
||||
| 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 |
|
||||
| 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, 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`) |
|
||||
| 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`) |
|
||||
| News (`/news` + optionale Detailseiten) | `data/news.json` (redaktionelle Meldungen; `artikel[]` = Fließtext-Blöcke, `status` = Entwurf/veröffentlicht) — Instagram-Posts und Ergebnisse kommen weiterhin nur aus `data/instagram.json`/`data/matchcenter.json`, werden auf der News-Seite nur gemergt | Von Claude im Chat gepflegt wie `events.json`/`teams.json` — **keine** Automatisierung, kein Bin-Skript. Instagram/Ergebnisse nie hier duplizieren. `text` bleibt der Teaser (Feed, Meta-Description, Article-Schema), der Artikel steht in `artikel[]` |
|
||||
| Dateiablage `/dateien` (Konfiguration) | `public/dateien/_filesconfig.php` (Rechte pro Konto: `storage/dateien/system/users/admin/config.php`) | **`storage/dateien/system/config/config.php` von Hand editieren** — die App generiert sie selbst und überschreibt sie bei jedem Update. Passwörter/Lizenz gehören in `config.php` (`dateien.*`) |
|
||||
| Icons | `public/assets/icons/` (Bootstrap Icons, lokal) via `icon()`-Helper | Inline-SVG-Pfade von Hand ins Markup, Emoji/Unicode-Glyphen, gezeichnete CSS-Icons, Icon-Font/CDN |
|
||||
@@ -516,18 +516,23 @@ kein Download-Link — nur die Web-Ansicht. Der Ablauf pro Heimspiel ist damit:
|
||||
**Feste Vorlage** (seit Interview 29.07.2026, Anzeigen-Verteilung überarbeitet 31.07.2026) — der
|
||||
Inhalt ist eine Liste von **Blöcken**, die Anzeigen füllen die Lücken dazwischen (⌷ = Lücke):
|
||||
```
|
||||
Cover → [Vorwort → Interview (nur wenn die Ausgabe eins trägt)] ⌷ Ergebnis-Rückblick ⌷
|
||||
Gegnerstatistik der tragenden Mannschaft (automatisch aus deren Tabelle) ⌷
|
||||
Cover → [Vorwort → Programm* → Interview → Aufruf (jedes nur wenn die Ausgabe es trägt)] ⌷
|
||||
[Turnierpläne*: je Turnier eine Seite, EIN Block] ⌷ Ergebnis-Rückblick ⌷
|
||||
[Gegnerstatistik: normal nur die tragende Mannschaft, in der Sportfest-Ausgabe je
|
||||
Mannschaft mit Heimspiel am Fest eine — EIN Block, automatisch aus deren Tabelle] ⌷
|
||||
[je Mannschaft eine Tabelle, Reihenfolge = matchcenter.json — EIN Block, dazwischen nie eine
|
||||
Anzeige; Erste + Zweite bilden im gefalteten Heft eine Doppelseite] ⌷ Vorschau ⌷ Termine ⌷
|
||||
News ⌷ Ansprechpartner ⌷ Mitglied-werden-Poster ⌷ Historie-Teaser (Website-Verweis +
|
||||
Material-Aufruf an club.email_historie) ⌷ Sportheim-Seite (Vermietungs-CTA im Stil der alten
|
||||
Canva-Seite) ⌷ Partner-werden-Seite (Anzeigen-Akquise im eigenen Heft, Sie-Form) ⌷
|
||||
Canva-Seite) ⌷ Partner-werden-Seite (Anzeigen-Akquise im eigenen Heft, Sie-Form) →
|
||||
Danke-Seite (Partner-Logo-Wand + QR auf /partner-werden, ohne Lücke davor: Akquise und Dank
|
||||
gehören zusammen) ⌷
|
||||
Momente-Seite (2×2-Galerie der neuesten Instagram-Fotos + Folge-uns-QR, Snapshot beim
|
||||
Veröffentlichen) ⌷ Impressum (aus club.json) → Rückseite
|
||||
```
|
||||
Alle Seiten außer Rückseite und Vorwort-Fallback sind Live-Seiten. Nach dem Cover, im Aufschlag
|
||||
und vor der Rückseite steht **keine** Lücke (Begründung unten).
|
||||
und vor der Rückseite steht **keine** Lücke (Begründung unten). \* = nur Sonderausgabe, siehe
|
||||
„Sportfest-Ausgabe" unten.
|
||||
**Doppelseiten-Regel fürs Falt-Heft:** aufgeschlagen liegen die Seiten (2,3), (4,5), … nebeneinander
|
||||
(gerade Seite links). Die erste Tabellenseite muss deshalb auf einer GERADEN Seitenzahl liegen, damit
|
||||
die Tabellen nebeneinander stehen. **Das rechnet das Skript selbst** (seit 31.07.2026): es verschiebt
|
||||
@@ -548,12 +553,30 @@ Lücken bekommen 7 Lücken zwei. Zwei hintereinander ist das Optimum, nicht eins
|
||||
Zweier-Paar auf einer Doppelseite landen kann, schlägt man das Heft gelegentlich auf zwei Anzeigen auf.
|
||||
Ein `images[]`-Eintrag hat
|
||||
`"type": "image"` (wie gehabt, `base`/`widths`/`alt`) oder `"type": "live"` (`page`: `cover` |
|
||||
`vorwort` | `interview` | `aufruf` | `tabelle` | `termine` | `news` | `ergebnisse` | `gegner` | `vorschau` | `kontakt` | `mitglied` | `historie` | `sportheim` | `partner` | `momente` | `impressum`, kein Bild;
|
||||
`tabelle` zusätzlich mit `team`: Team-Key aus `matchcenter.json`, ohne `team` greift der Team-Key
|
||||
der Ausgabe). Fehlt `type` (alte Phase-1-Ausgaben) → Fallback `image`, bricht nicht. **Gerendert
|
||||
`vorwort` | `interview` | `programm` | `aufruf` | `turnier` | `tabelle` | `termine` | `news` | `ergebnisse` | `gegner` | `vorschau` | `kontakt` | `mitglied` | `historie` | `sportheim` | `partner` | `danke` | `momente` | `impressum`, kein Bild;
|
||||
`tabelle` und `gegner` zusätzlich mit `team`: Team-Key aus `matchcenter.json`, ohne `team` greift der
|
||||
Team-Key der Ausgabe; `turnier` mit `turnier`: Key aus `turniere[]` der Ausgaben-Veranstaltung;
|
||||
`danke` mit `anzeige`: das platzierte Anzeigenmotiv, siehe unten). Fehlt `type` (alte Phase-1-Ausgaben) → Fallback `image`, bricht nicht. **Gerendert
|
||||
wird jede Seite (Bild wie Live) über `app/components/stadionzeitung-seite.php`** — die eine
|
||||
gemeinsame Render-Stelle für Viewer und Druckfassung, nie zwei Implementierungen derselben Seite.
|
||||
|
||||
**Danke-Seite + platzierte Anzeigen (03.09.2026).** Der Dank an die Partner war bis dahin unten an
|
||||
ein Anzeigen-Bild geklebt (ein Canva-Motiv, das Sponsorenanzeige und Dank in einer Datei hatte) und
|
||||
bestand aus Wappen, Titel und einer Zeile auf leerer Fläche. Jetzt ist es eine Live-Seite
|
||||
(`stadionzeitung-danke.php`): die **Logo-Wand aus `data/partners.json`** (Single Source, dieselbe
|
||||
Quelle wie die Startseite), Kopf und der QR auf `/partner-werden`. Dazu ein neues, generisches Feld
|
||||
im Anzeigenbestand: **`platzierung`** nimmt eine Anzeige aus dem Streu-Pool und gibt ihr einen festen
|
||||
Platz auf einer Live-Seite (derzeit `danke`, dort sitzt die Emons-Anzeige über der Logo-Wand).
|
||||
- **Das Motiv steht im Seiteneintrag, nicht in der Komponente**: `bin/stadionzeitung-add.php` schreibt
|
||||
es als `anzeige` in den `images[]`-Eintrag. Dadurch ist es eingefroren wie jede andere Anzeige, und
|
||||
eine veröffentlichte Ausgabe ändert sich nicht, wenn der Sponsor später ein neues Motiv liefert.
|
||||
- **Die Seitenzahl bleibt gleich**: eine Anzeige verlässt den Pool, eine Live-Seite kommt dazu.
|
||||
- Das Kombi-Motiv wurde an seiner roten Trennlinie zerschnitten, damit die Sponsorenanzeige
|
||||
vollständig bleibt und der Dank nicht zweimal im Heft steht.
|
||||
- **Falle in `bin/anzeige-add.php`** (dabei gefunden und behoben): das Aufräum-Muster lautete
|
||||
`{$slug}-*.jpg` und traf damit jeden Slug, der den neuen als Präfix hat — `emons` löschte die
|
||||
Motive von `emons-partnerdank`. Jetzt `{$slug}-[0-9]*.jpg`.
|
||||
|
||||
**Entwurf / Veröffentlicht (`status`-Feld):** `bin/stadionzeitung-add.php` legt jede Ausgabe als
|
||||
`entwurf` an — unsichtbar in Archiv und Sitemap, `noindex`, aber unter ihrer direkten URL erreichbar
|
||||
(zum Gegenlesen ohne Login-System; der Ausgaben-Kopf zeigt einen „Entwurf"-Badge). Im Entwurf rechnen
|
||||
@@ -635,6 +658,73 @@ Fragen/Antworten, Claude trägt sie ins `interview`-Feld der Ausgabe ein (`tease
|
||||
Live-Seite nur ein, wenn das Feld existiert — Ausgaben ohne Interview haben weder Seite noch
|
||||
Teaser-Zeile. Fragen in Abel fett, Antworten in `--ff-serif` (dieselbe Begründung wie beim Vorwort).
|
||||
|
||||
**Sportfest-Ausgabe (03.09.2026): ein Slug-Verweis schaltet drei Seitentypen zu.** Trägt eine Ausgabe
|
||||
das chat-gepflegte Feld `veranstaltung` (Slug aus `data/veranstaltungen.json`), kommen dazu: die
|
||||
**Programm-Seite** (`stadionzeitung-programm.php`, aus `programm[]` der Veranstaltung), ein Block
|
||||
**Turnierplan-Seiten** (`stadionzeitung-turnier.php`, je Eintrag in `turniere[]` eine Seite) und eine
|
||||
**Gegnerseite je Mannschaft mit Heimspiel im Zeitraum** der Veranstaltung statt nur für die tragende.
|
||||
Dazu trägt `cover.teaser` die Coverzeile „Im Heft"; das Feld überlebt erneute Skript-Läufe wie
|
||||
`vorwort`. **`stadionzeitung_teaser()`** ist die eine Quelle dieser Zeile für Cover **und**
|
||||
Archivkarte: `cover.teaser` vor `interview.teaser` — eine Sonderausgabe ohne Interview hat sonst keine.
|
||||
**Das Cover einer Sonderausgabe zeigt das FEST, nicht die Begegnung** (Felix, 03.09.2026): Wappen
|
||||
und „Heimspiel gegen …" entfallen, an ihrer Stelle steht der Veranstaltungstitel in der
|
||||
Display-Schrift plus Zeitraum (Quelle: `veranstaltungen.json`, nie neu getippt). Grund: ein Fest
|
||||
bringt mehrere Heimspiele mit, eines davon aufs Cover zu heben wäre schlicht falsch. Ausgelöst wird
|
||||
das vom `veranstaltung`-Feld, nicht von einem eigenen Schalter.
|
||||
**Statt des Spielerfotos steht das HAUPTPLAKAT der Veranstaltung rechts** (erstes Plakat aus
|
||||
`veranstaltungen.json`, dieselbe Quelle wie die Detailseite, nie eine Kopie): leicht schräg gelegt
|
||||
(4 Grad) und an zwei gegenüberliegenden Ecken gerundet, links oben und rechts unten, mit Schatten
|
||||
statt Rahmen — wie ein aufs Cover gelegter Aushang (Felix, 03.09.2026). Es ist mit 66cqi deutlich
|
||||
kleiner als das Spielerfoto (98cqi): ein Freisteller ist schmal, ein Plakat im Hochformat läge bei
|
||||
derselben Höhe über der Typografie unten links. Die Textspalte wird dafür auf 48cqi verengt.
|
||||
Ohne Plakat greift das Spielerfoto, ohne beides trägt der Hintergrund die Seite. `box-shadow` ist
|
||||
hier unbedenklich; die Druck-Falle des Projekts betrifft `text-shadow` auf umbrechendem Text.
|
||||
|
||||
**Sonst bleibt das Cover unverändert, und das ist eine Entscheidung:** eine rote Kennung
|
||||
„Sportfest-Ausgabe" als Kasten im Kopf und ein kräftiger roter Doppelrahmen waren gebaut und wurden
|
||||
am 03.09.2026 wieder entfernt (Felix: „sieht schrecklich aus"). Der Rahmen lief auf z2 quer über das
|
||||
freigestellte Spielerfoto, und der Kasten stach aus dem ruhigen Kopf heraus. Die Ausgabe kennzeichnet
|
||||
sich durch den großen Fest-Titel unten links ausreichend selbst. **Kein zweites Rot in den Kopf, und
|
||||
keinen farbigen Rahmen über das Spielerfoto** — der Kicker bleibt „Saison · Datum" wie überall.
|
||||
- **Die Daten liegen in `veranstaltungen.json`, nicht in der Ausgabe** — eine Quelle für Website und
|
||||
Heft: dasselbe `programm[]` rendert `veranstaltung-programm.php` auf `/veranstaltungen/<slug>` und
|
||||
`stadionzeitung-programm.php` auf dem A5-Blatt (zwei Darstellungen, nie zwei Datenstände).
|
||||
`turniere[]` ist bisher **nur** Heft-Inhalt (Website-Darstellung offen), Feldform:
|
||||
`{key, title, date, start, spieldauer, felder?, modus?, teams[], spiele[]{nr, zeit, feld?, heim, gast}}`.
|
||||
- **`stadionzeitung_gegner_fixtures()`** (`helpers.php`) ist die EINZIGE Stelle, die entscheidet,
|
||||
welche Mannschaft eine Gegnerseite bekommt. `bin/stadionzeitung-add.php` plant die Seiten damit,
|
||||
`stadionzeitung_build_snapshot()` baut die Daten damit — sonst entstünde eine Seite ohne Daten oder
|
||||
umgekehrt. `gegner` ist deshalb eine **Map je Team-Key** (wie `tabellen`) statt eines einzelnen
|
||||
Objekts; Ausgaben ohne `team` am Seiteneintrag fallen auf die tragende Mannschaft zurück. Ein
|
||||
eingefrorener Snapshot mit der alten Ein-Objekt-Form existiert nicht (die einzige veröffentlichte
|
||||
Ausgabe ist älter als die Gegnerseite und trägt dort `null`), deshalb gibt es dafür bewusst
|
||||
**keinen** Kompatibilitäts-Zweig.
|
||||
- **Die Vorschau überspringt jedes Spiel mit eigener Gegnerseite**, nicht mehr nur das Heftspiel:
|
||||
Zweite und Frauen stünden sonst zweimal im Heft. Sie zeigt dann die Spiele nach dem Fest.
|
||||
- **Programm und Turnierpläne sind live, ohne Snapshot** (wie Kontakt/Aufruf): chat-gepflegte Daten,
|
||||
die kein Sync wegräumt. Eine spätere Korrektur am Programm ändert damit auch eine archivierte
|
||||
Ausgabe — bewusst in Kauf genommen, dieselbe Abwägung wie bei der Kontakt-Seite.
|
||||
- **Die Turnierseite ist zum Beschreiben:** Spielplan mit LEERER Ergebnisspalte (Schreiblinien) und
|
||||
leere Abschlusstabelle. Die Zeilenhöhe macht dort die **Schreiblinie**, nicht die Schriftgröße
|
||||
(gemessen: eine kleinere Schrift brachte genau einen Pixel) — wer die E-Jugend-Seite mit zehn
|
||||
Spielen aufs Blatt bringen muss, geht an `.stadionzeitung-turnier--dicht .__blank`. Die
|
||||
Programm-Seite läuft aus demselben Grund mit straffen Abständen: zwölf Punkte plus drei Tagestitel
|
||||
liefen mit den üblichen 3.3cqi 59px über die Blattkante. **Nach jeder Änderung Seitenhöhe messen.**
|
||||
- **Beim Messen die scrollbaren Seiten mitnehmen.** Vorwort, Termine und Sportheim tragen
|
||||
`overflow-y: auto`: dort wächst nicht die Seite, sondern das Kind wird scrollbar — eine Messung
|
||||
von `scrollHeight - clientHeight` auf `.stadionzeitung-page` meldet 0, und im Druck fehlt trotzdem
|
||||
das Ende des Textes. Genau so fiel ein sechsabsätziges Vorwort erst in der Druckvorschau auf
|
||||
(03.09.2026, seitdem vier Absätze). Verlässlich ist: Unterkante der Signatur gegen die Blattkante
|
||||
prüfen, im Viewer **und** in `?druck=1`.
|
||||
- **Seitenzahl aufs Vielfache von 4 bringen, ohne Inhalt zu erfinden:** eine zeitgebundene
|
||||
Eigenanzeige lässt sich per `bis` in `data/anzeigen.json` für diese Ausgabe abschalten (der Filter
|
||||
vergleicht mit dem Ausgabedatum). So kam die Sportfest-Ausgabe von 49 auf 48 Seiten: das
|
||||
Programmplakat als Anzeige wurde abgelöst, weil die gesetzte Programmseite es ersetzt.
|
||||
- **Ablauf ist zweistufig** (wie beim Vorwort): erst `bin/stadionzeitung-add.php` laufen lassen, dann
|
||||
`veranstaltung`/`cover` in die Ausgabe eintragen, dann **erneut** laufen lassen. Vorher existiert
|
||||
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.
|
||||
|
||||
**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
|
||||
|
||||
@@ -36,11 +36,12 @@ $issues = $issues ?? [];
|
||||
<time datetime="<?= e($issue['date']) ?>"><?= e(german_date($issue['date'])) ?></time>
|
||||
· <?= (int) $issue['pages'] ?> Seiten
|
||||
</p>
|
||||
<?php /* Coverzeile des Interviews als Teaser — dieselbe Quelle, die auch auf dem
|
||||
Cover unter „Im Heft" steht. Sie füllt die breite Zeile mit echtem Inhalt
|
||||
statt Leerfläche und sagt, warum man die Ausgabe öffnen sollte. Trägt eine
|
||||
Ausgabe kein Interview, entfällt die Zeile (wie bei .veranstaltung-card__text). */
|
||||
$kartenTeaser = trim((string) ($issue['interview']['teaser'] ?? '')); ?>
|
||||
<?php /* Coverzeile als Teaser — dieselbe Quelle (stadionzeitung_teaser()), die auch
|
||||
auf dem Cover unter „Im Heft" steht: cover.teaser einer Sonderausgabe, sonst
|
||||
die Coverzeile des Interviews. Sie füllt die breite Zeile mit echtem Inhalt
|
||||
statt Leerfläche und sagt, warum man die Ausgabe öffnen sollte. Gibt es
|
||||
beides nicht, entfällt die Zeile (wie bei .veranstaltung-card__text). */
|
||||
$kartenTeaser = stadionzeitung_teaser($issue); ?>
|
||||
<?php if ($kartenTeaser !== ''): ?>
|
||||
<p class="stadionzeitung-card__text"><?= e($kartenTeaser) ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -69,18 +69,64 @@ if ($match !== null) {
|
||||
|
||||
// Gedruckte Zeile neben dem QR-Code — immer die echte Domain (club_domain()).
|
||||
$linkText = club_domain();
|
||||
$kicker = trim($season . ($date !== '' ? ' · ' . german_date($date) : ''));
|
||||
// Aufmacher unten links: normal die Begegnung (Wappen + „Heimspiel gegen …").
|
||||
// Zeigt die Ausgabe auf eine Veranstaltung (Sportfest-Ausgabe), tritt an deren
|
||||
// Stelle das FEST als Titelgeschichte — denn ein Fest bringt mehrere Heimspiele
|
||||
// mit (Erste, Zweite, Frauen), und ein einzelnes davon aufs Cover zu heben wäre
|
||||
// falsch (Felix, 03.09.2026). Titel und Zeitraum kommen aus
|
||||
// data/veranstaltungen.json, nie neu getippt.
|
||||
$feature = null;
|
||||
$featureV = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
|
||||
if ($featureV !== null) {
|
||||
$feature = [
|
||||
'title' => trim((string) ($featureV['title'] ?? '')),
|
||||
'range' => german_date_range((string) ($featureV['date_start'] ?? ''), $featureV['date_end'] ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
// Bildmotiv rechts: normal das freigestellte Spielerfoto. Eine Sonderausgabe mit
|
||||
// Veranstaltung zeigt stattdessen deren HAUPTPLAKAT (erstes Plakat, dieselbe
|
||||
// Quelle wie /veranstaltungen/<slug>, nie eine Kopie) — leicht schräg gelegt mit
|
||||
// zwei abgerundeten Ecken, wie ein aufs Cover gelegter Aushang (Felix,
|
||||
// 03.09.2026). Ohne Plakat bleibt es beim Spielerfoto, ohne beides trägt der
|
||||
// Hintergrund die Seite allein.
|
||||
$poster = null;
|
||||
if ($featureV !== null) {
|
||||
$ersteseite = $featureV['plakate'][0] ?? null;
|
||||
if (is_array($ersteseite) && !empty($ersteseite['base'])) {
|
||||
$poster = $ersteseite;
|
||||
}
|
||||
}
|
||||
|
||||
// Kicker: Saison · Datum, auf jedem Cover gleich. Eine Sonderausgabe bekommt
|
||||
// bewusst KEINE zusätzliche Kennung im Kopf: ein roter Kasten „Sportfest-Ausgabe"
|
||||
// war gebaut und am 03.09.2026 wieder entfernt (Felix) — er stach aus dem
|
||||
// ruhigen Kopf heraus, und die Ausgabe kennzeichnet sich unten links ohnehin
|
||||
// selbst durch den großen Fest-Titel.
|
||||
$kicker = implode(' · ', array_filter([
|
||||
$season,
|
||||
$date !== '' ? german_date($date) : '',
|
||||
], static fn (string $s): bool => $s !== ''));
|
||||
|
||||
// Zweite Coverzeile (Magazin-Praxis: Teaser auf die Geschichte im Heft) —
|
||||
// erscheint automatisch, sobald die Ausgabe ein Interview trägt
|
||||
// (interview-Feld, chat-gepflegt wie das Vorwort).
|
||||
$teaser = trim((string) ($issue['interview']['teaser'] ?? ''));
|
||||
// aus stadionzeitung_teaser(): `cover.teaser` einer Sonderausgabe, sonst die
|
||||
// Coverzeile des Interviews. Dieselbe Funktion speist die Archivkarte.
|
||||
$teaser = stadionzeitung_teaser($issue);
|
||||
?>
|
||||
<div class="stadionzeitung-cover">
|
||||
<div class="stadionzeitung-cover<?= $poster !== null ? ' stadionzeitung-cover--poster' : '' ?>">
|
||||
<?php if ($heroImage): ?>
|
||||
<?php component('img', ['image' => $heroImage, 'sizes' => '480px', 'class' => 'stadionzeitung-cover__bg']); ?>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($playerImage)): ?>
|
||||
<?php if ($poster !== null): ?>
|
||||
<?php /* Alt-Text bewusst leer: der Plakat-Alt-Text aus veranstaltungen.json
|
||||
beschreibt die Termine, die drei Zeilen weiter unten als Text stehen —
|
||||
auf dem Cover wäre er eine Dopplung fürs Screenreader-Ohr. */ ?>
|
||||
<?php component('img', [
|
||||
'image' => ['base' => $poster['base'], 'widths' => $poster['widths'] ?? [], 'alt' => ''],
|
||||
'class' => 'stadionzeitung-cover__poster',
|
||||
'sizes' => '(max-width: 600px) 55vw, 260px',
|
||||
]); ?>
|
||||
<?php elseif (!empty($playerImage)): ?>
|
||||
<?php
|
||||
/* Varianten-Set (base/widths/ext=png) statt der Originaldatei. Das Original ist
|
||||
ein Freisteller mit 2584x5093 Pixeln und 12 MB: iOS Safari verwirft Bilder,
|
||||
@@ -112,6 +158,17 @@ $teaser = trim((string) ($issue['interview']['teaser'] ?? ''));
|
||||
<p class="stadionzeitung-cover__tagline">Rund ums Heimspiel im Katzbachtal</p>
|
||||
</header>
|
||||
<div class="stadionzeitung-cover__corner">
|
||||
<?php if ($feature !== null): ?>
|
||||
<?php /* Sonderausgabe: das Fest ist die Titelgeschichte, nicht eine der
|
||||
Begegnungen. Titel in der Display-Schrift (Versalien wie der Masthead),
|
||||
Zeitraum darunter als gesperrte Zeile. */ ?>
|
||||
<div class="stadionzeitung-cover__feature">
|
||||
<p class="stadionzeitung-cover__feature-title"><?= e($feature['title']) ?></p>
|
||||
<?php if ($feature['range'] !== ''): ?>
|
||||
<p class="stadionzeitung-cover__meta"><?= e($feature['range']) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="stadionzeitung-cover__headline">
|
||||
<?php /* Bewusst ohne Trennzeichen zwischen den Wappen (Felix, 29.07.2026:
|
||||
ruhiger) — die beiden Logos sprechen für sich. */ ?>
|
||||
@@ -124,6 +181,7 @@ $teaser = trim((string) ($issue['interview']['teaser'] ?? ''));
|
||||
<p class="stadionzeitung-cover__meta"><?= e(trim($teamName . ($league !== '' ? ' · ' . $league : ''))) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($teaser !== ''): ?>
|
||||
<p class="stadionzeitung-cover__teaser">
|
||||
<span class="stadionzeitung-cover__teaser-label">Im Heft</span>
|
||||
|
||||
65
app/components/stadionzeitung-danke.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Danke-Seite der Stadionzeitung (Live-Seite): der Dank an unsere Partner mit
|
||||
* der Logo-Wand aus data/partners.json (Single Source, dieselbe Quelle wie die
|
||||
* Startseite) und dem QR auf /partner-werden. Gegenstück zur Akquise-Seite
|
||||
* stadionzeitung-partner.php: hier stehen die, die schon dabei sind.
|
||||
*
|
||||
* Vorgeschichte: der Dank war Teil eines Canva-Bilds, das unten an die
|
||||
* Emons-Anzeige geklebt war — eine leere Fläche mit Wappen und zwei Zeilen
|
||||
* (Felix, 03.09.2026: „sieht schrecklich aus"). Seitdem eine echte Seite, die
|
||||
* die Partner auch zeigt statt nur zu erwähnen.
|
||||
*
|
||||
* Die Anzeige oben ist KEIN Bestandteil der Komponente, sondern kommt als Prop:
|
||||
* bin/stadionzeitung-add.php nimmt Anzeigen mit `platzierung: danke` aus dem
|
||||
* Streu-Pool und schreibt das Motiv in den Seiteneintrag. Dadurch ist es in
|
||||
* images[] eingefroren wie jede andere Anzeige — eine veröffentlichte Ausgabe
|
||||
* ändert sich nicht mehr, wenn der Sponsor später ein neues Motiv liefert.
|
||||
*
|
||||
* Props: $anzeige (Varianten-Set {base,widths,alt} oder null)
|
||||
*/
|
||||
$anzeige = (is_array($anzeige ?? null) && !empty($anzeige['base'])) ? $anzeige : null;
|
||||
$partners = json_load('partners')['partners'] ?? [];
|
||||
$qr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-partner-werden.png')
|
||||
? 'img/stadionzeitung/qr-partner-werden.png'
|
||||
: null;
|
||||
?>
|
||||
<div class="stadionzeitung-danke">
|
||||
<?php if ($anzeige !== null): ?>
|
||||
<?php component('img', ['image' => $anzeige, 'sizes' => '420px', 'class' => 'stadionzeitung-danke__anzeige']); ?>
|
||||
<?php endif; ?>
|
||||
<header class="stadionzeitung-page__head stadionzeitung-danke__head">
|
||||
<p class="stadionzeitung-page__kicker">Unsere Partner</p>
|
||||
<?php /* Überschrift bewusst OHNE Anrede: Partner sind Unternehmen, die wir mit
|
||||
„Sie" ansprechen (Sprachregel, CLAUDE.md), ein „danke, dass ihr dabei
|
||||
seid" würde sie duzen. „Danke für die Unterstützung" trägt den Dank
|
||||
ohne diesen Konflikt, der CTA darunter bleibt in der Sie-Form. */ ?>
|
||||
<h2 class="stadionzeitung-page__heading">Danke für die Unterstützung</h2>
|
||||
</header>
|
||||
<?php if ($partners !== []): ?>
|
||||
<ul class="stadionzeitung-danke__logos">
|
||||
<?php foreach ($partners as $partner): ?>
|
||||
<?php if (empty($partner['logo'])) { continue; } ?>
|
||||
<li class="stadionzeitung-danke__logo">
|
||||
<img src="<?= e(asset((string) $partner['logo'])) ?>" alt="<?= e((string) ($partner['name'] ?? '')) ?>"<?= img_intrinsic_attrs((string) $partner['logo']) ?> loading="lazy" decoding="async">
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<div class="stadionzeitung-page__cta">
|
||||
<div class="stadionzeitung-page__cta-text">
|
||||
<p class="stadionzeitung-page__cta-label">Mitmachen</p>
|
||||
<p class="stadionzeitung-page__cta-title">Werden Sie unser Partner</p>
|
||||
<p class="stadionzeitung-page__cta-link"><?= e(club_domain('partner-werden')) ?></p>
|
||||
</div>
|
||||
<?php if ($qr !== null): ?>
|
||||
<div class="stadionzeitung-page__cta-abriss">
|
||||
<img class="stadionzeitung-page__cta-qr" src="<?= e(asset($qr)) ?>" alt="" width="60" height="60" loading="lazy" decoding="async">
|
||||
<span class="stadionzeitung-page__cta-scan">Scan mich</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,9 +7,14 @@ declare(strict_types=1);
|
||||
* aufgebauten Stadionzeitungs-Snapshot; die Komponente rendert nur. Dadurch
|
||||
* sind Entwurf und veröffentlichte Druckausgabe konsistent und archivfest.
|
||||
*
|
||||
* Props: $info stadionzeitung_live_data($issue)['gegner']
|
||||
* Props: $info stadionzeitung_live_data($issue)['gegner'][team] — ein Eintrag der
|
||||
* Map je Team-Key (team_name/opponent/league/season/fixture/own/
|
||||
* opponent_stats). Normal nur die tragende Mannschaft; in der
|
||||
* Sportfest-Ausgabe eine Seite je Heimspiel am Fest, deshalb
|
||||
* nennt der Kicker die Mannschaft.
|
||||
*/
|
||||
$info = is_array($info ?? null) ? $info : [];
|
||||
$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;
|
||||
@@ -39,7 +44,7 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
|
||||
?>
|
||||
<div class="stadionzeitung-gegner">
|
||||
<header class="stadionzeitung-page__head">
|
||||
<p class="stadionzeitung-page__kicker">Unser Gegner</p>
|
||||
<p class="stadionzeitung-page__kicker"><?= e($teamName !== '' ? 'Unser Gegner · ' . $teamName : 'Unser Gegner') ?></p>
|
||||
<h2 class="stadionzeitung-page__heading"><?= e($opponentName) ?></h2>
|
||||
<?php if (!empty($info['league']) || !empty($info['season'])): ?>
|
||||
<p class="stadionzeitung-page__sub"><?= e(trim(implode(' · ', array_filter([(string) ($info['league'] ?? ''), (string) ($info['season'] ?? '')])))) ?></p>
|
||||
@@ -70,9 +75,12 @@ $format = static function (?array $row, string $key, bool $signed = false): stri
|
||||
<?php if ($own !== null && $opponent !== null): ?>
|
||||
<div class="stadionzeitung-gegner__vergleich" role="table" aria-label="Tabellenvergleich">
|
||||
<div class="stadionzeitung-gegner__vergleich-head" role="row">
|
||||
<span role="columnheader">TSV 08</span>
|
||||
<?php /* Spaltenköpfe generisch: vorher standen hier „TSV 08" und „ASV" fest im
|
||||
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">ASV</span>
|
||||
<span role="columnheader">Gegner</span>
|
||||
</div>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<div class="stadionzeitung-gegner__vergleich-row" role="row">
|
||||
|
||||
71
app/components/stadionzeitung-programm.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Programm-Seite der Stadionzeitung (Live-Seite, Sportfest-Ausgabe): das
|
||||
* Tagesprogramm einer Veranstaltung, je Tag eine Gruppe (Wochentag + Datum),
|
||||
* darunter Zeit | Titel (+ Zusatz). Kopf im Muster der Geschwister-Seiten.
|
||||
*
|
||||
* Quelle ist AUSSCHLIESSLICH data/veranstaltungen.json → programm[] der
|
||||
* Veranstaltung, auf die die Ausgabe zeigt (`veranstaltung`-Feld, Slug). Dieselbe
|
||||
* Liste rendert die Website-Detailseite über veranstaltung-programm.php — eine
|
||||
* Quelle für Heft und Website, hier nur eine zweite Darstellung fürs A5-Blatt
|
||||
* (cqi-Maße statt Website-Tokens). Bewusst live, kein Snapshot: chat-gepflegte
|
||||
* Daten wie bei kontakt/aufruf, kein Sync räumt hier etwas weg.
|
||||
*
|
||||
* Props: $issue (Eintrag aus data/stadionzeitung.json)
|
||||
*/
|
||||
$issue = $issue ?? [];
|
||||
$v = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
|
||||
$programm = is_array($v['programm'] ?? null) ? $v['programm'] : [];
|
||||
if ($v === null || $programm === []) {
|
||||
return;
|
||||
}
|
||||
$zeitraum = german_date_range((string) ($v['date_start'] ?? ''), $v['date_end'] ?? null);
|
||||
$sub = trim(implode(' · ', array_filter([$zeitraum, (string) ($v['location'] ?? '')])));
|
||||
?>
|
||||
<div class="stadionzeitung-programm">
|
||||
<header class="stadionzeitung-page__head">
|
||||
<p class="stadionzeitung-page__kicker"><?= e((string) ($v['title'] ?? 'Veranstaltung')) ?></p>
|
||||
<h2 class="stadionzeitung-page__heading">Programm</h2>
|
||||
<?php if ($sub !== ''): ?>
|
||||
<p class="stadionzeitung-page__sub"><?= e($sub) ?></p>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
|
||||
<?php foreach ($programm as $tag): ?>
|
||||
<?php
|
||||
$tagIso = (string) ($tag['tag'] ?? '');
|
||||
$weekday = $tagIso !== '' ? german_weekday($tagIso) : '';
|
||||
$tagText = trim(($weekday !== '' ? $weekday . ', ' : '') . ($tagIso !== '' ? german_date($tagIso) : ''));
|
||||
$punkte = is_array($tag['punkte'] ?? null) ? $tag['punkte'] : [];
|
||||
if ($punkte === []) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<section class="stadionzeitung-programm__tag">
|
||||
<?php if ($tagText !== ''): ?>
|
||||
<h3 class="stadionzeitung-programm__tag-titel"><time datetime="<?= e($tagIso) ?>"><?= e($tagText) ?></time></h3>
|
||||
<?php endif; ?>
|
||||
<ul class="stadionzeitung-programm__liste">
|
||||
<?php foreach ($punkte as $punkt): ?>
|
||||
<?php $zeit = trim((string) ($punkt['zeit'] ?? '')); ?>
|
||||
<li class="stadionzeitung-programm__punkt">
|
||||
<?php if ($zeit !== ''): ?>
|
||||
<span class="stadionzeitung-programm__zeit"><time datetime="<?= e($tagIso !== '' ? $tagIso . 'T' . $zeit : $zeit) ?>"><?= e($zeit) ?></time></span>
|
||||
<?php else: ?>
|
||||
<span aria-hidden="true"></span>
|
||||
<?php endif; ?>
|
||||
<span class="stadionzeitung-programm__body">
|
||||
<span class="stadionzeitung-programm__titel"><?= e((string) ($punkt['titel'] ?? '')) ?></span>
|
||||
<?php if (!empty($punkt['text'])): ?>
|
||||
<span class="stadionzeitung-programm__text"><?= e((string) $punkt['text']) ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@@ -9,9 +9,12 @@ declare(strict_types=1);
|
||||
* anderer Anordnung (Lesereihenfolge vs. Broschüren-Reihenfolge). Props:
|
||||
* $entry Eintrag aus issue.images[]:
|
||||
* type 'image' (base/widths/alt),
|
||||
* type 'live' (page: cover|vorwort|interview|aufruf|tabelle|termine|
|
||||
* news|ergebnisse|gegner|vorschau|kontakt; tabelle optional
|
||||
* mit team: Team-Key aus matchcenter.json),
|
||||
* type 'live' (page: cover|vorwort|interview|programm|aufruf|turnier|
|
||||
* tabelle|termine|news|ergebnisse|gegner|vorschau|kontakt|
|
||||
* mitglied|historie|sportheim|partner|danke|momente|impressum;
|
||||
* tabelle und gegner optional mit team: Team-Key aus
|
||||
* matchcenter.json; turnier mit turnier: Key aus
|
||||
* veranstaltungen.json → turniere[] der Ausgaben-Veranstaltung),
|
||||
* type 'leer' (weiße Füllseite, nur Druckfassung — hält die
|
||||
* Seitenzahl auf einem Vielfachen von 4)
|
||||
* $issue Eintrag aus data/stadionzeitung.json
|
||||
@@ -56,7 +59,19 @@ $type = $entry['type'] ?? 'image';
|
||||
<?php component('stadionzeitung-ergebnisse', ['groups' => $data['ergebnisse'] ?? []]); ?>
|
||||
<?php break; ?>
|
||||
<?php case 'gegner': ?>
|
||||
<?php component('stadionzeitung-gegner', ['info' => $data['gegner'] ?? null]); ?>
|
||||
<?php
|
||||
// `gegner` ist eine Map je Team-Key (wie `tabellen`), seit die Sportfest-
|
||||
// Ausgabe je Heimspiel eine Gegnerseite hat. Ohne team-Feld (Ausgaben vor
|
||||
// dieser Änderung) greift die tragende Mannschaft.
|
||||
$gegnerTeam = (string) ($entry['team'] ?? $issue['team_key'] ?? '');
|
||||
component('stadionzeitung-gegner', ['info' => $data['gegner'][$gegnerTeam] ?? null]);
|
||||
?>
|
||||
<?php break; ?>
|
||||
<?php case 'programm': ?>
|
||||
<?php component('stadionzeitung-programm', ['issue' => $issue]); ?>
|
||||
<?php break; ?>
|
||||
<?php case 'turnier': ?>
|
||||
<?php component('stadionzeitung-turnier', ['issue' => $issue, 'turnier' => (string) ($entry['turnier'] ?? '')]); ?>
|
||||
<?php break; ?>
|
||||
<?php case 'vorschau': ?>
|
||||
<?php component('stadionzeitung-vorschau', ['items' => $data['vorschau'] ?? []]); ?>
|
||||
@@ -76,6 +91,11 @@ $type = $entry['type'] ?? 'image';
|
||||
<?php case 'partner': ?>
|
||||
<?php component('stadionzeitung-partner', []); ?>
|
||||
<?php break; ?>
|
||||
<?php case 'danke': ?>
|
||||
<?php /* Das Anzeigenmotiv steht im Seiteneintrag (platzierung: danke), damit es
|
||||
wie jede andere Anzeige in images[] eingefroren ist. */ ?>
|
||||
<?php component('stadionzeitung-danke', ['anzeige' => $entry['anzeige'] ?? null]); ?>
|
||||
<?php break; ?>
|
||||
<?php case 'momente': ?>
|
||||
<?php component('stadionzeitung-momente', ['items' => $data['momente'] ?? []]); ?>
|
||||
<?php break; ?>
|
||||
|
||||
158
app/components/stadionzeitung-turnier.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Turnierplan-Seite der Stadionzeitung (Live-Seite, Sportfest-Ausgabe): EIN
|
||||
* Jugendturnier pro Seite — Kopf (Veranstaltung · Tag / „Turnier F1-Jugend" /
|
||||
* Beginn · Spieldauer · Felder · Modus), Teilnehmer, Spielplan mit LEERER
|
||||
* Ergebnisspalte und eine leere Abschlusstabelle. Das Blatt ist zum
|
||||
* Beschreiben gedacht: am Platz trägt man Ergebnisse und Plätze von Hand ein.
|
||||
*
|
||||
* Quelle ist AUSSCHLIESSLICH data/veranstaltungen.json → turniere[] der
|
||||
* Veranstaltung, auf die die Ausgabe zeigt (`veranstaltung`-Feld); welches
|
||||
* Turnier, sagt der `turnier`-Key des Seiteneintrags. Bewusst live, kein
|
||||
* Snapshot (chat-gepflegte Daten, wie kontakt/aufruf/programm).
|
||||
*
|
||||
* Eintrag: key, title, date, start, spieldauer (Minuten), felder? (Zahl),
|
||||
* modus? (Freitext), teams[], spiele[]{nr, zeit, feld?, heim, gast}
|
||||
*
|
||||
* Eigenes Team wie league-table tr.is-own hervorgehoben (Tönung + Abel-Fett);
|
||||
* erkannt über den Vereinsnamen aus data/club.json als Präfix, damit auch
|
||||
* „TSV 08 Kulmbach 2" getroffen wird.
|
||||
*
|
||||
* Props: $issue (Eintrag aus data/stadionzeitung.json), $turnier (Key)
|
||||
*/
|
||||
$issue = $issue ?? [];
|
||||
$turnierKey = trim((string) ($turnier ?? ''));
|
||||
$v = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
|
||||
$t = null;
|
||||
foreach ($v['turniere'] ?? [] as $eintrag) {
|
||||
if (is_array($eintrag) && (string) ($eintrag['key'] ?? '') === $turnierKey) {
|
||||
$t = $eintrag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($v === null || $t === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$clubName = trim((string) (json_load('club')['name'] ?? 'TSV 08 Kulmbach'));
|
||||
$isOwn = static fn (string $team): bool => $clubName !== ''
|
||||
&& str_starts_with(mb_strtolower(trim($team)), mb_strtolower($clubName));
|
||||
|
||||
$date = (string) ($t['date'] ?? '');
|
||||
$weekday = $date !== '' ? german_weekday($date) : '';
|
||||
$kicker = trim(implode(' · ', array_filter([
|
||||
(string) ($v['title'] ?? ''),
|
||||
trim(($weekday !== '' ? $weekday . ', ' : '') . ($date !== '' ? german_date($date) : '')),
|
||||
])));
|
||||
|
||||
$teams = array_values(array_filter(
|
||||
array_map(static fn ($x): string => trim((string) $x), $t['teams'] ?? []),
|
||||
static fn (string $x): bool => $x !== ''
|
||||
));
|
||||
$spiele = array_values(array_filter($t['spiele'] ?? [], 'is_array'));
|
||||
$felder = (int) ($t['felder'] ?? 0);
|
||||
$hatFeld = $felder > 1;
|
||||
|
||||
$meta = [];
|
||||
if (!empty($t['start'])) {
|
||||
$meta[] = 'Beginn ' . (string) $t['start'] . ' Uhr';
|
||||
}
|
||||
if (!empty($t['spieldauer'])) {
|
||||
$meta[] = 'Spieldauer ' . (int) $t['spieldauer'] . ' Min.';
|
||||
}
|
||||
if ($hatFeld) {
|
||||
$meta[] = $felder . ' Felder';
|
||||
}
|
||||
if (!empty($t['modus'])) {
|
||||
$meta[] = (string) $t['modus'];
|
||||
}
|
||||
$sub = trim(implode(' · ', $meta));
|
||||
$title = trim((string) ($t['title'] ?? $turnierKey));
|
||||
// Ab neun Spielen (E-Jugend: zehn) rücken die Zeilen enger, sonst läuft der
|
||||
// Spielplan mit der Abschlusstabelle über die Blattkante.
|
||||
$dicht = count($spiele) > 8;
|
||||
?>
|
||||
<div class="stadionzeitung-turnier<?= $dicht ? ' stadionzeitung-turnier--dicht' : '' ?>">
|
||||
<header class="stadionzeitung-page__head">
|
||||
<?php if ($kicker !== ''): ?>
|
||||
<p class="stadionzeitung-page__kicker"><?= e($kicker) ?></p>
|
||||
<?php endif; ?>
|
||||
<h2 class="stadionzeitung-page__heading"><?= e('Turnier ' . $title) ?></h2>
|
||||
<?php if ($sub !== ''): ?>
|
||||
<p class="stadionzeitung-page__sub"><?= e($sub) ?></p>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
<span class="stadionzeitung-page__rule" aria-hidden="true"></span>
|
||||
<?php if ($teams !== []): ?>
|
||||
<p class="stadionzeitung-turnier__label">Teilnehmer</p>
|
||||
<p class="stadionzeitung-turnier__teams">
|
||||
<?php foreach ($teams as $i => $team): ?>
|
||||
<?= $i > 0 ? ' · ' : '' ?><span class="stadionzeitung-turnier__team<?= $isOwn($team) ? ' is-own' : '' ?>"><?= e($team) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if ($spiele !== []): ?>
|
||||
<table class="stadionzeitung-turnier__spiele">
|
||||
<?php /* Sichtbares Label IST die Tabellenbeschriftung (caption), nicht zusätzlich
|
||||
eine versteckte: sonst liest ein Screenreader beides und damit doppelt. */ ?>
|
||||
<caption class="stadionzeitung-turnier__label">Spielplan</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Nr.</th>
|
||||
<th scope="col">Zeit</th>
|
||||
<?php if ($hatFeld): ?>
|
||||
<th scope="col">Feld</th>
|
||||
<?php endif; ?>
|
||||
<th scope="col">Begegnung</th>
|
||||
<th scope="col" class="stadionzeitung-turnier__ergebnis">Ergebnis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($spiele as $i => $spiel): ?>
|
||||
<?php
|
||||
$heim = trim((string) ($spiel['heim'] ?? ''));
|
||||
$gast = trim((string) ($spiel['gast'] ?? ''));
|
||||
$zeit = trim((string) ($spiel['zeit'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td class="stadionzeitung-turnier__nr"><?= e((string) ($spiel['nr'] ?? ($i + 1))) ?></td>
|
||||
<td class="stadionzeitung-turnier__zeit"><?php if ($zeit !== ''): ?><time datetime="<?= e($date !== '' ? $date . 'T' . $zeit : $zeit) ?>"><?= e($zeit) ?></time><?php endif; ?></td>
|
||||
<?php if ($hatFeld): ?>
|
||||
<td class="stadionzeitung-turnier__nr"><?= e((string) ($spiel['feld'] ?? '')) ?></td>
|
||||
<?php endif; ?>
|
||||
<td class="stadionzeitung-turnier__paarung"><span class="stadionzeitung-turnier__team<?= $isOwn($heim) ? ' is-own' : '' ?>"><?= e($heim) ?></span> <span class="stadionzeitung-turnier__vs">gegen</span> <span class="stadionzeitung-turnier__team<?= $isOwn($gast) ? ' is-own' : '' ?>"><?= e($gast) ?></span></td>
|
||||
<td class="stadionzeitung-turnier__ergebnis"><span class="stadionzeitung-turnier__blank" aria-hidden="true"></span><span class="stadionzeitung-turnier__colon">:</span><span class="stadionzeitung-turnier__blank" aria-hidden="true"></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
<?php if ($teams !== []): ?>
|
||||
<table class="stadionzeitung-turnier__tabelle">
|
||||
<caption class="stadionzeitung-turnier__label">Platzierung zum Eintragen</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Pl.</th>
|
||||
<th scope="col">Teilnehmer</th>
|
||||
<th scope="col"><abbr title="Tore">T</abbr></th>
|
||||
<th scope="col"><abbr title="Tordifferenz">TD</abbr></th>
|
||||
<th scope="col"><abbr title="Punkte">Pkt</abbr></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php for ($i = 1; $i <= count($teams); $i++): ?>
|
||||
<tr>
|
||||
<td class="stadionzeitung-turnier__nr"><?= $i ?>.</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* Swipe-Viewer für eine Stadionzeitung-Ausgabe. Props:
|
||||
* $issue Eintrag aus data/stadionzeitung.json (opponent, team_key, status,
|
||||
* images[]{type: 'image'{base,widths,alt} | 'live'{page[,team]}})
|
||||
* images[]{type: 'image'{base,widths,alt} | 'live'{page[,team][,turnier]}})
|
||||
*
|
||||
* Ohne JS: alle Seiten normal gestapelt (stadionzeitung-seite-Komponente) —
|
||||
* voll lesbar, sequentiell auch per Screenreader. Mit JS
|
||||
|
||||
138
app/helpers.php
@@ -568,6 +568,27 @@ function veranstaltungen_load(): array
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Veranstaltung per Slug, UNGEFILTERT (auch Entwürfe) — für die
|
||||
* Detailseite (app/pages/veranstaltung.php) und die Stadionzeitungs-Live-Seiten
|
||||
* der Sportfest-Ausgabe (Programm, Turnierpläne, Gegnerseiten je Heimspiel).
|
||||
* Ungefiltert aus demselben Grund wie die Detailseite: ein Entwurf muss sich
|
||||
* gegenlesen lassen, und eine Entwurfs-Ausgabe der Stadionzeitung darf auf
|
||||
* einen Entwurfs-Termin zeigen. null, wenn es den Slug nicht gibt.
|
||||
*/
|
||||
function veranstaltung_find(string $slug): ?array
|
||||
{
|
||||
if ($slug === '') {
|
||||
return null;
|
||||
}
|
||||
foreach (json_load('veranstaltungen')['veranstaltungen'] ?? [] as $entry) {
|
||||
if (($entry['slug'] ?? '') === $slug) {
|
||||
return $entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Steht ein Termin zum Bezugsdatum noch an? Maßgeblich ist das ENDE, nicht der
|
||||
* Beginn: ein mehrtägiges Fest (Sportfest, Freitag bis Sonntag) gehört am
|
||||
@@ -955,6 +976,86 @@ function stadionzeitung_is_published(array $issue): bool
|
||||
return ($issue['status'] ?? 'veroeffentlicht') !== 'entwurf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Coverzeile „Im Heft" einer Ausgabe — die EINE Quelle für das Cover
|
||||
* (stadionzeitung-cover.php) und die Archivkarte (stadionzeitung-cards.php),
|
||||
* damit beide nie Verschiedenes sagen. Eine Sonderausgabe (Sportfest) setzt
|
||||
* `cover.teaser` chat-gepflegt; sonst trägt die Coverzeile des Interviews
|
||||
* (`interview.teaser`). Leer, wenn es weder das eine noch das andere gibt.
|
||||
*/
|
||||
function stadionzeitung_teaser(array $issue): string
|
||||
{
|
||||
$teaser = trim((string) ($issue['cover']['teaser'] ?? ''));
|
||||
if ($teaser !== '') {
|
||||
return $teaser;
|
||||
}
|
||||
return trim((string) ($issue['interview']['teaser'] ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Welche Mannschaften bekommen in einer Ausgabe eine Gegnerseite — die EINE
|
||||
* Logik dafür, benutzt von bin/stadionzeitung-add.php (Seitenplanung) UND
|
||||
* stadionzeitung_build_snapshot() (Daten), damit nie eine Seite ohne Daten
|
||||
* oder Daten ohne Seite entstehen.
|
||||
*
|
||||
* Immer: die tragende Mannschaft (Gegner = `opponent`, Tag = `date`).
|
||||
* Zusätzlich, wenn die Ausgabe eine `veranstaltung` (Slug) mit Zeitraum trägt
|
||||
* (Sportfest-Ausgabe): jede weitere Mannschaft aus matchcenter.json mit einem
|
||||
* HEIMspiel in diesem Zeitraum — drei Heimspiele am Fest, drei Gegnerseiten.
|
||||
* Der Gegner ist dann der andere Verein der Begegnung. Reihenfolge wie in
|
||||
* matchcenter.json (= config 'matchcenter.teams').
|
||||
*
|
||||
* Rückgabe: [team_key => ['opponent' => string, 'date' => 'YYYY-MM-DD']]
|
||||
*/
|
||||
function stadionzeitung_gegner_fixtures(array $issue): array
|
||||
{
|
||||
$date = (string) ($issue['date'] ?? '');
|
||||
$issueTeam = (string) ($issue['team_key'] ?? '');
|
||||
$issueOpponent = trim((string) ($issue['opponent'] ?? ''));
|
||||
|
||||
$von = '';
|
||||
$bis = '';
|
||||
$veranstaltung = veranstaltung_find((string) ($issue['veranstaltung'] ?? ''));
|
||||
if ($veranstaltung !== null) {
|
||||
$von = (string) ($veranstaltung['date_start'] ?? '');
|
||||
$bis = (string) ($veranstaltung['date_end'] ?? $von);
|
||||
}
|
||||
|
||||
$fixtures = [];
|
||||
foreach (json_load('matchcenter')['teams'] ?? [] as $team) {
|
||||
$key = (string) ($team['key'] ?? '');
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
if ($key === $issueTeam) {
|
||||
if ($issueOpponent !== '') {
|
||||
$fixtures[$key] = ['opponent' => $issueOpponent, 'date' => $date];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($von === '') {
|
||||
continue;
|
||||
}
|
||||
foreach ($team['next_matches'] ?? [] as $match) {
|
||||
if (empty($match['is_home'])) {
|
||||
continue;
|
||||
}
|
||||
$matchDate = substr((string) ($match['kickoff'] ?? ''), 0, 10);
|
||||
if ($matchDate < $von || $matchDate > $bis) {
|
||||
continue;
|
||||
}
|
||||
$opponent = trim((string) ($match['away'] ?? ''));
|
||||
if ($opponent === '' || $opponent === 'SPIELFREI') {
|
||||
continue;
|
||||
}
|
||||
$fixtures[$key] = ['opponent' => $opponent, 'date' => $matchDate];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $fixtures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die neueste VERÖFFENTLICHTE Ausgabe (oder null, wenn es keine gibt) — für die
|
||||
* Kurz-URL /stadionzeitung/aktuell, auf die gedruckte QR-Codes zeigen. Sortiert
|
||||
@@ -988,12 +1089,15 @@ function stadionzeitung_build_snapshot(array $issue): array
|
||||
{
|
||||
$date = (string) ($issue['date'] ?? '');
|
||||
$issueTeam = (string) ($issue['team_key'] ?? '');
|
||||
$issueOpponent = trim((string) ($issue['opponent'] ?? ''));
|
||||
|
||||
$tabellen = [];
|
||||
$ergebnisse = [];
|
||||
$vorschau = [];
|
||||
$gegner = null;
|
||||
// Gegnerseiten als Map je Team-Key (wie `tabellen`): welche Mannschaften
|
||||
// eine bekommen, entscheidet stadionzeitung_gegner_fixtures() — dieselbe
|
||||
// Funktion, mit der bin/stadionzeitung-add.php die Seiten einplant.
|
||||
$gegnerFixtures = stadionzeitung_gegner_fixtures($issue);
|
||||
$gegner = [];
|
||||
foreach (json_load('matchcenter')['teams'] ?? [] as $team) {
|
||||
$key = (string) ($team['key'] ?? '');
|
||||
if ($key === '') {
|
||||
@@ -1006,11 +1110,14 @@ function stadionzeitung_build_snapshot(array $issue): array
|
||||
'table' => $team['table'] ?? [],
|
||||
];
|
||||
|
||||
// Gegnerseite der tragenden Mannschaft: ausschließlich aus demselben
|
||||
// Matchcenter-Datensatz wie die Tabelle ableiten. So gibt es keinen
|
||||
// zweiten BFV-Abruf und der Stand wird beim Veröffentlichen zusammen
|
||||
// mit den übrigen Live-Daten eingefroren.
|
||||
if ($key === $issueTeam && $issueOpponent !== '') {
|
||||
// Gegnerseite(n): ausschließlich aus demselben Matchcenter-Datensatz
|
||||
// wie die Tabelle ableiten. So gibt es keinen zweiten BFV-Abruf und der
|
||||
// Stand wird beim Veröffentlichen zusammen mit den übrigen Live-Daten
|
||||
// eingefroren. Normal nur die tragende Mannschaft; in der Sportfest-
|
||||
// Ausgabe jede Mannschaft mit Heimspiel am Fest (s. gegner_fixtures).
|
||||
if (isset($gegnerFixtures[$key])) {
|
||||
$gegnerName = $gegnerFixtures[$key]['opponent'];
|
||||
$gegnerDate = $gegnerFixtures[$key]['date'];
|
||||
$table = is_array($team['table'] ?? null) ? $team['table'] : [];
|
||||
$ownRow = null;
|
||||
$opponentRow = null;
|
||||
@@ -1018,7 +1125,7 @@ function stadionzeitung_build_snapshot(array $issue): array
|
||||
if (!empty($row['is_own_club'])) {
|
||||
$ownRow = $row;
|
||||
}
|
||||
if (strcasecmp(trim((string) ($row['club'] ?? '')), $issueOpponent) === 0) {
|
||||
if (strcasecmp(trim((string) ($row['club'] ?? '')), $gegnerName) === 0) {
|
||||
$opponentRow = $row;
|
||||
}
|
||||
}
|
||||
@@ -1027,15 +1134,15 @@ function stadionzeitung_build_snapshot(array $issue): array
|
||||
foreach ($team['next_matches'] ?? [] as $match) {
|
||||
$matchDate = substr((string) ($match['kickoff'] ?? ''), 0, 10);
|
||||
$clubs = [(string) ($match['home'] ?? ''), (string) ($match['away'] ?? '')];
|
||||
if ($matchDate === $date && in_array($issueOpponent, $clubs, true)) {
|
||||
if ($matchDate === $gegnerDate && in_array($gegnerName, $clubs, true)) {
|
||||
$fixture = $match;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$gegner = [
|
||||
$gegner[$key] = [
|
||||
'team_name' => (string) ($team['name'] ?? ''),
|
||||
'opponent' => $issueOpponent,
|
||||
'opponent' => $gegnerName,
|
||||
'league' => (string) ($team['league'] ?? ''),
|
||||
'season' => (string) ($team['season'] ?? ''),
|
||||
'fixture' => $fixture,
|
||||
@@ -1064,7 +1171,11 @@ function stadionzeitung_build_snapshot(array $issue): array
|
||||
|
||||
// Vorschau: das nächste Spiel dieser Mannschaft NACH dem Spieltag der
|
||||
// Ausgabe. Das Heft-Spiel selbst fliegt raus (das Cover zeigt es
|
||||
// schon); ein Spiel einer ANDEREN Mannschaft am selben Tag bleibt drin.
|
||||
// schon), und ebenso jedes Spiel, das eine eigene Gegnerseite hat
|
||||
// (Sportfest-Ausgabe: Zweite und Frauen stünden sonst doppelt im Heft).
|
||||
// Ein Spiel einer ANDEREN Mannschaft am selben Tag ohne Gegnerseite
|
||||
// bleibt drin.
|
||||
$gegnerDate = $gegnerFixtures[$key]['date'] ?? null;
|
||||
foreach ($team['next_matches'] ?? [] as $match) {
|
||||
// Spielfreie Runden führt der BFV als Pseudo-Begegnung gegen
|
||||
// "SPIELFREI" — kein echtes Spiel, im Heft überspringen.
|
||||
@@ -1078,6 +1189,9 @@ function stadionzeitung_build_snapshot(array $issue): array
|
||||
if ($matchDate === $date && $key === $issueTeam) {
|
||||
continue;
|
||||
}
|
||||
if ($gegnerDate !== null && $matchDate === $gegnerDate) {
|
||||
continue;
|
||||
}
|
||||
$vorschau[] = [
|
||||
'team_key' => $key,
|
||||
'team_name' => (string) ($team['name'] ?? ''),
|
||||
|
||||
@@ -9,9 +9,9 @@ declare(strict_types=1);
|
||||
* data/veranstaltungen.json — Texte chat-gepflegt, `plakat` und `galerie`
|
||||
* schreibt bin/veranstaltung-bilder.php.
|
||||
*
|
||||
* Geladen wird hier bewusst UNGEFILTERT (nicht über veranstaltungen_load()):
|
||||
* ein Entwurf muss unter seiner direkten URL lesbar bleiben, damit er
|
||||
* gegengelesen werden kann. Sichtbar wird er dadurch nirgends sonst, und er
|
||||
* Geladen wird hier bewusst UNGEFILTERT (veranstaltung_find(), nicht
|
||||
* veranstaltungen_load()): ein Entwurf muss unter seiner direkten URL lesbar
|
||||
* bleiben, damit er gegengelesen werden kann. Sichtbar wird er dadurch nirgends sonst, und er
|
||||
* trägt noindex.
|
||||
*
|
||||
* Bewusst OHNE das übliche hero-Component (wie die Stadionzeitungs-Ausgabe):
|
||||
@@ -23,13 +23,7 @@ declare(strict_types=1);
|
||||
$urlSlug = trim((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
|
||||
$veranstaltungSlug = substr($urlSlug, strlen('veranstaltungen/'));
|
||||
|
||||
$v = null;
|
||||
foreach (json_load('veranstaltungen')['veranstaltungen'] ?? [] as $entry) {
|
||||
if (($entry['slug'] ?? '') === $veranstaltungSlug) {
|
||||
$v = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$v = veranstaltung_find($veranstaltungSlug);
|
||||
|
||||
if ($v === null) {
|
||||
// http_response_code(404) ist globaler Engine-State (kein Scope-Problem wie
|
||||
|
||||
@@ -123,7 +123,11 @@ foreach ($sources as $src) {
|
||||
// Alte Varianten dieses Slugs weg, sonst bleiben bei einem kleineren Motiv
|
||||
// Breiten von vorher liegen und die JSON verspricht Dateien, die zum Bild
|
||||
// nicht mehr passen.
|
||||
foreach (glob("{$destDir}/{$slug}-*.jpg") ?: [] as $alt) {
|
||||
// Muster bewusst auf die BREITEN-Endung eingeschränkt (`-<zahl>.jpg`): mit
|
||||
// `{$slug}-*.jpg` löscht ein Slug die Motive jedes Slugs, der ihn als Präfix
|
||||
// hat — `emons` räumte so die Dateien von `emons-partnerdank` mit weg
|
||||
// (03.09.2026 beobachtet, dort zufällig gewollt, generell Datenverlust).
|
||||
foreach (glob("{$destDir}/{$slug}-[0-9]*.jpg") ?: [] as $alt) {
|
||||
unlink($alt);
|
||||
}
|
||||
|
||||
|
||||
@@ -326,6 +326,41 @@ if (is_file($szFile)) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Stadionzeitung ↔ Veranstaltung (Sportfest-Ausgabe) ---
|
||||
// Eine Ausgabe mit `veranstaltung`-Feld zieht Programm, Turnierpläne und die
|
||||
// Gegnerseiten je Heimspiel aus data/veranstaltungen.json. Ein Tippfehler im Slug
|
||||
// oder im Turnier-Key rendert dort eine LEERE A5-Seite — ohne Fehlermeldung, weil
|
||||
// die Komponenten bewusst still zurückkehren. Deshalb hier laut.
|
||||
if (is_file($szFile)) {
|
||||
$szData = $szData ?? (json_decode((string) file_get_contents($szFile), true) ?: []);
|
||||
$vBySlug = array_column($veranstaltungRows, null, 'slug');
|
||||
foreach ($szData['issues'] ?? [] as $ausgabe) {
|
||||
$vSlug = (string) ($ausgabe['veranstaltung'] ?? '');
|
||||
if ($vSlug === '') {
|
||||
continue;
|
||||
}
|
||||
if (!isset($vBySlug[$vSlug])) {
|
||||
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': veranstaltung '{$vSlug}' fehlt in data/veranstaltungen.json.";
|
||||
continue;
|
||||
}
|
||||
$turniere = $vBySlug[$vSlug]['turniere'] ?? [];
|
||||
foreach ($turniere as $i => $turnier) {
|
||||
foreach (['key', 'title', 'date', 'teams', 'spiele'] as $feld) {
|
||||
if (empty($turnier[$feld])) {
|
||||
$warnings[] = "data/veranstaltungen.json, '{$vSlug}' → turniere[{$i}]: Feld '{$feld}' fehlt oder ist leer.";
|
||||
}
|
||||
}
|
||||
}
|
||||
$keys = array_column($turniere, 'key');
|
||||
foreach ($ausgabe['images'] ?? [] as $seite) {
|
||||
if (($seite['type'] ?? '') === 'live' && ($seite['page'] ?? '') === 'turnier'
|
||||
&& !in_array((string) ($seite['turnier'] ?? ''), $keys, true)) {
|
||||
$errors[] = "Stadionzeitung '{$ausgabe['slug']}': Turnierseite '" . ($seite['turnier'] ?? '') . "' hat keinen Eintrag in turniere[] von '{$vSlug}'.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Inline-Styles (die CSP verwirft sie stillschweigend) ---
|
||||
// Diese Prüfung existiert wegen eines echten Ausfalls am 31.07.2026: der
|
||||
// Banner-Slider setzte sein Hintergrundbild per style="--banner-bg: url(…)". Auf
|
||||
|
||||
@@ -58,6 +58,16 @@ declare(strict_types=1);
|
||||
* Interview → Aufruf. Für die Aufruf-Seite erzeugt das Skript zusätzlich den
|
||||
* QR (qr-aufruf.png im Ausgaben-Ordner) aus `aufruf.link`.
|
||||
*
|
||||
* Sportfest-Ausgabe (03.09.2026): trägt die Ausgabe ein `veranstaltung`-Feld
|
||||
* (Slug aus data/veranstaltungen.json, chat-gepflegt), kommen dazu: die
|
||||
* Programm-Seite im Aufschlag (aus programm[] der Veranstaltung, dieselbe Liste
|
||||
* wie die Website), ein Block Turnierplan-Seiten (je Eintrag in turniere[] eine
|
||||
* Seite) direkt danach, und eine Gegnerseite je Mannschaft mit HEIMspiel im
|
||||
* Zeitraum der Veranstaltung statt nur für die tragende Mannschaft — welche das
|
||||
* sind, sagt stadionzeitung_gegner_fixtures() (helpers.php), dieselbe Funktion,
|
||||
* aus der der Snapshot die Daten baut. `cover` ({edition, teaser}) kennzeichnet
|
||||
* das Cover als Sonderausgabe. Beide Felder überleben erneute Läufe wie `vorwort`.
|
||||
*
|
||||
* Erneuter Aufruf mit demselben Slug ersetzt die Ausgabe vollständig (alte
|
||||
* Seiten-Bilder werden vorher gelöscht) — idempotent für Korrekturen.
|
||||
*/
|
||||
@@ -188,6 +198,19 @@ $oldVorwort = $oldIssue['vorwort'] ?? null;
|
||||
$oldInterview = $oldIssue['interview'] ?? null;
|
||||
// Aufruf-Seite (z. B. Sportfest-Helferaufruf) ebenso chat-gepflegt.
|
||||
$oldAufruf = $oldIssue['aufruf'] ?? null;
|
||||
// Sportfest-Ausgabe: Slug-Verweis auf data/veranstaltungen.json (Programm,
|
||||
// Turnierpläne, Gegnerseiten je Heimspiel) und Cover-Kennung — chat-gepflegt.
|
||||
$oldVeranstaltung = $oldIssue['veranstaltung'] ?? null;
|
||||
$oldCover = $oldIssue['cover'] ?? null;
|
||||
$veranstaltung = null;
|
||||
if (is_string($oldVeranstaltung) && $oldVeranstaltung !== '') {
|
||||
$veranstaltung = veranstaltung_find($oldVeranstaltung);
|
||||
if ($veranstaltung === null) {
|
||||
$log("veranstaltung '{$oldVeranstaltung}' steht nicht in data/veranstaltungen.json — Programm-, Turnier- und Zusatz-Gegnerseiten entfallen.", 'WARN');
|
||||
} else {
|
||||
$log("Sportfest-Ausgabe: Veranstaltung '{$oldVeranstaltung}' (" . count($veranstaltung['programm'] ?? []) . ' Programmtage, ' . count($veranstaltung['turniere'] ?? []) . ' Turniere).');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Ziel-Ordner vorbereiten: bei erneutem Lauf alte Seiten dieser Ausgabe wegräumen ---
|
||||
$destDir = PUBLIC_PATH . "/assets/img/stadionzeitung/{$slug}";
|
||||
@@ -215,6 +238,28 @@ $widths = [480, 960, 1440];
|
||||
// erscheint oft. Gepflegt wird der Bestand über bin/anzeige-add.php (Bilder) und
|
||||
// im Chat (Sponsorenname, Reihenfolge, aktiv). Hier wird nichts gerendert, die
|
||||
// Bilder liegen schon. ---
|
||||
// Anzeigen mit `platzierung` haben einen FESTEN Platz auf einer Live-Seite
|
||||
// (derzeit `danke`: die Emons-Anzeige über der Partner-Logo-Wand) und laufen
|
||||
// deshalb nicht im Streu-Pool mit — sonst stünden sie zweimal im Heft.
|
||||
$platzierteAnzeigen = [];
|
||||
foreach (json_load('anzeigen')['anzeigen'] ?? [] as $a) {
|
||||
$platz = trim((string) ($a['platzierung'] ?? ''));
|
||||
if ($platz === '' || ($a['aktiv'] ?? true) === false || empty($a['base'])) {
|
||||
continue;
|
||||
}
|
||||
if (!empty($a['bis']) && (string) $a['bis'] < $date) {
|
||||
continue;
|
||||
}
|
||||
if (isset($platzierteAnzeigen[$platz])) {
|
||||
continue;
|
||||
}
|
||||
$platzierteAnzeigen[$platz] = [
|
||||
'base' => (string) $a['base'],
|
||||
'widths' => $a['widths'] ?? [],
|
||||
'alt' => (string) ($a['alt'] ?? ('Anzeige: ' . trim((string) ($a['sponsor'] ?? '')))),
|
||||
];
|
||||
}
|
||||
|
||||
$anzeigenSeiten = array_values(array_map(
|
||||
static function (array $a): array {
|
||||
$sponsor = trim((string) ($a['sponsor'] ?? ''));
|
||||
@@ -237,6 +282,7 @@ $anzeigenSeiten = array_values(array_map(
|
||||
// nächsten Handgriff im Heft zu bleiben.
|
||||
static fn (array $a): bool => ($a['aktiv'] ?? true)
|
||||
&& !empty($a['base'])
|
||||
&& empty($a['platzierung'])
|
||||
&& (empty($a['bis']) || (string) $a['bis'] >= $date)
|
||||
)
|
||||
));
|
||||
@@ -245,12 +291,8 @@ $anzeigenSeiten = array_values(array_map(
|
||||
// Ausgaben-Bilder (Vorwort-Fallback, Rückseite) schon. Beide bekommen SPRECHENDE
|
||||
// Dateinamen statt der früheren Positionsnummern (page-07) — die verschoben sich
|
||||
// bei jeder Umverteilung. ---
|
||||
$live = static function (string $page, ?string $team = null): array {
|
||||
$entry = ['type' => 'live', 'page' => $page];
|
||||
if ($team !== null) {
|
||||
$entry['team'] = $team;
|
||||
}
|
||||
return $entry;
|
||||
$live = static function (string $page, array $extra = []): array {
|
||||
return ['type' => 'live', 'page' => $page] + $extra;
|
||||
};
|
||||
|
||||
$bild = static function (string $src, string $name, string $alt) use ($destDir, $widths, $slug): array {
|
||||
@@ -293,6 +335,11 @@ try {
|
||||
} elseif ($vorwort !== null) {
|
||||
$aufschlag[] = $bild($vorwort, 'vorwort', 'Vorwort der Stadionzeitung');
|
||||
}
|
||||
// Programm-Seite der Sportfest-Ausgabe direkt hinter dem Vorwort: aufgeschlagen
|
||||
// liegen Vorwort (links) und Programm (rechts) als Doppelseite 2/3 nebeneinander.
|
||||
if ($veranstaltung !== null && !empty($veranstaltung['programm'])) {
|
||||
$aufschlag[] = $live('programm');
|
||||
}
|
||||
// Interview-Seite nur, wenn die Ausgabe ein Interview trägt (chat-gepflegtes
|
||||
// Feld) — das Cover zeigt dann automatisch die Teaser-Zeile dazu.
|
||||
if ($oldInterview !== null) {
|
||||
@@ -308,8 +355,40 @@ try {
|
||||
$bloecke[] = ['name' => 'aufschlag', 'seiten' => $aufschlag, 'anzeigen_danach' => true];
|
||||
}
|
||||
|
||||
// Turnierplan-Seiten der Sportfest-Ausgabe: je Turnier eine Seite, EIN Block
|
||||
// (zusammenhängender Sportfest-Teil, keine Anzeige zwischen zwei Turnieren),
|
||||
// gleich nach dem Aufschlag und vor dem Fußball-Teil.
|
||||
$turniere = [];
|
||||
foreach ($veranstaltung['turniere'] ?? [] as $turnier) {
|
||||
if (is_array($turnier) && !empty($turnier['key'])) {
|
||||
$turniere[] = $live('turnier', ['turnier' => (string) $turnier['key']]);
|
||||
}
|
||||
}
|
||||
if ($turniere !== []) {
|
||||
$bloecke[] = ['name' => 'turniere', 'seiten' => $turniere, 'anzeigen_danach' => true];
|
||||
}
|
||||
|
||||
$bloecke[] = ['name' => 'ergebnisse', 'seiten' => [$live('ergebnisse')], 'anzeigen_danach' => true];
|
||||
$bloecke[] = ['name' => 'gegner', 'seiten' => [$live('gegner')], 'anzeigen_danach' => true];
|
||||
|
||||
// Gegnerseite(n): normal nur die tragende Mannschaft; in der Sportfest-Ausgabe
|
||||
// je Mannschaft mit Heimspiel am Fest eine (drei Heimspiele, drei Seiten), als
|
||||
// EIN Block. Welche das sind, entscheidet stadionzeitung_gegner_fixtures() —
|
||||
// dieselbe Funktion, aus der stadionzeitung_build_snapshot() die Daten baut,
|
||||
// damit nie eine Seite ohne Daten oder Daten ohne Seite entstehen.
|
||||
$gegnerSeiten = [];
|
||||
foreach (stadionzeitung_gegner_fixtures([
|
||||
'date' => $date, 'team_key' => $teamKey, 'opponent' => $opponent,
|
||||
'veranstaltung' => $oldVeranstaltung,
|
||||
]) as $gegnerTeam => $fixture) {
|
||||
$gegnerSeiten[] = $live('gegner', ['team' => (string) $gegnerTeam]);
|
||||
$log("Gegnerseite {$gegnerTeam}: {$fixture['opponent']} ({$fixture['date']}).");
|
||||
}
|
||||
if ($gegnerSeiten === []) {
|
||||
// Ohne Treffer (Gegner leer) bleibt die Seite der tragenden Mannschaft —
|
||||
// sie meldet dann selbst, dass keine Vergleichsdaten vorliegen.
|
||||
$gegnerSeiten[] = $live('gegner', ['team' => $teamKey]);
|
||||
}
|
||||
$bloecke[] = ['name' => 'gegner', 'seiten' => $gegnerSeiten, 'anzeigen_danach' => true];
|
||||
|
||||
// Eine Tabellen-Seite je Mannschaft, Reihenfolge wie in matchcenter.json
|
||||
// (= config 'matchcenter.teams'). Kommt eine Mannschaft dazu (z. B. die
|
||||
@@ -317,15 +396,28 @@ try {
|
||||
$tabellen = [];
|
||||
foreach ($mc['teams'] ?? [] as $team) {
|
||||
if (!empty($team['key'])) {
|
||||
$tabellen[] = $live('tabelle', (string) $team['key']);
|
||||
$tabellen[] = $live('tabelle', ['team' => (string) $team['key']]);
|
||||
}
|
||||
}
|
||||
if ($tabellen !== []) {
|
||||
$bloecke[] = ['name' => 'tabellen', 'seiten' => $tabellen, 'anzeigen_danach' => true];
|
||||
}
|
||||
|
||||
foreach (['vorschau', 'termine', 'news', 'kontakt', 'mitglied', 'historie', 'sportheim', 'partner', 'momente'] as $seite) {
|
||||
$bloecke[] = ['name' => $seite, 'seiten' => [$live($seite)], 'anzeigen_danach' => true];
|
||||
foreach (['vorschau', 'termine', 'news', 'kontakt', 'mitglied', 'historie', 'sportheim', 'partner', 'danke', 'momente'] as $seite) {
|
||||
// Die Danke-Seite trägt ihr Anzeigenmotiv im Seiteneintrag (siehe oben),
|
||||
// direkt hinter der Partner-werden-Seite: aufgeschlagen liegen damit
|
||||
// Akquise und Dank als Partner-Doppelseite nebeneinander.
|
||||
$extra = ($seite === 'danke' && isset($platzierteAnzeigen['danke']))
|
||||
? ['anzeige' => $platzierteAnzeigen['danke']]
|
||||
: [];
|
||||
// Hinter der Partner-werden-Seite bleibt die Lücke leer, damit die
|
||||
// Danke-Seite direkt folgt: Akquise und Dank gehören zusammen und
|
||||
// liegen aufgeschlagen im günstigen Fall als Doppelseite nebeneinander.
|
||||
$bloecke[] = [
|
||||
'name' => $seite,
|
||||
'seiten' => [$live($seite, $extra)],
|
||||
'anzeigen_danach' => $seite !== 'partner',
|
||||
];
|
||||
}
|
||||
|
||||
$bloecke[] = ['name' => 'impressum', 'seiten' => [$live('impressum')], 'anzeigen_danach' => false];
|
||||
@@ -651,6 +743,8 @@ $issues[] = [
|
||||
'vorwort' => $oldVorwort,
|
||||
'interview' => $oldInterview,
|
||||
'aufruf' => $oldAufruf,
|
||||
'veranstaltung' => $oldVeranstaltung,
|
||||
'cover' => $oldCover,
|
||||
'verfasser_photo' => $verfasserPhoto,
|
||||
'termine_snapshot' => $termineSnapshot,
|
||||
'images' => $images,
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
"slug": "sportfest-2026-programm",
|
||||
"sponsor": "Programm Sportfest 2026",
|
||||
"aktiv": true,
|
||||
"bis": "2026-09-06",
|
||||
"bis": "2026-09-03",
|
||||
"alt": "Eigenanzeige: Programm des Sportfests 2026 mit allen Spielen, Turnieren und der Kinderolympiade",
|
||||
"base": "img/anzeigen/sportfest-2026-programm",
|
||||
"widths": [
|
||||
@@ -248,18 +248,6 @@
|
||||
1440
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "emons-partnerdank",
|
||||
"sponsor": "Emons Spedition und Dank an unsere Partner",
|
||||
"aktiv": true,
|
||||
"alt": "Anzeige: Emons Spedition, darunter unser Dank an alle Partner",
|
||||
"base": "img/anzeigen/emons-partnerdank",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "gagel-pickelmann",
|
||||
"sponsor": "Gagel Bäder und Mineralölhandel Pickelmann",
|
||||
@@ -294,6 +282,19 @@
|
||||
960,
|
||||
1440
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "emons",
|
||||
"sponsor": "Emons Spedition",
|
||||
"aktiv": true,
|
||||
"base": "img/anzeigen/emons",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1141
|
||||
],
|
||||
"alt": "Anzeige: Emons Spedition Himmelkron, Ihr Partner für nationale und internationale Transporte",
|
||||
"platzierung": "danke"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,847 @@
|
||||
{
|
||||
"issues": [
|
||||
{
|
||||
"slug": "2026-09-04-ats-wartenfels",
|
||||
"opponent": "ATS Wartenfels",
|
||||
"date": "2026-09-04",
|
||||
"team_key": "erste-mannschaft",
|
||||
"status": "entwurf",
|
||||
"pages": 48,
|
||||
"player_image": {
|
||||
"base": "img/stadionzeitung/2026-09-04-ats-wartenfels/spieler",
|
||||
"widths": [
|
||||
320,
|
||||
640,
|
||||
1000
|
||||
],
|
||||
"ext": "png",
|
||||
"alt": ""
|
||||
},
|
||||
"qr_image": "img/stadionzeitung/2026-09-04-ats-wartenfels/qr.png",
|
||||
"vorwort": {
|
||||
"heading": "Drei Tage Fußball im Katzbachtal",
|
||||
"paragraphs": [
|
||||
"Liebe Fußballfreunde,",
|
||||
"herzlich willkommen zu unserem Sportfest. Ein besonderer Gruß gilt dem Schiedsrichtergespann und unseren Gästen vom ATS Wartenfels.",
|
||||
"Bis Sonntag ist bei uns durchgehend Betrieb: am Samstag unsere Zweite, am Sonntag unsere Frauen, dazwischen die Turniere unserer Jugend. Danke an alle, die dieses Wochenende möglich machen.",
|
||||
"Der 7:3-Heimsieg gegen Marktschorgast gibt uns Rückenwind. Leidenschaft, Wille, Heimsieg. Los geht's!"
|
||||
],
|
||||
"closing": "Rot-schwarze Grüße",
|
||||
"signee_name": "Benedikt Höfner",
|
||||
"signee_role": "Sportlicher Leiter"
|
||||
},
|
||||
"interview": null,
|
||||
"aufruf": null,
|
||||
"veranstaltung": "sportfest-2026",
|
||||
"cover": {
|
||||
"teaser": "Das komplette Programm zum Sportfest und die Spielpläne aller vier Jugendturniere."
|
||||
},
|
||||
"verfasser_photo": {
|
||||
"base": "img/stadionzeitung/2026-09-04-ats-wartenfels/verfasser",
|
||||
"widths": [
|
||||
120,
|
||||
222
|
||||
],
|
||||
"alt": ""
|
||||
},
|
||||
"termine_snapshot": [
|
||||
{
|
||||
"date": "2026-09-04",
|
||||
"date_end": "2026-09-06",
|
||||
"title": "Sportfest 2026",
|
||||
"department": "verein",
|
||||
"type": "event",
|
||||
"meta": "16:30 Uhr · Hans-Rausch-Sportanlage",
|
||||
"href": "/veranstaltungen/sportfest-2026",
|
||||
"time": "16:30",
|
||||
"location": "Hans-Rausch-Sportanlage",
|
||||
"raw": {
|
||||
"slug": "sportfest-2026",
|
||||
"title": "Sportfest 2026",
|
||||
"date_start": "2026-09-04",
|
||||
"date_end": "2026-09-06",
|
||||
"time": "16:30",
|
||||
"location": "Hans-Rausch-Sportanlage",
|
||||
"department": "verein",
|
||||
"status": "veroeffentlicht",
|
||||
"text": "Drei Tage Fußball, Essen, Trinken, Kaffee und Kuchen: bei uns ist für groß und klein alles dabei, für die Kinder steht eine Hüpfburg.",
|
||||
"ankuendigung": null,
|
||||
"rueckblick": null,
|
||||
"karte": "club",
|
||||
"anfahrt": [],
|
||||
"programm": [
|
||||
{
|
||||
"tag": "2026-09-04",
|
||||
"punkte": [
|
||||
{
|
||||
"zeit": "16:30",
|
||||
"titel": "Spiel unserer E2-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "18:00",
|
||||
"titel": "Unsere erste Mannschaft gegen ATS Wartenfels",
|
||||
"text": "Kreisklasse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "2026-09-05",
|
||||
"punkte": [
|
||||
{
|
||||
"zeit": "09:00",
|
||||
"titel": "Turnier der G-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "12:00",
|
||||
"titel": "Turnier der F1-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "12:00",
|
||||
"titel": "Turnier der F2-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "14:30",
|
||||
"titel": "Kinderolympiade",
|
||||
"text": "bis 16:30 Uhr in der Turnhalle Maximum Gym"
|
||||
},
|
||||
{
|
||||
"zeit": "15:00",
|
||||
"titel": "Turnier der D1-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "18:00",
|
||||
"titel": "Unsere zweite Mannschaft gegen SV Weidenberg III/SSV Warmensteinach II",
|
||||
"text": "Reserve"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "2026-09-06",
|
||||
"punkte": [
|
||||
{
|
||||
"zeit": "11:00",
|
||||
"titel": "Unsere Frauenmannschaft gegen 1. FC 1928 Schwarzenbach/Saale",
|
||||
"text": "Kreisliga Ost"
|
||||
},
|
||||
{
|
||||
"zeit": "13:00",
|
||||
"titel": "Turnier der E1-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "15:00",
|
||||
"titel": "Spiel unserer D2-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "16:30",
|
||||
"titel": "Spiel unserer C/B-Jugend"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"turniere": [
|
||||
{
|
||||
"key": "f1",
|
||||
"title": "F1-Jugend",
|
||||
"date": "2026-09-05",
|
||||
"start": "12:00",
|
||||
"spieldauer": 10,
|
||||
"modus": "Jeder gegen jeden mit Hin und Rückspiel",
|
||||
"teams": [
|
||||
"VfR Katschenreuth 1",
|
||||
"SV Motschenbach 1",
|
||||
"TSV Neudrossenfeld 1"
|
||||
],
|
||||
"spiele": [
|
||||
{
|
||||
"nr": 1,
|
||||
"zeit": "12:00",
|
||||
"heim": "SV Motschenbach 1",
|
||||
"gast": "TSV Neudrossenfeld 1"
|
||||
},
|
||||
{
|
||||
"nr": 2,
|
||||
"zeit": "12:15",
|
||||
"heim": "VfR Katschenreuth 1",
|
||||
"gast": "SV Motschenbach 1"
|
||||
},
|
||||
{
|
||||
"nr": 3,
|
||||
"zeit": "12:30",
|
||||
"heim": "TSV Neudrossenfeld 1",
|
||||
"gast": "VfR Katschenreuth 1"
|
||||
},
|
||||
{
|
||||
"nr": 4,
|
||||
"zeit": "12:45",
|
||||
"heim": "TSV Neudrossenfeld 1",
|
||||
"gast": "SV Motschenbach 1"
|
||||
},
|
||||
{
|
||||
"nr": 5,
|
||||
"zeit": "13:00",
|
||||
"heim": "SV Motschenbach 1",
|
||||
"gast": "VfR Katschenreuth 1"
|
||||
},
|
||||
{
|
||||
"nr": 6,
|
||||
"zeit": "13:15",
|
||||
"heim": "VfR Katschenreuth 1",
|
||||
"gast": "TSV Neudrossenfeld 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "f2",
|
||||
"title": "F2-Jugend",
|
||||
"date": "2026-09-05",
|
||||
"start": "12:00",
|
||||
"spieldauer": 15,
|
||||
"modus": "Jeder gegen jeden",
|
||||
"teams": [
|
||||
"TSV 08 Kulmbach",
|
||||
"SG Neuenmarkt",
|
||||
"VfR Katschenreuth 2",
|
||||
"SV Motschenbach 2"
|
||||
],
|
||||
"spiele": [
|
||||
{
|
||||
"nr": 1,
|
||||
"zeit": "12:00",
|
||||
"heim": "SG Neuenmarkt",
|
||||
"gast": "SV Motschenbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 2,
|
||||
"zeit": "12:16",
|
||||
"heim": "TSV 08 Kulmbach",
|
||||
"gast": "VfR Katschenreuth 2"
|
||||
},
|
||||
{
|
||||
"nr": 3,
|
||||
"zeit": "12:32",
|
||||
"heim": "SG Neuenmarkt",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 4,
|
||||
"zeit": "12:48",
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "SV Motschenbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 5,
|
||||
"zeit": "13:04",
|
||||
"heim": "SV Motschenbach 2",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 6,
|
||||
"zeit": "13:20",
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "SG Neuenmarkt"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "d1",
|
||||
"title": "D1-Jugend",
|
||||
"date": "2026-09-05",
|
||||
"start": "15:00",
|
||||
"spieldauer": 35,
|
||||
"modus": "Jeder gegen jeden",
|
||||
"teams": [
|
||||
"TSV 08 Kulmbach",
|
||||
"JFG Maintal/Friesenbachtal",
|
||||
"VfB Kulmbach"
|
||||
],
|
||||
"spiele": [
|
||||
{
|
||||
"nr": 1,
|
||||
"zeit": "15:00",
|
||||
"heim": "TSV 08 Kulmbach",
|
||||
"gast": "JFG Maintal/Friesenbachtal"
|
||||
},
|
||||
{
|
||||
"nr": 2,
|
||||
"zeit": "15:45",
|
||||
"heim": "JFG Maintal/Friesenbachtal",
|
||||
"gast": "VfB Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 3,
|
||||
"zeit": "16:30",
|
||||
"heim": "VfB Kulmbach",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "e1",
|
||||
"title": "E1-Jugend",
|
||||
"date": "2026-09-06",
|
||||
"start": "13:00",
|
||||
"spieldauer": 12,
|
||||
"felder": 2,
|
||||
"modus": "Jeder gegen jeden",
|
||||
"teams": [
|
||||
"TSV 08 Kulmbach",
|
||||
"SV Motschenbach 2",
|
||||
"VfB Kulmbach 2",
|
||||
"VfR Katschenreuth 2",
|
||||
"SG Wirsberg"
|
||||
],
|
||||
"spiele": [
|
||||
{
|
||||
"nr": 1,
|
||||
"zeit": "13:00",
|
||||
"feld": 1,
|
||||
"heim": "TSV 08 Kulmbach",
|
||||
"gast": "SG Wirsberg"
|
||||
},
|
||||
{
|
||||
"nr": 2,
|
||||
"zeit": "13:00",
|
||||
"feld": 2,
|
||||
"heim": "SV Motschenbach 2",
|
||||
"gast": "VfB Kulmbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 3,
|
||||
"zeit": "13:16",
|
||||
"feld": 1,
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 4,
|
||||
"zeit": "13:16",
|
||||
"feld": 2,
|
||||
"heim": "VfB Kulmbach 2",
|
||||
"gast": "SG Wirsberg"
|
||||
},
|
||||
{
|
||||
"nr": 5,
|
||||
"zeit": "13:32",
|
||||
"feld": 1,
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "SV Motschenbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 6,
|
||||
"zeit": "13:32",
|
||||
"feld": 2,
|
||||
"heim": "VfB Kulmbach 2",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 7,
|
||||
"zeit": "13:48",
|
||||
"feld": 1,
|
||||
"heim": "SG Wirsberg",
|
||||
"gast": "VfR Katschenreuth 2"
|
||||
},
|
||||
{
|
||||
"nr": 8,
|
||||
"zeit": "13:48",
|
||||
"feld": 2,
|
||||
"heim": "TSV 08 Kulmbach",
|
||||
"gast": "SV Motschenbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 9,
|
||||
"zeit": "14:04",
|
||||
"feld": 1,
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "VfB Kulmbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 10,
|
||||
"zeit": "14:04",
|
||||
"feld": 2,
|
||||
"heim": "SV Motschenbach 2",
|
||||
"gast": "SG Wirsberg"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"plakate": [
|
||||
{
|
||||
"base": "img/veranstaltungen/sportfest-2026/plakat-01",
|
||||
"widths": [
|
||||
640,
|
||||
1000,
|
||||
1587
|
||||
],
|
||||
"alt": "Plakat zum Sportfest 2026 mit Luftbild unserer Sportanlage und den Terminen vom 4. bis 6. September",
|
||||
"source": "1-ankuendigung.png"
|
||||
},
|
||||
{
|
||||
"base": "img/veranstaltungen/sportfest-2026/plakat-02",
|
||||
"widths": [
|
||||
640,
|
||||
1000,
|
||||
1587
|
||||
],
|
||||
"alt": "Programmplakat zum Sportfest 2026 mit allen Spielen, Turnieren und der Kinderolympiade am Samstag",
|
||||
"source": "2-programm.png"
|
||||
}
|
||||
],
|
||||
"galerie": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"date": "2026-09-05",
|
||||
"date_end": null,
|
||||
"title": "(SG1) TSV Melkendorf III/ TSV 08 Kulmbach II – (SG 2) SV Weidenberg III/SSV Warmensteinach II (flex)",
|
||||
"department": "fussball",
|
||||
"type": "spiel",
|
||||
"meta": "Heimspiel · Reserve BK 5 BT-KU",
|
||||
"href": "/matchcenter",
|
||||
"kickoff": "2026-09-05T18:00",
|
||||
"team_name": "2. Mannschaft",
|
||||
"league": "Reserve BK 5 BT-KU",
|
||||
"raw": {
|
||||
"kickoff": "2026-09-05T18:00",
|
||||
"date_display": "Sa, 05.09.",
|
||||
"time_display": "18:00 Uhr",
|
||||
"competition": "Liga",
|
||||
"home": "(SG1) TSV Melkendorf III/ TSV 08 Kulmbach II",
|
||||
"away": "(SG 2) SV Weidenberg III/SSV Warmensteinach II (flex)",
|
||||
"home_crest": "img/crests/00ES8GNK4O000002VV0AG08LVUPGND5I.png",
|
||||
"away_crest": "img/crests/00ES8GNK4O000013VV0AG08LVUPGND5I.png",
|
||||
"is_home": true,
|
||||
"team_key": "zweite-mannschaft",
|
||||
"team_name": "2. Mannschaft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"date": "2026-09-06",
|
||||
"date_end": null,
|
||||
"title": "TSV 08 Kulmbach – 1. FC 1928 Schwarzenbach/Saale",
|
||||
"department": "fussball",
|
||||
"type": "spiel",
|
||||
"meta": "Heimspiel · Frauen KL Ost",
|
||||
"href": "/matchcenter",
|
||||
"kickoff": "2026-09-06T11:00",
|
||||
"team_name": "Frauenmannschaft",
|
||||
"league": "Frauen KL Ost",
|
||||
"raw": {
|
||||
"kickoff": "2026-09-06T11:00",
|
||||
"date_display": "So, 06.09.",
|
||||
"time_display": "11:00 Uhr",
|
||||
"competition": "Liga",
|
||||
"home": "TSV 08 Kulmbach",
|
||||
"away": "1. FC 1928 Schwarzenbach/Saale",
|
||||
"home_crest": "img/crests/00ES8GNK3G000012VV0AG08LVUPGND5I.png",
|
||||
"away_crest": "img/crests/00ES8GNK9O00000QVV0AG08LVUPGND5I.png",
|
||||
"is_home": true,
|
||||
"team_key": "damen",
|
||||
"team_name": "Frauenmannschaft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"date": "2026-09-13",
|
||||
"date_end": null,
|
||||
"title": "TSV 08 Kulmbach – TSV Engelmannsreuth",
|
||||
"department": "fussball",
|
||||
"type": "spiel",
|
||||
"meta": "Heimspiel · Frauen KL Ost",
|
||||
"href": "/matchcenter",
|
||||
"kickoff": "2026-09-13T10:30",
|
||||
"team_name": "Frauenmannschaft",
|
||||
"league": "Frauen KL Ost",
|
||||
"raw": {
|
||||
"kickoff": "2026-09-13T10:30",
|
||||
"date_display": "So, 13.09.",
|
||||
"time_display": "10:30 Uhr",
|
||||
"competition": "Liga",
|
||||
"home": "TSV 08 Kulmbach",
|
||||
"away": "TSV Engelmannsreuth",
|
||||
"home_crest": "img/crests/00ES8GNK3G000012VV0AG08LVUPGND5I.png",
|
||||
"away_crest": "img/crests/00ES8GNK3G000009VV0AG08LVUPGND5I.png",
|
||||
"is_home": true,
|
||||
"team_key": "damen",
|
||||
"team_name": "Frauenmannschaft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"date": "2026-09-13",
|
||||
"date_end": null,
|
||||
"title": "SV Lindenhardt II flex – (SG1) TSV Melkendorf III/ TSV 08 Kulmbach II",
|
||||
"department": "fussball",
|
||||
"type": "spiel",
|
||||
"meta": "Auswärtsspiel · Reserve BK 5 BT-KU",
|
||||
"href": "/matchcenter",
|
||||
"kickoff": "2026-09-13T13:00",
|
||||
"team_name": "2. Mannschaft",
|
||||
"league": "Reserve BK 5 BT-KU",
|
||||
"raw": {
|
||||
"kickoff": "2026-09-13T13:00",
|
||||
"date_display": "So, 13.09.",
|
||||
"time_display": "13:00 Uhr",
|
||||
"competition": "Liga",
|
||||
"home": "SV Lindenhardt II flex",
|
||||
"away": "(SG1) TSV Melkendorf III/ TSV 08 Kulmbach II",
|
||||
"home_crest": "img/crests/00ES8GNK3G00001AVV0AG08LVUPGND5I.png",
|
||||
"away_crest": "img/crests/00ES8GNK4O000002VV0AG08LVUPGND5I.png",
|
||||
"is_home": false,
|
||||
"team_key": "zweite-mannschaft",
|
||||
"team_name": "2. Mannschaft"
|
||||
}
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"type": "live",
|
||||
"page": "cover"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "vorwort"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "programm"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/eldorado",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: eldorado Radsporttechnik Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "turnier",
|
||||
"turnier": "f1"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "turnier",
|
||||
"turnier": "f2"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "turnier",
|
||||
"turnier": "d1"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "turnier",
|
||||
"turnier": "e1"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/egu",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Erfrischungsgetränke-Union Kulmbacher Gruppe GmbH"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/hofmann-graef",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Buchhandlung Hofmann, Kulmbach, und Automobilwerkstatt Gräf, Thurnau"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "ergebnisse"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/muench-richterstegner",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Planungsbüro Münch und Richter & Stegner Steuerungstechnik"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "gegner",
|
||||
"team": "erste-mannschaft"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "gegner",
|
||||
"team": "zweite-mannschaft"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "gegner",
|
||||
"team": "damen"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/malteser-herrmann",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Malteser-Hilfsdienst und Schlosserei Herrmann, Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/apotheken-wenkel",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Apotheke im Fritz, Stadtpark Apotheke und Schlüsseldienst Wenkel, Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "tabelle",
|
||||
"team": "erste-mannschaft"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "tabelle",
|
||||
"team": "zweite-mannschaft"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "tabelle",
|
||||
"team": "damen"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/karl-geyer",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Geyer Farbe + Raum, Kulmbach, Starthilfe Boden"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "vorschau"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/gsb",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: GSB Security und GSB Gebäude Service, Bindlach"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/physiofit-wohlfart",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Physio Fit Neudrossenfeld und Textilpflege Wohlfart, Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "termine"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/sport2000-pizza",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Sport 2000 Downstairs und Kulmbacher Pizza-Service"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/schulz-csu",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Schreinerei Schulz, Mainleus, und CSU Stadtverband Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "news"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/mattis-ka2",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Karl Mattis Flaschnerei und KA2 Fitness, Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "kontakt"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/motor-nuetzel",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Motor-Nützel Vertriebs-GmbH, Bayreuth"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/vereinskarte-edeka",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1410
|
||||
],
|
||||
"alt": "Eigenanzeige: So funktioniert die Vereinskarte beim E center Seidl Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "mitglied"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/trapper-verkehrsakademie",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Trapper GmbH Entsorgungsfachbetrieb und Verkehrsakademie, Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "historie"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/ohnemueller-dippold",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Gaststätte und Metzgerei Ohne-Müller, Kulmbach-Weiher, und Bäckerei Dippold, Melkendorf"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/hanft",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Hanft Bürotechnik GmbH, Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "sportheim"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/elma-kleinheinz",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: elma GmbH Bayreuth und Metzgerei Kleinheinz, Kulmbach"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "partner"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "danke",
|
||||
"anzeige": {
|
||||
"base": "img/anzeigen/emons",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1141
|
||||
],
|
||||
"alt": "Anzeige: Emons Spedition Himmelkron, Ihr Partner für nationale und internationale Transporte"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/stadionzeitung-digital",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Eigenanzeige: Unsere Stadionzeitung gibt es jetzt auch online, mit QR-Code zum Scannen"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/gagel-pickelmann",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Gagel Bäder und Mineralölhandel Pickelmann"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "momente"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/schramm",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Anzeige: Landgasthof und Metzgerei Schramm, Forstlahm"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/anzeigen/tagesskifahrt-2027",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1440
|
||||
],
|
||||
"alt": "Eigenanzeige: Unsere Tagesskifahrt ins Hochzillertal am 20. Februar 2027"
|
||||
},
|
||||
{
|
||||
"type": "live",
|
||||
"page": "impressum"
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"base": "img/stadionzeitung/2026-09-04-ats-wartenfels/rueckseite",
|
||||
"widths": [
|
||||
480,
|
||||
960,
|
||||
1410
|
||||
],
|
||||
"alt": "Rückseite der Stadionzeitung gegen ATS Wartenfels"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "2026-08-29-asv-marktschorgast",
|
||||
"opponent": "ASV Marktschorgast",
|
||||
|
||||
@@ -45,6 +45,301 @@
|
||||
"rueckblick": null,
|
||||
"karte": "club",
|
||||
"anfahrt": [],
|
||||
"programm": [
|
||||
{
|
||||
"tag": "2026-09-04",
|
||||
"punkte": [
|
||||
{
|
||||
"zeit": "16:30",
|
||||
"titel": "Spiel unserer E2-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "18:00",
|
||||
"titel": "Unsere erste Mannschaft gegen ATS Wartenfels",
|
||||
"text": "Kreisklasse"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "2026-09-05",
|
||||
"punkte": [
|
||||
{
|
||||
"zeit": "09:00",
|
||||
"titel": "Turnier der G-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "12:00",
|
||||
"titel": "Turnier der F1-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "12:00",
|
||||
"titel": "Turnier der F2-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "14:30",
|
||||
"titel": "Kinderolympiade",
|
||||
"text": "bis 16:30 Uhr in der Turnhalle Maximum Gym"
|
||||
},
|
||||
{
|
||||
"zeit": "15:00",
|
||||
"titel": "Turnier der D1-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "18:00",
|
||||
"titel": "Unsere zweite Mannschaft gegen SV Weidenberg III/SSV Warmensteinach II",
|
||||
"text": "Reserve"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "2026-09-06",
|
||||
"punkte": [
|
||||
{
|
||||
"zeit": "11:00",
|
||||
"titel": "Unsere Frauenmannschaft gegen 1. FC 1928 Schwarzenbach/Saale",
|
||||
"text": "Kreisliga Ost"
|
||||
},
|
||||
{
|
||||
"zeit": "13:00",
|
||||
"titel": "Turnier der E1-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "15:00",
|
||||
"titel": "Spiel unserer D2-Jugend"
|
||||
},
|
||||
{
|
||||
"zeit": "16:30",
|
||||
"titel": "Spiel unserer C/B-Jugend"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"turniere": [
|
||||
{
|
||||
"key": "f1",
|
||||
"title": "F1-Jugend",
|
||||
"date": "2026-09-05",
|
||||
"start": "12:00",
|
||||
"spieldauer": 10,
|
||||
"modus": "Jeder gegen jeden mit Hin und Rückspiel",
|
||||
"teams": [
|
||||
"VfR Katschenreuth 1",
|
||||
"SV Motschenbach 1",
|
||||
"TSV Neudrossenfeld 1"
|
||||
],
|
||||
"spiele": [
|
||||
{
|
||||
"nr": 1,
|
||||
"zeit": "12:00",
|
||||
"heim": "SV Motschenbach 1",
|
||||
"gast": "TSV Neudrossenfeld 1"
|
||||
},
|
||||
{
|
||||
"nr": 2,
|
||||
"zeit": "12:15",
|
||||
"heim": "VfR Katschenreuth 1",
|
||||
"gast": "SV Motschenbach 1"
|
||||
},
|
||||
{
|
||||
"nr": 3,
|
||||
"zeit": "12:30",
|
||||
"heim": "TSV Neudrossenfeld 1",
|
||||
"gast": "VfR Katschenreuth 1"
|
||||
},
|
||||
{
|
||||
"nr": 4,
|
||||
"zeit": "12:45",
|
||||
"heim": "TSV Neudrossenfeld 1",
|
||||
"gast": "SV Motschenbach 1"
|
||||
},
|
||||
{
|
||||
"nr": 5,
|
||||
"zeit": "13:00",
|
||||
"heim": "SV Motschenbach 1",
|
||||
"gast": "VfR Katschenreuth 1"
|
||||
},
|
||||
{
|
||||
"nr": 6,
|
||||
"zeit": "13:15",
|
||||
"heim": "VfR Katschenreuth 1",
|
||||
"gast": "TSV Neudrossenfeld 1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "f2",
|
||||
"title": "F2-Jugend",
|
||||
"date": "2026-09-05",
|
||||
"start": "12:00",
|
||||
"spieldauer": 15,
|
||||
"modus": "Jeder gegen jeden",
|
||||
"teams": [
|
||||
"TSV 08 Kulmbach",
|
||||
"SG Neuenmarkt",
|
||||
"VfR Katschenreuth 2",
|
||||
"SV Motschenbach 2"
|
||||
],
|
||||
"spiele": [
|
||||
{
|
||||
"nr": 1,
|
||||
"zeit": "12:00",
|
||||
"heim": "SG Neuenmarkt",
|
||||
"gast": "SV Motschenbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 2,
|
||||
"zeit": "12:16",
|
||||
"heim": "TSV 08 Kulmbach",
|
||||
"gast": "VfR Katschenreuth 2"
|
||||
},
|
||||
{
|
||||
"nr": 3,
|
||||
"zeit": "12:32",
|
||||
"heim": "SG Neuenmarkt",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 4,
|
||||
"zeit": "12:48",
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "SV Motschenbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 5,
|
||||
"zeit": "13:04",
|
||||
"heim": "SV Motschenbach 2",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 6,
|
||||
"zeit": "13:20",
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "SG Neuenmarkt"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "d1",
|
||||
"title": "D1-Jugend",
|
||||
"date": "2026-09-05",
|
||||
"start": "15:00",
|
||||
"spieldauer": 35,
|
||||
"modus": "Jeder gegen jeden",
|
||||
"teams": [
|
||||
"TSV 08 Kulmbach",
|
||||
"JFG Maintal/Friesenbachtal",
|
||||
"VfB Kulmbach"
|
||||
],
|
||||
"spiele": [
|
||||
{
|
||||
"nr": 1,
|
||||
"zeit": "15:00",
|
||||
"heim": "TSV 08 Kulmbach",
|
||||
"gast": "JFG Maintal/Friesenbachtal"
|
||||
},
|
||||
{
|
||||
"nr": 2,
|
||||
"zeit": "15:45",
|
||||
"heim": "JFG Maintal/Friesenbachtal",
|
||||
"gast": "VfB Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 3,
|
||||
"zeit": "16:30",
|
||||
"heim": "VfB Kulmbach",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "e1",
|
||||
"title": "E1-Jugend",
|
||||
"date": "2026-09-06",
|
||||
"start": "13:00",
|
||||
"spieldauer": 12,
|
||||
"felder": 2,
|
||||
"modus": "Jeder gegen jeden",
|
||||
"teams": [
|
||||
"TSV 08 Kulmbach",
|
||||
"SV Motschenbach 2",
|
||||
"VfB Kulmbach 2",
|
||||
"VfR Katschenreuth 2",
|
||||
"SG Wirsberg"
|
||||
],
|
||||
"spiele": [
|
||||
{
|
||||
"nr": 1,
|
||||
"zeit": "13:00",
|
||||
"feld": 1,
|
||||
"heim": "TSV 08 Kulmbach",
|
||||
"gast": "SG Wirsberg"
|
||||
},
|
||||
{
|
||||
"nr": 2,
|
||||
"zeit": "13:00",
|
||||
"feld": 2,
|
||||
"heim": "SV Motschenbach 2",
|
||||
"gast": "VfB Kulmbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 3,
|
||||
"zeit": "13:16",
|
||||
"feld": 1,
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 4,
|
||||
"zeit": "13:16",
|
||||
"feld": 2,
|
||||
"heim": "VfB Kulmbach 2",
|
||||
"gast": "SG Wirsberg"
|
||||
},
|
||||
{
|
||||
"nr": 5,
|
||||
"zeit": "13:32",
|
||||
"feld": 1,
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "SV Motschenbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 6,
|
||||
"zeit": "13:32",
|
||||
"feld": 2,
|
||||
"heim": "VfB Kulmbach 2",
|
||||
"gast": "TSV 08 Kulmbach"
|
||||
},
|
||||
{
|
||||
"nr": 7,
|
||||
"zeit": "13:48",
|
||||
"feld": 1,
|
||||
"heim": "SG Wirsberg",
|
||||
"gast": "VfR Katschenreuth 2"
|
||||
},
|
||||
{
|
||||
"nr": 8,
|
||||
"zeit": "13:48",
|
||||
"feld": 2,
|
||||
"heim": "TSV 08 Kulmbach",
|
||||
"gast": "SV Motschenbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 9,
|
||||
"zeit": "14:04",
|
||||
"feld": 1,
|
||||
"heim": "VfR Katschenreuth 2",
|
||||
"gast": "VfB Kulmbach 2"
|
||||
},
|
||||
{
|
||||
"nr": 10,
|
||||
"zeit": "14:04",
|
||||
"feld": 2,
|
||||
"heim": "SV Motschenbach 2",
|
||||
"gast": "SG Wirsberg"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"plakate": [
|
||||
{
|
||||
"base": "img/veranstaltungen/sportfest-2026/plakat-01",
|
||||
|
||||
@@ -6097,6 +6097,25 @@ body.is-sz-full .back-to-top {
|
||||
}
|
||||
|
||||
|
||||
/* Titelgeschichte einer Sonderausgabe: das Fest statt der Begegnung. Coolvetica
|
||||
in Versalien wie der Masthead, aber kleiner — es soll die zweite Stimme auf
|
||||
dem Cover sein, nicht mit der Wortmarke konkurrieren. */
|
||||
.stadionzeitung-cover__feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1.7cqi;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stadionzeitung-cover__feature-title {
|
||||
font-family: var(--ff-heading);
|
||||
font-size: 13cqi;
|
||||
line-height: 0.88;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 0 2px 10px rgb(0 0 0 / 0.5);
|
||||
}
|
||||
|
||||
/* Vereins-Hero-Bild als Hintergrund (dieselbe Quelle wie die Startseite). */
|
||||
.stadionzeitung-cover__bg {
|
||||
position: absolute;
|
||||
@@ -6135,6 +6154,63 @@ body.is-sz-full .back-to-top {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Hauptplakat einer Sonderausgabe an der Stelle des Spielerfotos: rechts
|
||||
randabfallend, leicht schräg gelegt und an zwei gegenüberliegenden Ecken
|
||||
gerundet (links oben, rechts unten) — wie ein aufs Cover gelegter Aushang,
|
||||
nicht wie ein eingesetztes Bild (Felix, 03.09.2026).
|
||||
|
||||
Warum kleiner als das Spielerfoto (98cqi hoch): ein Freisteller ist schmal,
|
||||
ein Plakat im Hochformat ist bei gleicher Höhe knapp 60cqi breit und läge damit
|
||||
über der Typografie unten links. Bei 66cqi Höhe belegt es die rechte Bildhälfte
|
||||
und lässt Masthead und Fest-Titel frei — die Textspalte wird dafür schmaler
|
||||
(siehe --poster .stadionzeitung-cover__corner).
|
||||
|
||||
Schatten statt Rahmen, damit das Plakat auf dem Foto-Hintergrund abhebt:
|
||||
box-shadow ist unbedenklich, die Druck-Falle im Projekt betrifft text-shadow
|
||||
auf umbrechendem Text (siehe .stadionzeitung-cover__corner). */
|
||||
.stadionzeitung-cover__poster {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
/* VOLLSTÄNDIG im Bild, nicht randabfallend wie das Spielerfoto: ein Plakat
|
||||
trägt Text (Datum, Ort), und angeschnitten fehlte rechts genau der. Die
|
||||
Rotation lässt die Ecken trotzdem über die Passepartout-Linie laufen, das
|
||||
Plakat liegt also sichtbar „auf" der Seite. */
|
||||
/* Unten rechts randabfallend („angeschnitten", Felix 03.09.2026): das Plakat
|
||||
läuft über zwei Kanten hinaus und wirkt dadurch aufgelegt statt eingesetzt.
|
||||
Angeschnitten wird nur die untere rechte Ecke, nicht die Kopfzeile mit den
|
||||
Terminen — die bleibt lesbar. */
|
||||
right: -1.5cqi;
|
||||
/* bottom: 0 statt negativ — die Box endet an der Blattkante, angeschnitten
|
||||
wird nur noch die gedrehte Ecke (4 Grad auf 53cqi Breite = knapp 4cqi
|
||||
Überhang). Das hält den Anschnitt-Look und rückt das Plakat gleichzeitig
|
||||
aus dem unteren Bereich nach oben (Felix, 03.09.2026). */
|
||||
bottom: 0;
|
||||
/* Höhe ist NICHT frei wählbar: das Plakat darf die Textspalte links nicht
|
||||
berühren. Rechnung bei 4 Grad Drehung — belegte Breite = Höhe / 1,414
|
||||
(Hochformat) + sin(4°) × Höhe (Überhang der gedrehten Ecken) ≈ 0,78 × Höhe.
|
||||
Bei 72cqi sind das 56,2cqi; mit 1,5cqi Überhang rechts liegt die linke Kante
|
||||
bei 45,3% und die Textspalte endet bei 44%. Der Überhang ist bewusst winzig:
|
||||
vom Plakat bleiben so 97% sichtbar und seine Schlagzeile „Sportfest" steht
|
||||
ganz im Bild (Felix, 03.09.2026) — bei 9cqi Überhang fehlte rechts ein
|
||||
Siebtel und das Wort lief aus dem Bild. Angeschnitten wirkt es trotzdem,
|
||||
das macht die gedrehte Ecke unten. Höhe und Überhang hängen also
|
||||
zusammen: weiter nach links (kleinerer Überhang) heißt kleiner, größer
|
||||
heißt weiter nach rechts — oder die Textspalte muss schmaler
|
||||
(--poster .stadionzeitung-cover__corner). Nicht nur eine Zahl drehen. */
|
||||
height: 72cqi;
|
||||
width: auto;
|
||||
border-radius: 5cqi 0 5cqi 0;
|
||||
transform: rotate(4deg);
|
||||
box-shadow: 0 3cqi 6cqi rgb(0 0 0 / 0.45);
|
||||
}
|
||||
|
||||
/* Schmaler als das Standard-Cover (56cqi): das Plakat belegt die rechte Hälfte.
|
||||
„Sportfest 2026" bricht dadurch zweizeilig — auf einem Cover trägt das eher,
|
||||
als es schadet. */
|
||||
.stadionzeitung-cover--poster .stadionzeitung-cover__corner {
|
||||
max-width: 38cqi;
|
||||
}
|
||||
|
||||
.stadionzeitung-cover__inner {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
@@ -7452,6 +7528,225 @@ a.news-feature__link:hover .news-feature__more {
|
||||
color: var(--clr-accent);
|
||||
}
|
||||
|
||||
/* === stadionzeitung-programm (Live-Seite, Sportfest-Ausgabe) === */
|
||||
/* Tagesprogramm aus data/veranstaltungen.json → programm[] (dieselbe Liste wie
|
||||
die Website-Detailseite, hier fürs A5-Blatt gesetzt): je Tag ein Abel-Caps-
|
||||
Tagestitel, darunter Zeit | Titel. Die Zeit trägt links den roten Strich wie
|
||||
.veranstaltung-programm__zeit auf der Website — gleiche Sprache, andere
|
||||
Skala. Maße in cqi (Selbst-Containment, siehe .stadionzeitung-page). */
|
||||
/* Abstände bewusst straff: ein dreitägiges Fest bringt gut zwölf Punkte plus
|
||||
drei Tagestitel auf EIN A5-Blatt (gemessen: mit den üblichen 3.3cqi lief die
|
||||
Seite 59px über die Blattkante). Wer hier lockerer setzt, misst die
|
||||
Seitenhöhe nach. */
|
||||
.stadionzeitung-programm__tag {
|
||||
margin-bottom: 2.2cqi;
|
||||
}
|
||||
|
||||
.stadionzeitung-programm__tag:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stadionzeitung-programm__tag-titel {
|
||||
margin-bottom: 0.9cqi;
|
||||
padding-bottom: 0.6cqi;
|
||||
border-bottom: 1px solid var(--clr-glass-border);
|
||||
font-family: var(--ff-body);
|
||||
font-weight: var(--fw-bold);
|
||||
font-size: 2.9cqi;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--ls-caps);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.stadionzeitung-programm__liste {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.stadionzeitung-programm__punkt {
|
||||
display: grid;
|
||||
grid-template-columns: 11cqi minmax(0, 1fr);
|
||||
gap: 2.2cqi;
|
||||
align-items: baseline;
|
||||
padding-block: 0.5cqi;
|
||||
}
|
||||
|
||||
.stadionzeitung-programm__zeit {
|
||||
padding-left: 1.4cqi;
|
||||
border-left: 2px solid var(--clr-accent);
|
||||
font-family: var(--ff-body);
|
||||
font-weight: var(--fw-bold);
|
||||
font-size: 3.3cqi;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stadionzeitung-programm__titel {
|
||||
display: block;
|
||||
font-size: 3.3cqi;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.stadionzeitung-programm__text {
|
||||
display: block;
|
||||
margin-top: 0.3cqi;
|
||||
font-size: 2.7cqi;
|
||||
line-height: 1.3;
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
/* === stadionzeitung-turnier (Live-Seite, Sportfest-Ausgabe) === */
|
||||
/* Ein Jugendturnier pro Blatt: Spielplan mit LEERER Ergebnisspalte
|
||||
(Schreiblinien) und leere Abschlusstabelle — das Blatt ist zum Eintragen am
|
||||
Platz gedacht, deshalb echte Schreibhöhe in den leeren Zellen. Eigenes Team
|
||||
wie league-table tr.is-own (rote Tönung + Abel-Fett). Tabellenmaße an
|
||||
.stadionzeitung-page .league-table angelehnt (2.7/2.3cqi). Der Modifier
|
||||
--dicht (ab neun Spielen, E-Jugend) rückt die Zeilen enger, sonst läuft die
|
||||
Abschlusstabelle über die Blattkante. */
|
||||
.stadionzeitung-turnier__label {
|
||||
caption-side: top;
|
||||
text-align: left;
|
||||
margin: 3.3cqi 0 1.2cqi;
|
||||
font-family: var(--ff-body);
|
||||
font-weight: var(--fw-bold);
|
||||
font-size: 2.9cqi;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--ls-caps);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
p.stadionzeitung-turnier__label:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__teams {
|
||||
font-size: 3.1cqi;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__spiele,
|
||||
.stadionzeitung-turnier__tabelle {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 2.9cqi;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__spiele thead th,
|
||||
.stadionzeitung-turnier__tabelle thead th {
|
||||
padding: 0.8cqi 0.9cqi;
|
||||
border-bottom: 1px solid var(--clr-glass-border-strong);
|
||||
font-family: var(--ff-body);
|
||||
font-weight: var(--fw-bold);
|
||||
font-size: 2.3cqi;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--ls-caps);
|
||||
color: var(--clr-text-muted);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__spiele td,
|
||||
.stadionzeitung-turnier__tabelle td {
|
||||
padding: 1cqi 0.9cqi;
|
||||
border-bottom: 1px solid var(--clr-glass-border);
|
||||
vertical-align: middle;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* Dichte Variante ab neun Spielen: die E-Jugend spielt zehn Partien auf zwei
|
||||
Feldern und braucht darunter noch eine Tabelle mit fünf Zeilen. Mit den
|
||||
normalen Abständen lief das Blatt 69px über. Alle vier Stellen unten gehören
|
||||
zusammen — eine allein reicht nicht. */
|
||||
.stadionzeitung-turnier--dicht .stadionzeitung-turnier__spiele td {
|
||||
padding-block: 0.3cqi;
|
||||
}
|
||||
|
||||
/* Die Zeilenhöhe des Spielplans macht NICHT die Schriftgröße, sondern die
|
||||
Schreiblinie: sie ist das höchste Element in der Zeile. Bei zehn Spielen
|
||||
zählt deshalb nur diese Stelle (eine kleinere Schrift brachte gemessen
|
||||
einen einzigen Pixel). 2.8cqi sind auf A5 gut 4mm über der Linie, genug
|
||||
für zwei Ziffern mit dem Stift. */
|
||||
.stadionzeitung-turnier--dicht .stadionzeitung-turnier__blank {
|
||||
height: 2.8cqi;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier--dicht .stadionzeitung-turnier__tabelle td {
|
||||
padding-block: 0.6cqi;
|
||||
}
|
||||
|
||||
/* 4cqi Zellhöhe sind auf A5 knapp 6mm plus Innenabstand — genug, um Tore und
|
||||
Punkte mit dem Stift einzutragen, und es passt neben zehn Spielzeilen. */
|
||||
.stadionzeitung-turnier--dicht .stadionzeitung-turnier__tabelle td:empty {
|
||||
height: 4cqi;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier--dicht .stadionzeitung-turnier__label {
|
||||
margin: 1.8cqi 0 0.7cqi;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier--dicht .stadionzeitung-turnier__teams {
|
||||
font-size: 2.9cqi;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__nr {
|
||||
width: 6cqi;
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__zeit {
|
||||
width: 9cqi;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__paarung {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__vs {
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__team.is-own {
|
||||
padding-inline: 0.6cqi;
|
||||
border-radius: 0.6cqi;
|
||||
background: rgb(var(--clr-accent-rgb) / 0.16);
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
|
||||
/* Leere Ergebnisspalte: zwei Schreiblinien um den Doppelpunkt, unten
|
||||
ausgerichtet wie eine Zeile zum Ausfüllen. */
|
||||
.stadionzeitung-turnier__ergebnis {
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__blank {
|
||||
display: inline-block;
|
||||
width: 6cqi;
|
||||
height: 3.4cqi;
|
||||
border-bottom: 1px solid var(--clr-text-faint);
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__colon {
|
||||
display: inline-block;
|
||||
padding-inline: 0.8cqi;
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
|
||||
/* Schreibhöhe der leeren Tabellenzellen (Platzierung wird von Hand eingetragen). */
|
||||
.stadionzeitung-turnier__tabelle td:empty {
|
||||
height: 5.6cqi;
|
||||
}
|
||||
|
||||
.stadionzeitung-turnier__tabelle th:nth-child(n + 3),
|
||||
.stadionzeitung-turnier__tabelle td:nth-child(n + 3) {
|
||||
width: 9cqi;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* === stadionzeitung-news (Live-Seite) === */
|
||||
/* Eigene Liste statt des geteilten news-feed von /news: feste Medienspalte
|
||||
(Wappen/Foto/Icon-Badge in EINER Größe), damit alle Titel bündig stehen —
|
||||
@@ -7864,6 +8159,57 @@ a.news-feature__link:hover .news-feature__more {
|
||||
color: var(--clr-text);
|
||||
}
|
||||
|
||||
/* === stadionzeitung-danke (Live-Seite) === */
|
||||
/* Dank an die Partner: oben die platzierte Anzeige (Motiv aus dem Seiteneintrag,
|
||||
siehe stadionzeitung-danke.php), darunter Kopf, Logo-Wand und der QR auf
|
||||
/partner-werden. Vier Logos pro Reihe in EINER festen Zellhöhe, damit
|
||||
unterschiedlich hohe Logos auf einer Linie sitzen — dieselbe Idee wie die
|
||||
feste Medienspalte auf der News-Seite. Maße in cqi (Selbst-Containment). */
|
||||
.stadionzeitung-danke {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.stadionzeitung-danke__anzeige {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 5cqi;
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
}
|
||||
|
||||
.stadionzeitung-danke__head {
|
||||
margin-bottom: 3.3cqi;
|
||||
}
|
||||
|
||||
.stadionzeitung-danke__logos {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 3.3cqi 2.5cqi;
|
||||
margin: 0 0 auto;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.stadionzeitung-danke__logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Feste Zellhöhe: Logos sind teils quer, teils hoch — ohne sie tanzt die Reihe. */
|
||||
height: 11cqi;
|
||||
}
|
||||
|
||||
/* Logos liegen hier auf Weiß statt auf dem dunklen Website-Grund: contain statt
|
||||
cover, damit nichts beschnitten wird, und keine Tönung — ein Sponsorenlogo
|
||||
wird nicht umgefärbt. */
|
||||
.stadionzeitung-danke__logo img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* === stadionzeitung-momente (Live-Seite, Heft-Abschluss) === */
|
||||
/* 2×2-Galerie der neuesten Instagram-Fotos + Folge-uns-Ticket — quadratische
|
||||
Kacheln wie im Instagram-Raster, bewusst ohne Bildunterschriften: die
|
||||
|
||||
BIN
public/assets/img/anzeigen/emons-1141.jpg
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
public/assets/img/anzeigen/emons-480.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/assets/img/anzeigen/emons-960.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 207 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 579 B |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 804 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 440 B |
|
Before Width: | Height: | Size: 430 B After Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 421 B After Width: | Height: | Size: 424 B |
|
Before Width: | Height: | Size: 427 B After Width: | Height: | Size: 421 B |
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 497 B |
|
Before Width: | Height: | Size: 505 B After Width: | Height: | Size: 496 B |
|
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 426 B |