Matchcenter: „Nächste Spiele" als Band statt Autoplay-Slider
Der Bereich wirkte deplatziert: der Track war auf 620px begrenzt und zeigte eine Folie, wodurch die Karte auf breiten Fenstern in ~840px Totraum schwebte; dazu derselbe Hintergrund wie der Hero direkt darüber, kein Akzent, und ein einsames „:" zwischen den Wappen, das wie ein fehlendes Ergebnis aussah. Neu als abgesetztes Band (.section--sunken, nutzt endlich das dafür vorgesehene, bisher unbenutzte Token --clr-bg-deep) mit roter Oberkante: links der Aufmacher (match-feature), rechts die folgenden Termine als verlinkte match-row-Zeilen ohne Kartenfläche. Der Aufmacher ist die große Variante der Terminzeile — Heim über Gast, linksbündig, gleiche Struktur wie die Zeilen daneben. Bewusst nur drei typografische Rollen (Meta-Zeile · Vereinsnamen in Coolvetica · Anstoßzeile mit Countdown) statt der acht der ersten Fassung, ohne Badges, Icons oder Button. Die eigene Mannschaft wird über Helligkeit markiert, nicht über font-weight — Coolvetica hat nur einen Schnitt, bold wäre synthetisch. match-row bekommt optionale $href/$meta-Props; Raster und Innenabstand liegen jetzt im __link-Wrapper, damit als Link die ganze Zeilenfläche klickbar ist. Neue Helper match_when() (ISO + deutscher Countdown, explizit Europe/Berlin, weil PHP global auf UTC läuft) und match_competition_label(). match-slider.php und match-card.php entfallen — Letztere wurde ausschließlich vom Slider genutzt. carousel.js treibt damit nur noch den Banner-Slider der Startseite und wird im Matchcenter nicht mehr geladen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
CLAUDE.md
17
CLAUDE.md
@@ -70,7 +70,9 @@ Sichtbare FAQ + FAQPage-Schema aus derselben `data/*.json`-Quelle, damit Inhalt
|
||||
`config/config.php`), `icon('name', 'klasse', 'label?')` (Inline-SVG aus `public/assets/icons/`, siehe
|
||||
Abschnitt Icons), `form_token()`/`form_token_valid()` (Time-Trap-Token, siehe Formulare),
|
||||
`img_intrinsic_attrs('pfad')` (width/height-Attribute aus der Bilddatei für CLS-freie `<img>`,
|
||||
wenn die Maße nicht schon statisch bekannt sind).
|
||||
wenn die Maße nicht schon statisch bekannt sind), `match_when('2026-08-01T14:00')` (Anstoß aus
|
||||
`matchcenter.json` → `iso` für `<time datetime>`/JS-Ticker + deutscher Countdown-Text, gelesen als
|
||||
Europe/Berlin, weil PHP global auf UTC läuft).
|
||||
- **CSS:** 6 Dateien als `<link>` in fester Reihenfolge: tokens → reset → base → layout → components →
|
||||
utilities. Kein `@import`, kein Inline-Style. Komponenten-Styles in `components.css` mit
|
||||
Banner-Kommentaren (`/* === hero === */`).
|
||||
@@ -157,5 +159,14 @@ aus der öffentlichen **BFV-Widget-JSON-API** (`widget-prod.bfv.de`, reverse-eng
|
||||
Browser-User-Agent + `Referer` nötig (sonst HTTP 418). Eigene-Verein-Erkennung exakt über `permanentId`.
|
||||
Bei Fehlern bleibt der letzte gute Cache unangetastet (Seite degradiert, bricht nie; ohne Cache rendert
|
||||
`matchcenter-empty`). Cron 2×/Tag + am Wochenende ½-stündlich nachmittags (siehe `config.example.php`).
|
||||
Der „Nächste Spiele"-Slider teilt sich den Antrieb `assets/js/carousel.js` (generisch, `[data-carousel]`)
|
||||
mit dem Banner-Slider. Besucher laden alle Matchcenter-Inhalte ausschließlich von unserer Domain.
|
||||
Das „Nächste Spiele"-Band (`next-matches`) zeigt links den Aufmacher (`match-feature`: das zeitlich
|
||||
nächste Spiel aller Mannschaften) und rechts die folgenden Termine als verlinkte `match-row`-Zeilen
|
||||
(`match-list--plain`, ohne Kartenfläche). **Ein Bauprinzip fürs ganze Band:** der Aufmacher ist die
|
||||
große Variante der Terminzeile — Heim über Gast, linksbündig, dieselbe Struktur, nur größer.
|
||||
Genau **drei Schriftrollen**, nicht mehr: Meta-Zeile „Mannschaft · Liga · Heim/Auswärts" (Abel,
|
||||
uppercase, gesperrt) · Vereinsnamen (Coolvetica, einziges Heading-Element) · Anstoßzeile mit Countdown
|
||||
via `match_when()` (Abel). Keine Badges, Icons oder Buttons — die Wirkung kommt aus der Größe, Rot nur
|
||||
als Oberkante des Bandes. Die eigene Mannschaft wird über Helligkeit markiert, nie über `font-weight`
|
||||
(Coolvetica hat nur einen Schnitt → sonst Fake-Bold). Kein Slider —
|
||||
`assets/js/carousel.js` treibt nur noch den Banner-Slider der Startseite.
|
||||
Besucher laden alle Matchcenter-Inhalte ausschließlich von unserer Domain.
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Einzelnes Spiel (Begegnung) als Karte. Props:
|
||||
* $match Eintrag aus data/matchcenter.json (next_matches[]/last_results[]/upcoming[])
|
||||
* $variant '' (Default) | 'hero' (groß, für den Slider)
|
||||
* Aufbau (kicker-Stil): Datum oben mittig, darunter Heim | Ergebnis/Anstoß | Gast,
|
||||
* die Zeit mittig unter dem Ergebnis. Die Heimmannschaft steht immer links — daher
|
||||
* kein zusätzlicher Heim/Auswärts-Hinweis. Die eigene Mannschaft wird hervorgehoben.
|
||||
*/
|
||||
$variant = $variant ?? '';
|
||||
$m = $match;
|
||||
$hasResult = isset($m['goals_home'], $m['goals_away']);
|
||||
$isHome = !empty($m['is_home']);
|
||||
|
||||
$classes = 'match-card';
|
||||
if ($variant !== '') {
|
||||
$classes .= ' match-card--' . preg_replace('/[^a-z0-9-]+/', '', strtolower($variant));
|
||||
}
|
||||
?>
|
||||
<article class="<?= $classes ?>">
|
||||
<?php if (!empty($m['date_display'])): ?>
|
||||
<p class="match-card__date"><?= icon('calendar-event', 'match-card__date-icon') ?><?= e($m['date_display']) ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="match-card__fixture">
|
||||
<div class="match-card__side<?= $isHome ? ' is-own' : '' ?>">
|
||||
<?php component('crest', ['src' => $m['home_crest'] ?? null, 'name' => $m['home'] ?? '']); ?>
|
||||
<span class="match-card__club"><?= e($m['home'] ?? '') ?></span>
|
||||
</div>
|
||||
<div class="match-card__center">
|
||||
<?php if ($hasResult): ?>
|
||||
<span class="match-card__score"><?= (int) $m['goals_home'] ?><span class="match-card__colon">:</span><?= (int) $m['goals_away'] ?></span>
|
||||
<?php else: ?>
|
||||
<span class="match-card__vs" aria-label="gegen">:</span>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($m['time_display'])): ?>
|
||||
<span class="match-card__time"><?= e($m['time_display']) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="match-card__side<?= !$isHome ? ' is-own' : '' ?>">
|
||||
<?php component('crest', ['src' => $m['away_crest'] ?? null, 'name' => $m['away'] ?? '']); ?>
|
||||
<span class="match-card__club"><?= e($m['away'] ?? '') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
70
app/components/match-feature.php
Normal file
70
app/components/match-feature.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Aufmacher des „Nächste Spiele"-Bands: das zeitlich nächste Spiel — strukturgleich
|
||||
* mit match-row (Heim über Gast, linksbündig), nur groß. Ein Bauprinzip für das
|
||||
* ganze Band; die Wirkung kommt aus der Größe, nicht aus Badges oder Icons.
|
||||
* Drei Schriftrollen: Meta-Zeile (Abel, gesperrt) · Vereinsnamen (Coolvetica) ·
|
||||
* Anstoßzeile (Abel). Props:
|
||||
* $match Eintrag aus data/matchcenter.json → upcoming[]
|
||||
* $team_name Name der eigenen Mannschaft („1. Mannschaft")
|
||||
* $league Liganame der Mannschaft (für die Meta-Zeile)
|
||||
* $href Sprungmarke zur Mannschafts-Sektion (optional; macht den
|
||||
* Mannschaftsnamen in der Meta-Zeile zum Link)
|
||||
*/
|
||||
$m = $match;
|
||||
$isHome = !empty($m['is_home']);
|
||||
$teamName = (string) ($team_name ?? '');
|
||||
$league = (string) ($league ?? '');
|
||||
$href = $href ?? null;
|
||||
$when = match_when((string) ($m['kickoff'] ?? ''));
|
||||
|
||||
// Meta-Zeile: Mannschaft · Wettbewerb · Heim/Auswärts (leere Teile fallen weg).
|
||||
$competition = match_competition_label((string) ($m['competition'] ?? ''), $league);
|
||||
$meta = array_values(array_filter([
|
||||
$teamName,
|
||||
$competition,
|
||||
$isHome ? 'Heimspiel' : 'Auswärtsspiel',
|
||||
], static fn (string $part): bool => $part !== ''));
|
||||
|
||||
$datetime = trim(($m['date_display'] ?? '') . ' · ' . ($m['time_display'] ?? ''), ' ·');
|
||||
?>
|
||||
<article class="match-feature">
|
||||
<?php if ($meta !== []): ?>
|
||||
<p class="match-feature__meta">
|
||||
<?php foreach ($meta as $i => $part): ?>
|
||||
<?php if ($i > 0): ?>
|
||||
<span class="match-feature__sep" aria-hidden="true">·</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($i === 0 && $href !== null): ?>
|
||||
<a href="<?= e($href) ?>"><?= e($part) ?></a>
|
||||
<?php else: ?>
|
||||
<span><?= e($part) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<div class="match-feature__fixture">
|
||||
<span class="match-feature__side<?= $isHome ? ' is-own' : '' ?>">
|
||||
<?php component('crest', ['src' => $m['home_crest'] ?? null, 'name' => $m['home'] ?? '']); ?>
|
||||
<span class="match-feature__club"><?= e($m['home'] ?? '') ?></span>
|
||||
</span>
|
||||
<span class="match-feature__side<?= !$isHome ? ' is-own' : '' ?>">
|
||||
<?php component('crest', ['src' => $m['away_crest'] ?? null, 'name' => $m['away'] ?? '']); ?>
|
||||
<span class="match-feature__club"><?= e($m['away'] ?? '') ?></span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="match-feature__when">
|
||||
<?php if ($when['iso'] !== ''): ?>
|
||||
<time datetime="<?= e($when['iso']) ?>"><?= e($datetime) ?></time>
|
||||
<?php else: ?>
|
||||
<span><?= e($datetime) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($when['countdown'] !== ''): ?>
|
||||
<span class="match-feature__sep" aria-hidden="true">·</span>
|
||||
<span class="match-feature__countdown" data-countdown="<?= e($when['iso']) ?>"><?= e($when['countdown']) ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</article>
|
||||
@@ -3,36 +3,60 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Kompakte Spielzeile für die Matchcenter-Seitenspalte (kicker-Stil): Datum oben,
|
||||
* darunter Heim über Gast (Wappen + Name, lange Namen werden mit … gekürzt), rechts
|
||||
* das Ergebnis bzw. — bei anstehenden Spielen — die Anstoßzeit. Heim steht immer
|
||||
* oben/links. Props: $match (Eintrag aus data/matchcenter.json).
|
||||
* Kompakte Spielzeile (kicker-Stil): Datum oben, darunter Heim über Gast (Wappen +
|
||||
* Name, lange Namen werden mit … gekürzt), rechts das Ergebnis bzw. — bei
|
||||
* anstehenden Spielen — die Anstoßzeit. Heim steht immer oben. Props:
|
||||
* $match Eintrag aus data/matchcenter.json (next_matches[]/last_results[]/upcoming[])
|
||||
* $meta optionale Angabe neben dem Datum (im „Nächste Spiele"-Band: die Mannschaft)
|
||||
* $href optionales Ziel — dann ist die ganze Zeile ein Link
|
||||
* Der innere __link-Wrapper trägt Raster und Innenabstand, damit als Link die
|
||||
* komplette Zeilenfläche klickbar ist (und ohne Link nichts anders aussieht).
|
||||
*/
|
||||
$m = $match;
|
||||
$hasResult = isset($m['goals_home'], $m['goals_away']);
|
||||
$isHome = !empty($m['is_home']);
|
||||
$meta = (string) ($meta ?? '');
|
||||
$href = $href ?? null;
|
||||
?>
|
||||
<li class="match-row">
|
||||
<?php if (!empty($m['date_display'])): ?>
|
||||
<span class="match-row__date"><?= e($m['date_display']) ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="match-row__teams">
|
||||
<span class="match-row__team<?= $isHome ? ' is-own' : '' ?>">
|
||||
<?php component('crest', ['src' => $m['home_crest'] ?? null, 'name' => $m['home'] ?? '']); ?>
|
||||
<span class="match-row__name" title="<?= e($m['home'] ?? '') ?>"><?= e($m['home'] ?? '') ?></span>
|
||||
</span>
|
||||
<span class="match-row__team<?= !$isHome ? ' is-own' : '' ?>">
|
||||
<?php component('crest', ['src' => $m['away_crest'] ?? null, 'name' => $m['away'] ?? '']); ?>
|
||||
<span class="match-row__name" title="<?= e($m['away'] ?? '') ?>"><?= e($m['away'] ?? '') ?></span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="match-row__outcome">
|
||||
<?php if ($hasResult): ?>
|
||||
<span class="match-row__score"><?= (int) $m['goals_home'] ?>:<?= (int) $m['goals_away'] ?></span>
|
||||
<?php elseif (!empty($m['time_display'])): ?>
|
||||
<span class="match-row__time"><?= e($m['time_display']) ?></span>
|
||||
<?php if ($href !== null): ?>
|
||||
<a class="match-row__link" href="<?= e($href) ?>">
|
||||
<?php else: ?>
|
||||
<span class="match-row__time" aria-hidden="true">–</span>
|
||||
<div class="match-row__link">
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($m['date_display']) || $meta !== ''): ?>
|
||||
<span class="match-row__head">
|
||||
<?php if (!empty($m['date_display'])): ?>
|
||||
<span class="match-row__date"><?= e($m['date_display']) ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($meta !== ''): ?>
|
||||
<span class="match-row__meta"><?= e($meta) ?></span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<span class="match-row__teams">
|
||||
<span class="match-row__team<?= $isHome ? ' is-own' : '' ?>">
|
||||
<?php component('crest', ['src' => $m['home_crest'] ?? null, 'name' => $m['home'] ?? '']); ?>
|
||||
<span class="match-row__name" title="<?= e($m['home'] ?? '') ?>"><?= e($m['home'] ?? '') ?></span>
|
||||
</span>
|
||||
<span class="match-row__team<?= !$isHome ? ' is-own' : '' ?>">
|
||||
<?php component('crest', ['src' => $m['away_crest'] ?? null, 'name' => $m['away'] ?? '']); ?>
|
||||
<span class="match-row__name" title="<?= e($m['away'] ?? '') ?>"><?= e($m['away'] ?? '') ?></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="match-row__outcome">
|
||||
<?php if ($hasResult): ?>
|
||||
<span class="match-row__score"><?= (int) $m['goals_home'] ?>:<?= (int) $m['goals_away'] ?></span>
|
||||
<?php elseif (!empty($m['time_display'])): ?>
|
||||
<span class="match-row__time"><?= e($m['time_display']) ?></span>
|
||||
<?php else: ?>
|
||||
<span class="match-row__time" aria-hidden="true">–</span>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php if ($href !== null): ?>
|
||||
<span class="visually-hidden">– zur Mannschaft</span>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* „Nächste Spiele"-Slider (teamübergreifend). Props: $matches (data/matchcenter.json
|
||||
* → upcoming[]). Spiegelt die Mechanik des banner-slider (Crossfade, Pause/Play,
|
||||
* prefers-reduced-motion) über den generischen Antrieb assets/js/carousel.js.
|
||||
* Ohne JS ist nur die erste Folie sichtbar. Leer → nichts rendern (Off-Season).
|
||||
*/
|
||||
$matches = $matches ?? [];
|
||||
if ($matches === []) {
|
||||
return;
|
||||
}
|
||||
$total = count($matches);
|
||||
?>
|
||||
<section class="section match-slider" id="naechste-spiele" aria-roledescription="Karussell" aria-label="Nächste Spiele unserer Mannschaften" data-carousel>
|
||||
<div class="container">
|
||||
<h2>Nächste Spiele</h2>
|
||||
<ul class="match-slider__track" id="match-slider-track" data-carousel-track aria-live="off">
|
||||
<?php foreach ($matches as $i => $m): ?>
|
||||
<li class="match-slider__slide" data-carousel-slide role="group" aria-roledescription="Folie" aria-label="<?= ($i + 1) ?> von <?= $total ?>"<?= $i === 0 ? '' : ' hidden' ?>>
|
||||
<p class="match-slider__team"><?= e($m['team_name'] ?? '') ?></p>
|
||||
<?php component('match-card', ['match' => $m, 'variant' => 'hero']); ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php if ($total > 1): ?>
|
||||
<div class="match-slider__controls" data-carousel-controls hidden>
|
||||
<button class="match-slider__toggle" type="button" data-carousel-toggle aria-pressed="false" aria-controls="match-slider-track" hidden>
|
||||
<?= icon('pause-fill', 'match-slider__toggle-icon match-slider__toggle-icon--pause') ?>
|
||||
<?= icon('play-fill', 'match-slider__toggle-icon match-slider__toggle-icon--play') ?>
|
||||
<span class="visually-hidden" data-carousel-toggle-label>Automatischen Wechsel pausieren</span>
|
||||
</button>
|
||||
<div class="match-slider__dots" role="group" aria-label="Spiel wählen">
|
||||
<?php foreach ($matches as $i => $m): ?>
|
||||
<button class="match-slider__dot" type="button" data-carousel-dot aria-label="Spiel <?= ($i + 1) ?> von <?= $total ?>: <?= e(($m['home'] ?? '') . ' gegen ' . ($m['away'] ?? '')) ?>">
|
||||
<span class="match-slider__progress" data-carousel-progress aria-hidden="true"></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
61
app/components/next-matches.php
Normal file
61
app/components/next-matches.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* „Nächste Spiele"-Band (teamübergreifend): abgesetzte dunklere Fläche mit roter
|
||||
* Oberkante, links der Aufmacher (das zeitlich nächste Spiel aller Mannschaften),
|
||||
* rechts die folgenden Termine als kompakte, anklickbare Zeilen. Props:
|
||||
* $matches data/matchcenter.json → upcoming[] (chronologisch)
|
||||
* $teams data/matchcenter.json → teams[] (liefert Liganame + Sprungmarke)
|
||||
* Leer → nichts rendern.
|
||||
*/
|
||||
$matches = $matches ?? [];
|
||||
$teams = $teams ?? [];
|
||||
if ($matches === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Liganame und Sprungmarke je Mannschaft vorbereiten, damit die Karten nicht
|
||||
// selbst in den Teams suchen müssen. Anker wie in matchcenter-section.php.
|
||||
$leagues = [];
|
||||
$anchors = [];
|
||||
foreach ($teams as $t) {
|
||||
$key = (string) ($t['key'] ?? '');
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$leagues[$key] = (string) ($t['league'] ?? '');
|
||||
$anchors[$key] = '#team-' . preg_replace('/[^a-z0-9-]+/', '', strtolower($key));
|
||||
}
|
||||
|
||||
$feature = array_shift($matches);
|
||||
$featureKey = (string) ($feature['team_key'] ?? '');
|
||||
?>
|
||||
<section class="section section--sunken next-matches" id="naechste-spiele">
|
||||
<div class="container">
|
||||
<h2 class="next-matches__title">Nächste Spiele</h2>
|
||||
<div class="next-matches__grid">
|
||||
<?php component('match-feature', [
|
||||
'match' => $feature,
|
||||
'team_name' => $feature['team_name'] ?? '',
|
||||
'league' => $leagues[$featureKey] ?? '',
|
||||
'href' => $anchors[$featureKey] ?? null,
|
||||
]); ?>
|
||||
<?php if ($matches !== []): ?>
|
||||
<div class="next-matches__list">
|
||||
<h3 class="next-matches__list-title">Danach</h3>
|
||||
<ul class="match-list match-list--plain">
|
||||
<?php foreach ($matches as $m): ?>
|
||||
<?php component('match-row', [
|
||||
'match' => $m,
|
||||
'meta' => $m['team_name'] ?? '',
|
||||
'href' => $anchors[(string) ($m['team_key'] ?? '')] ?? null,
|
||||
]); ?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -125,6 +125,61 @@ function img_intrinsic_attrs(string $rel): string
|
||||
return $cache[$rel];
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzeigewerte zu einem Anstoß. $kickoff = Zeit ohne Zonenangabe aus
|
||||
* data/matchcenter.json (z. B. 2026-08-01T14:00), gelesen als Europe/Berlin —
|
||||
* PHP läuft hier global auf UTC, deshalb explizit. Liefert:
|
||||
* iso maschinenlesbar mit Zone (für <time datetime> und den JS-Ticker)
|
||||
* countdown „heute, 14:00 Uhr" | „morgen, 14:00 Uhr" | „in 6 Tagen" | „in 3 Wochen"
|
||||
* Unlesbarer Wert → beide Felder leer; bereits angestoßenes Spiel → countdown leer.
|
||||
*/
|
||||
function match_when(string $kickoff): array
|
||||
{
|
||||
if ($kickoff === '') {
|
||||
return ['iso' => '', 'countdown' => ''];
|
||||
}
|
||||
$tz = new DateTimeZone('Europe/Berlin');
|
||||
try {
|
||||
$start = new DateTimeImmutable($kickoff, $tz);
|
||||
} catch (Exception) {
|
||||
return ['iso' => '', 'countdown' => ''];
|
||||
}
|
||||
$now = new DateTimeImmutable('now', $tz);
|
||||
if ($start <= $now) {
|
||||
return ['iso' => $start->format('c'), 'countdown' => ''];
|
||||
}
|
||||
// Kalendertage zählen, nicht 24-Stunden-Blöcke: ein Anstoß morgen um 14 Uhr
|
||||
// ist „morgen", auch wenn es nur 20 Stunden hin sind.
|
||||
$days = (int) $now->setTime(0, 0)->diff($start->setTime(0, 0))->format('%a');
|
||||
$time = $start->format('H:i') . ' Uhr';
|
||||
return [
|
||||
'iso' => $start->format('c'),
|
||||
'countdown' => match (true) {
|
||||
$days === 0 => 'heute, ' . $time,
|
||||
$days === 1 => 'morgen, ' . $time,
|
||||
$days < 14 => 'in ' . $days . ' Tagen',
|
||||
default => 'in ' . (int) round($days / 7) . ' Wochen',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Anzeigename eines Wettbewerbs aus data/matchcenter.json. Die BFV-API kennt nur
|
||||
* „Liga" und „Freundschaft": „Liga" wird durch den echten Liganamen ersetzt — ohne
|
||||
* $league bleibt sie leer, damit Ligaspiele in den Mannschafts-Sektionen nicht
|
||||
* redundant beschriftet werden (die Sektion nennt die Liga schon). „Freundschaft"
|
||||
* wird ausgeschrieben, damit erkennbar ist, warum ein Testspiel nicht in der
|
||||
* Tabelle zählt.
|
||||
*/
|
||||
function match_competition_label(string $competition, string $league = ''): string
|
||||
{
|
||||
return match ($competition) {
|
||||
'Liga' => $league,
|
||||
'Freundschaft' => 'Freundschaftsspiel',
|
||||
default => $competition,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponente rendern: component('hero', ['title' => …]).
|
||||
* Props werden als lokale Variablen extrahiert; Komponenten sind dumme Includes.
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
/**
|
||||
* Matchcenter: Spielplan, Ergebnisse und Tabellen der Aktivenmannschaften.
|
||||
* Daten: data/matchcenter.json (maschinenverwaltet von bin/matchcenter-sync.php).
|
||||
* Struktur: Hero → „Nächste Spiele"-Slider → pro Mannschaft eine Sektion
|
||||
* Struktur: Hero → „Nächste Spiele"-Band → pro Mannschaft eine Sektion
|
||||
* (Nächste Spiele · Letzte Ergebnisse · Tabelle). Ohne Cache rendert die Seite
|
||||
* einen freundlichen Hinweis statt eines Fehlers (Graceful Degradation).
|
||||
*/
|
||||
@@ -17,7 +17,7 @@ $meta = [
|
||||
'title' => 'Matchcenter – Spiele, Ergebnisse & Tabellen',
|
||||
'description' => 'Spielplan, Ergebnisse und Tabellen der Mannschaften des TSV 08 Kulmbach – 1. und 2. Mannschaft sowie Damen, automatisch aktuell.',
|
||||
'og_image' => 'img/pages/fussball-hero-1600.jpg',
|
||||
'scripts' => ['carousel.js', 'crest.js', 'group-tabs.js'],
|
||||
'scripts' => ['crest.js', 'group-tabs.js'],
|
||||
'schema' => array_merge(
|
||||
page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
@@ -45,7 +45,7 @@ if ($teams === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
component('match-slider', ['matches' => $upcoming]);
|
||||
component('next-matches', ['matches' => $upcoming, 'teams' => $teams]);
|
||||
|
||||
foreach ($teams as $team) {
|
||||
component('matchcenter-section', ['team' => $team]);
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-items: end;
|
||||
/* svh statt vh: stabil gegenüber ein-/ausfahrender Browser-UI auf Phones. */
|
||||
min-height: min(92vh, 900px);
|
||||
min-height: min(92svh, 900px);
|
||||
padding-block: calc(var(--header-h) + 80px) var(--space-section);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -15,9 +17,26 @@
|
||||
/* Kompakte Variante: schlanker Unterseiten-Header statt vollflächig. */
|
||||
.hero--compact {
|
||||
min-height: min(60vh, 560px);
|
||||
min-height: min(60svh, 560px);
|
||||
padding-block: calc(var(--header-h) + 64px) 3.5rem;
|
||||
}
|
||||
|
||||
/* Mobile: Höhe am Inhalt statt am Viewport orientieren — weniger Leer-Scroll.
|
||||
Startseiten-Hero lässt bewusst einen Streifen Folge-Inhalt anblitzen (Scroll-Affordance),
|
||||
Unterseiten-Heros kollabieren auf Überschrift + Teaser. */
|
||||
@media (max-width: 768px) {
|
||||
.hero {
|
||||
min-height: min(82vh, 700px);
|
||||
min-height: min(82svh, 700px);
|
||||
padding-block: calc(var(--header-h) + 48px) 3rem;
|
||||
}
|
||||
|
||||
.hero--compact {
|
||||
min-height: 0;
|
||||
padding-block: calc(var(--header-h) + 44px) 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hero__media {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -78,6 +97,13 @@
|
||||
margin-top: 1rem;
|
||||
max-width: 60ch;
|
||||
text-shadow: 0 1px 12px rgb(0 0 0 / 0.6);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
/* Display-Hero: schmaleres Maß fürs Zentrieren — ausgewogene Zeilen statt
|
||||
vier Zeilen mit unruhigem Flatterrand. */
|
||||
.hero--display .hero__text {
|
||||
max-width: 52ch;
|
||||
}
|
||||
|
||||
.hero__actions {
|
||||
@@ -103,10 +129,6 @@
|
||||
text-indent: 0.14em;
|
||||
}
|
||||
|
||||
.hero--center.hero--display .hero__tagline {
|
||||
text-indent: 0.1em;
|
||||
}
|
||||
|
||||
.hero--center .hero__actions {
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -126,47 +148,35 @@
|
||||
linear-gradient(180deg, rgb(0 0 0 / 0.4) 0%, rgb(0 0 0 / 0.14) 45%, rgb(0 0 0 / 0.5) 100%);
|
||||
}
|
||||
|
||||
/* Kicker: einziges gesperrtes Versalien-Element im Hero — bewusst ohne
|
||||
Schmuck-Linien, damit die Typo-Hierarchie ruhig bleibt (Eyebrow → Titel →
|
||||
Subline → Text). */
|
||||
.hero__kicker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: var(--fs-500);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--clr-text);
|
||||
}
|
||||
|
||||
.hero__kicker::before {
|
||||
content: '';
|
||||
width: 3rem;
|
||||
height: 4px;
|
||||
background: var(--clr-accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Kicker zentriert: Text mittig, Akzentlinie beidseitig (— Text —) */
|
||||
.hero--center .hero__kicker {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero--center .hero__kicker::after {
|
||||
content: '';
|
||||
width: 3rem;
|
||||
height: 4px;
|
||||
background: var(--clr-accent);
|
||||
border-radius: 2px;
|
||||
/* Sperrung optisch kompensieren (Spacing nach letztem Zeichen). */
|
||||
text-indent: 0.18em;
|
||||
color: rgb(255 255 255 / 0.85);
|
||||
}
|
||||
|
||||
.hero--display .hero__title {
|
||||
font-size: clamp(3.5rem, 2rem + 7.5vw, 7.5rem);
|
||||
}
|
||||
|
||||
/* Subline ruhig in Abel, Groß-/Kleinschreibung — konkurriert nicht mehr mit
|
||||
Kicker (gesperrte Versalien) und Coolvetica-Titel. */
|
||||
.hero--display .hero__tagline {
|
||||
margin-top: 0.5rem;
|
||||
font-size: clamp(1.75rem, 1rem + 2.5vw, 2.75rem);
|
||||
letter-spacing: 0.1em;
|
||||
color: rgb(255 255 255 / 0.88);
|
||||
margin-top: 0.75rem;
|
||||
font-family: var(--ff-body);
|
||||
font-size: clamp(1.25rem, 0.95rem + 1.3vw, 1.75rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
text-indent: 0;
|
||||
text-transform: none;
|
||||
line-height: 1.35;
|
||||
color: rgb(255 255 255 / 0.92);
|
||||
}
|
||||
|
||||
.hero__scroll-cue {
|
||||
@@ -212,10 +222,26 @@
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/* Mobil: Ein-Reihen-Swiper statt 2er-Grid — volle Bildgröße, natürliche
|
||||
Instagram-Geste. Fluchtet mit dem Text-Container; die angeschnittene
|
||||
Folgekarte ist die Wisch-Affordance, Scrollbar bleibt darum versteckt. */
|
||||
@media (max-width: 600px) {
|
||||
.instagram__grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: none;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: min(72vw, 320px);
|
||||
gap: 0.75rem;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.instagram__grid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.instagram__link {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +314,13 @@
|
||||
border-block: 1px solid rgb(255 255 255 / 0.1);
|
||||
}
|
||||
|
||||
/* Abgesetztes, dunkleres Band über die volle Breite — das Gegenstück zu
|
||||
--tinted, wenn eine Sektion sich nach unten statt nach oben abheben soll. */
|
||||
.section--sunken {
|
||||
background: var(--clr-bg-deep);
|
||||
border-block: 1px solid rgb(255 255 255 / 0.08);
|
||||
}
|
||||
|
||||
.section--tinted + .section--tinted {
|
||||
border-top: 0;
|
||||
}
|
||||
@@ -3623,267 +3656,104 @@
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Einzelne Begegnung. */
|
||||
.match-card {
|
||||
/* Aufmacher des „Nächste Spiele"-Bands: strukturgleich mit .match-row (Heim über
|
||||
Gast, linksbündig), nur groß — ein Bauprinzip für das ganze Band. Bewusst ohne
|
||||
eigene Fläche, ohne Badges und ohne Icons: die Wirkung kommt aus der Größe.
|
||||
Drei Schriftrollen — Meta (Abel gesperrt) · Namen (Coolvetica) · Anstoß (Abel). */
|
||||
.match-feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
padding: clamp(1rem, 0.8rem + 0.8vw, 1.4rem);
|
||||
background: var(--clr-glass-bg);
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
border-radius: var(--radius-card);
|
||||
gap: clamp(1rem, 0.8rem + 0.6vw, 1.5rem);
|
||||
}
|
||||
|
||||
/* Datum oben mittig (kicker-Stil). */
|
||||
.match-card__date {
|
||||
.match-feature__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4em;
|
||||
font-size: var(--fs-300);
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
font-size: var(--fs-400);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.match-card__date-icon {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.match-card__fixture {
|
||||
display: grid;
|
||||
/* minmax(0, …) statt 1fr: erlaubt den Spalten, schmaler als ihr Inhalt zu
|
||||
werden (sonst überläuft die Karte auf sehr schmalen Screens). */
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.match-card__side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0; /* verhindert Grid-Blowout der 1fr-Spalten bei langen Namen */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.match-card__club {
|
||||
font-size: var(--fs-400);
|
||||
line-height: 1.2;
|
||||
overflow-wrap: anywhere; /* lange Namen brechen um statt zu überlaufen */
|
||||
}
|
||||
|
||||
.match-card__side.is-own .match-card__club {
|
||||
font-weight: var(--fw-bold);
|
||||
color: var(--clr-text);
|
||||
}
|
||||
|
||||
.match-card__center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 3.25rem;
|
||||
}
|
||||
|
||||
.match-card__score {
|
||||
font-family: var(--ff-heading);
|
||||
font-size: var(--fs-700);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.match-card__colon {
|
||||
margin: 0 0.12em;
|
||||
}
|
||||
|
||||
.match-card__vs {
|
||||
font-family: var(--ff-heading);
|
||||
font-size: var(--fs-650);
|
||||
.match-feature__sep {
|
||||
color: var(--clr-text-faint);
|
||||
}
|
||||
|
||||
/* Zeit mittig unter dem Ergebnis/Anstoß (kicker-Stil). */
|
||||
.match-card__time {
|
||||
margin-top: 0.4rem;
|
||||
font-size: var(--fs-400);
|
||||
color: var(--clr-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Hero-Variante (im Slider): größer und ruhiger. */
|
||||
.match-card--hero {
|
||||
padding: clamp(1.5rem, 1rem + 2vw, 2.5rem);
|
||||
}
|
||||
|
||||
.match-card--hero .match-card__score {
|
||||
font-size: var(--fs-800);
|
||||
}
|
||||
|
||||
.match-card--hero .crest,
|
||||
.match-card--hero .crest--initials {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.match-card--hero .match-card__club {
|
||||
font-size: var(--fs-500);
|
||||
}
|
||||
|
||||
/* „Nächste Spiele"-Slider — Mechanik wie .ad-banner (gemeinsamer Antrieb
|
||||
carousel.js, gemeinsame Keyframe ad-progress). */
|
||||
.match-slider {
|
||||
--slider-interval: 6s;
|
||||
}
|
||||
|
||||
.match-slider > .container > h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.match-slider__track {
|
||||
list-style: none;
|
||||
margin: 2rem auto 0;
|
||||
padding: 0;
|
||||
max-width: 620px;
|
||||
}
|
||||
|
||||
.match-slider__slide[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__team {
|
||||
margin-bottom: 0.85rem;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: var(--fs-300);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
/* Crossfade nur mit JS: Slides im selben Grid-Feld stapeln (konstante Höhe). */
|
||||
.match-slider.is-enhanced .match-slider__track {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr); /* gestapelte Slides nie breiter als der Track */
|
||||
}
|
||||
|
||||
.match-slider.is-enhanced .match-slider__slide {
|
||||
grid-area: 1 / 1;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.55s ease, transform 0.55s ease, visibility 0s linear 0.55s;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.match-slider.is-enhanced .match-slider__slide.is-active {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
pointer-events: auto;
|
||||
visibility: visible;
|
||||
transition: opacity 0.55s ease, transform 0.55s ease, visibility 0s;
|
||||
}
|
||||
|
||||
.match-slider__controls {
|
||||
.match-feature__fixture {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
.match-slider__controls[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__toggle {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
color: var(--clr-text);
|
||||
background: var(--clr-glass-bg);
|
||||
border: 1px solid var(--clr-glass-border);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.match-slider__toggle[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__toggle:hover {
|
||||
background: rgb(255 255 255 / 0.2);
|
||||
}
|
||||
|
||||
.match-slider__toggle:focus-visible {
|
||||
outline: 2px solid var(--clr-text);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.match-slider__toggle-icon {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.match-slider__toggle .match-slider__toggle-icon--play {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__toggle[aria-pressed="true"] .match-slider__toggle-icon--pause {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.match-slider__toggle[aria-pressed="true"] .match-slider__toggle-icon--play {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.match-slider__dots {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.match-slider__dot {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 6px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--clr-glass-border);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: background var(--transition);
|
||||
.match-feature__side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(0.6rem, 0.4rem + 0.6vw, 1rem);
|
||||
min-width: 0; /* lange Vereinsnamen dürfen umbrechen statt zu überlaufen */
|
||||
}
|
||||
|
||||
.match-slider__dot:hover {
|
||||
background: rgb(255 255 255 / 0.35);
|
||||
.match-feature .crest,
|
||||
.match-feature .crest--initials {
|
||||
width: clamp(2.25rem, 1.6rem + 2vw, 3rem);
|
||||
height: clamp(2.25rem, 1.6rem + 2vw, 3rem);
|
||||
font-size: var(--fs-400);
|
||||
}
|
||||
|
||||
.match-slider__dot:focus-visible {
|
||||
outline: 2px solid var(--clr-text);
|
||||
outline-offset: 3px;
|
||||
/* Die Vereinsnamen sind das einzige Coolvetica-Element („wer"), alles Beschreibende
|
||||
bleibt Abel („wann/was"). Coolvetica hat nur einen Schnitt — die eigene Mannschaft
|
||||
wird deshalb über Helligkeit markiert, nicht über fett (kein Fake-Bold). */
|
||||
.match-feature__club {
|
||||
font-family: var(--ff-heading);
|
||||
font-size: var(--fs-700);
|
||||
line-height: var(--lh-heading);
|
||||
text-transform: uppercase;
|
||||
color: var(--clr-text-muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.match-slider__progress {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--clr-accent);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
.match-feature__side.is-own .match-feature__club {
|
||||
color: var(--clr-text);
|
||||
}
|
||||
|
||||
.match-slider__dot.is-active .match-slider__progress {
|
||||
animation: ad-progress var(--slider-interval) linear forwards;
|
||||
.match-feature__when {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
font-size: var(--fs-600);
|
||||
}
|
||||
|
||||
.match-slider.is-paused .match-slider__dot.is-active .match-slider__progress {
|
||||
animation-play-state: paused;
|
||||
|
||||
/* Band „Nächste Spiele" — abgesetzte Fläche (.section--sunken) mit roter Oberkante,
|
||||
damit der Bereich nicht als Hero-Überhang gelesen wird. */
|
||||
.next-matches {
|
||||
border-top: 2px solid var(--clr-accent);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.match-slider__dot.is-active .match-slider__progress {
|
||||
animation: none;
|
||||
transform: scaleX(1);
|
||||
.next-matches__grid {
|
||||
display: grid;
|
||||
/* Der Aufmacher trägt mehr Gewicht als die Terminliste. */
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: clamp(1.25rem, 1rem + 1.5vw, 2.5rem);
|
||||
margin-top: clamp(1.5rem, 1.2rem + 1vw, 2.25rem);
|
||||
}
|
||||
|
||||
.next-matches__list-title {
|
||||
margin-bottom: 0.75rem;
|
||||
/* Coolvetica wäre in dieser Größe unleserlich — Abel mit Sperrung (CLAUDE.md). */
|
||||
font-family: var(--ff-body);
|
||||
font-size: var(--fs-400);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.next-matches__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3967,12 +3837,23 @@
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
/* Im „Nächste Spiele"-Band trägt die Terminliste keine Kartenfläche — sonst stünde
|
||||
ein gerahmter Block neben dem flächenlosen Aufmacher. Die Trennlinien der Zeilen
|
||||
reichen; in den Mannschafts-Sektionen bleibt .match-list die Karte.
|
||||
Zwei Klassen nötig: die Glas-Registry am Dateiende setzt .match-list den
|
||||
box-shadow und würde einen Ein-Klassen-Selektor sonst überschreiben. */
|
||||
.match-list.match-list--plain {
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.match-list--plain .match-row__link {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.match-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
padding: 0.7rem 0.9rem;
|
||||
border-bottom: 1px solid var(--clr-glass-border);
|
||||
}
|
||||
|
||||
@@ -3980,13 +3861,43 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.match-row__date {
|
||||
/* Raster und Innenabstand liegen im Wrapper: ist die Zeile ein Link, ist damit
|
||||
die komplette Zeilenfläche klickbar — ohne Link sieht nichts anders aus. */
|
||||
.match-row__link {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
column-gap: 0.75rem;
|
||||
padding: 0.7rem 0.9rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.match-row__link:hover {
|
||||
background: rgb(255 255 255 / 0.06);
|
||||
}
|
||||
|
||||
.match-row__head {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
.match-row__date {
|
||||
font-size: var(--fs-300);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.match-row__meta {
|
||||
font-size: var(--fs-300);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--clr-text-faint);
|
||||
}
|
||||
|
||||
.match-row__teams {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -4045,6 +3956,25 @@
|
||||
border-radius: var(--radius-card);
|
||||
}
|
||||
|
||||
/* Ruhender Rahmen um den Scroller: trägt den Wisch-Hinweis (Fade am rechten
|
||||
Rand), der nicht mitscrollen darf. Nur auf Breiten sichtbar, wo die Tabelle
|
||||
(min-width 520px) tatsächlich überläuft. */
|
||||
.table-scroll-frame {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.table-scroll-frame::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 1px 1px 1px auto;
|
||||
width: 32px;
|
||||
border-radius: 0 var(--radius-card) var(--radius-card) 0;
|
||||
background: linear-gradient(90deg, transparent, rgb(0 0 0 / 0.55));
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.table-scroll:focus-visible {
|
||||
outline: 2px solid var(--clr-text);
|
||||
outline-offset: 2px;
|
||||
@@ -4386,7 +4316,6 @@
|
||||
.group-card,
|
||||
.group-tabs__panel,
|
||||
.age-group,
|
||||
.match-card,
|
||||
.match-list,
|
||||
.matchcenter-empty,
|
||||
.roster-card,
|
||||
@@ -4413,8 +4342,7 @@
|
||||
.key-facts__item,
|
||||
.bento__tile,
|
||||
.group-card,
|
||||
.match-card,
|
||||
.matchcenter-empty,
|
||||
.matchcenter-empty,
|
||||
.roster-card,
|
||||
.team-card,
|
||||
.department-choice__card,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
--clr-sepia-bg: #f3efe4; /* Timeline-Museumsrahmen (Foto-Mount) */
|
||||
--clr-sepia-text: #3a372f; /* Timeline-Text auf hellem Sepia-Grund */
|
||||
--clr-glass-bg: rgb(0 0 0 / 0.22); /* dunkles Glas (dunkler als der Grund) */
|
||||
--clr-surface-raised: rgb(255 255 255 / 0.055); /* Karte, die auf einem abgesetzten (dunkleren) Band liegt */
|
||||
--clr-glass-border: rgb(255 255 255 / 0.14); /* sehr dezenter Card-Rand — Glow/Sheen tragen die Kontur */
|
||||
--clr-glass-border-strong: rgb(255 255 255 / 0.45); /* Back-to-Top, position-/group-card etc.: ≥3:1 (WCAG 1.4.11) */
|
||||
--clr-field-bg: rgb(255 255 255 / 0.07); /* Formularfelder: sanfte Füllfläche (etwas heller als das Glas) */
|
||||
@@ -37,8 +38,10 @@
|
||||
--fs-650: 1.75rem; /* 28px Zwischengröße (z.B. Footer-Headings) */
|
||||
--fs-675: 2.125rem; /* 34px Header-Wortmarke */
|
||||
--fs-700: clamp(2rem, 1.2rem + 3.5vw, 3.125rem); /* 50px h3 */
|
||||
--fs-800: clamp(2.25rem, 1.3rem + 4.5vw, 3.75rem); /* 60px h2 */
|
||||
--fs-900: clamp(3rem, 1.75rem + 5.5vw, 6rem); /* 96px h1 */
|
||||
/* Mobil-Minima bewusst hoch: Coolvetica Condensed wirkt bei gleicher
|
||||
nomineller Größe deutlich kleiner als normale Schnitte. */
|
||||
--fs-800: clamp(2.625rem, 1.3rem + 4.5vw, 3.75rem); /* 42–60px h2 */
|
||||
--fs-900: clamp(3.5rem, 1.75rem + 5.5vw, 6rem); /* 56–96px h1 */
|
||||
--lh-heading: 0.9;
|
||||
--lh-body: 1.5;
|
||||
--fw-regular: 400;
|
||||
|
||||
Reference in New Issue
Block a user