Banner-Slider: Crossfade-Übergang + per-Slide-Akzentfarbe

Folienwechsel als sauberes Crossfade (best practice): Slides im selben
Grid-Feld gestapelt → konstante Höhe ohne Sprung, ein-/ausgehende Folie
blendet über (Opacity + leichtes translateY). JS steuert die Sichtbarkeit
über .is-active statt hidden; ohne JS bleibt der hidden-Fallback (erste Folie).
Reduced-Motion über den globalen Kill-Switch instant.

Neue Slide-Prop "accent" (CSS-Farbe) überschreibt das Vereins-Rot der Buttons
pro Slide; Breadcrumb-Slide nutzt #6cbbbc. Button-Text wird aus der Akzentfarbe
dunkel abgeleitet (color-mix), da helle Akzente mit Weiß zu wenig Kontrast haben.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDQZ6FuchNUh2bxSW9PeNv
This commit is contained in:
2026-06-19 08:40:35 +02:00
parent f294f1a9de
commit ede0258cd8
4 changed files with 143 additions and 24 deletions

View File

@@ -9,7 +9,8 @@ declare(strict_types=1);
* "tag" (Badge-Text, Default "Anzeige"), "background" (Bild), "cta.internal" * "tag" (Badge-Text, Default "Anzeige"), "background" (Bild), "cta.internal"
* (interner Link ohne target=_blank/rel=sponsored, für eigene Seiten/Anker), * (interner Link ohne target=_blank/rel=sponsored, für eigene Seiten/Anker),
* "cta_secondary" (zweiter Button als Outline), "logo_light" (helle Logo-Platte * "cta_secondary" (zweiter Button als Outline), "logo_light" (helle Logo-Platte
* für dunkle Firmenlogos). * für dunkle Firmenlogos), "accent" (CSS-Farbe; überschreibt das Vereins-Rot
* der Buttons für diese Slide, z. B. Sponsor-CI).
* Auto-Rotation + Fortschritts-Indikatoren übernimmt assets/js/banner.js * Auto-Rotation + Fortschritts-Indikatoren übernimmt assets/js/banner.js
* (pausiert bei Hover/Fokus, respektiert prefers-reduced-motion). * (pausiert bei Hover/Fokus, respektiert prefers-reduced-motion).
* Ohne JS: nur der erste Banner ist sichtbar (Graceful Degradation). * Ohne JS: nur der erste Banner ist sichtbar (Graceful Degradation).
@@ -35,9 +36,32 @@ $total = count($banners);
<?php endif; ?> <?php endif; ?>
<ul class="ad-banner__track" id="ad-banner-track" aria-live="off"> <ul class="ad-banner__track" id="ad-banner-track" aria-live="off">
<?php foreach ($banners as $i => $banner): ?> <?php foreach ($banners as $i => $banner): ?>
<?php $bg = $banner['background'] ?? null; ?> <?php
<?php $variant = $banner['variant'] ?? null; ?> $bg = $banner['background'] ?? null;
<li class="ad-banner__slide<?= $bg ? ' ad-banner__slide--bg' : '' ?><?= $variant ? ' ad-banner__slide--' . e($variant) : '' ?>"<?= $bg ? ' style="--banner-bg: url(' . e(asset($bg)) . ')"' : '' ?> data-banner-slide role="group" aria-roledescription="Folie" aria-label="<?= ($i + 1) ?> von <?= $total ?>"<?= $i === 0 ? '' : ' hidden' ?>> $variant = $banner['variant'] ?? null;
$accent = $banner['accent'] ?? null;
$classes = 'ad-banner__slide';
if ($bg) {
$classes .= ' ad-banner__slide--bg';
}
if ($accent) {
$classes .= ' ad-banner__slide--accent';
}
if ($variant) {
$classes .= ' ad-banner__slide--' . preg_replace('/[^a-z0-9-]+/', '', strtolower($variant));
}
$styleParts = [];
if ($bg) {
$styleParts[] = '--banner-bg: url(' . asset($bg) . ')';
}
if ($accent) {
$styleParts[] = '--banner-accent: ' . $accent;
}
$style = $styleParts !== [] ? ' style="' . e(implode('; ', $styleParts)) . '"' : '';
?>
<li class="<?= e($classes) ?>"<?= $style ?> data-banner-slide role="group" aria-roledescription="Folie" aria-label="<?= ($i + 1) ?> von <?= $total ?>"<?= $i === 0 ? '' : ' hidden' ?>>
<div class="ad-banner__content"> <div class="ad-banner__content">
<span class="ad-banner__tag"><?= e($banner['tag'] ?? 'Anzeige') ?></span> <span class="ad-banner__tag"><?= e($banner['tag'] ?? 'Anzeige') ?></span>
<p class="ad-banner__title"><?= e($banner['title']) ?></p> <p class="ad-banner__title"><?= e($banner['title']) ?></p>

View File

@@ -18,6 +18,7 @@
"text": "Beratung, Software & Implementierung aus einer Hand. Wir entwickeln Intranets, Kundenportale, Apps und Individualsoftware inklusive Einführung, sodass sie im Alltag tatsächlich ankommt.", "text": "Beratung, Software & Implementierung aus einer Hand. Wir entwickeln Intranets, Kundenportale, Apps und Individualsoftware inklusive Einführung, sodass sie im Alltag tatsächlich ankommt.",
"logo": "img/banners/breadcrumb-solutions.svg", "logo": "img/banners/breadcrumb-solutions.svg",
"logo_light": true, "logo_light": true,
"accent": "#6cbbbc",
"background": "img/banners/breadcrumb-hero-1600.jpg", "background": "img/banners/breadcrumb-hero-1600.jpg",
"cta": { "label": "Lösungen entdecken", "url": "https://www.breadcrumb-solutions.de/bc/de/loesungen/" }, "cta": { "label": "Lösungen entdecken", "url": "https://www.breadcrumb-solutions.de/bc/de/loesungen/" },
"cta_secondary": { "label": "Über uns", "url": "https://www.breadcrumb-solutions.de/bc/de/unternehmen/ueber-uns/" } "cta_secondary": { "label": "Über uns", "url": "https://www.breadcrumb-solutions.de/bc/de/unternehmen/ueber-uns/" }

View File

@@ -907,6 +907,32 @@
box-shadow: var(--shadow-card); box-shadow: var(--shadow-card);
} }
/* Crossfade-Übergang (nur mit JS): Slides im selben Grid-Feld stapeln, sodass
die Höhe konstant bleibt (kein Sprung) und ein- wie ausgehende Folie sauber
überblenden. Die ausgehende driftet leicht nach unten, die eingehende steigt
auf. Ohne JS greift weiter .ad-banner__slide[hidden] (nur erste Folie).
Reduced-Motion: globaler Kill-Switch in reset.css macht den Wechsel instant. */
.ad-banner.is-enhanced .ad-banner__track {
display: grid;
}
.ad-banner.is-enhanced .ad-banner__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;
}
.ad-banner.is-enhanced .ad-banner__slide.is-active {
opacity: 1;
transform: none;
pointer-events: auto;
visibility: visible;
transition: opacity 0.55s ease, transform 0.55s ease, visibility 0s;
}
/* Optionaler Hintergrund pro Banner (data: "background"). /* Optionaler Hintergrund pro Banner (data: "background").
Dunkler Verlauf über dem Bild hält Text & CTA links lesbar. */ Dunkler Verlauf über dem Bild hält Text & CTA links lesbar. */
.ad-banner__slide--bg { .ad-banner__slide--bg {
@@ -918,6 +944,30 @@
border-color: transparent; border-color: transparent;
} }
/* Pro-Slide-Akzentfarbe (data: "accent") — z. B. Sponsor-CI statt Vereins-Rot.
--banner-accent kommt als Inline-Variable von der Komponente. */
.ad-banner__slide--accent {
--clr-accent: var(--banner-accent);
--clr-accent-dark: color-mix(in srgb, var(--banner-accent) 82%, #000);
}
/* Solider Button: dunkler, aus der Akzentfarbe abgeleiteter Text — helle
Akzentfarben (z. B. Teal) hätten mit Weiß zu wenig Kontrast (WCAG 1.4.3). */
.ad-banner__slide--accent .ad-banner__cta.btn:not(.btn--outline) {
color: color-mix(in srgb, var(--banner-accent) 22%, #000);
}
.ad-banner__slide--accent .ad-banner__cta.btn--outline {
color: var(--banner-accent);
border-color: var(--banner-accent);
}
.ad-banner__slide--accent .ad-banner__cta.btn--outline:hover {
background: color-mix(in srgb, var(--banner-accent) 16%, transparent);
border-color: var(--banner-accent);
color: var(--banner-accent);
}
.ad-banner__slide--bg .ad-banner__text { .ad-banner__slide--bg .ad-banner__text {
color: var(--clr-text); color: var(--clr-text);
} }
@@ -2343,7 +2393,7 @@
/* === membership (Mitglied werden) === */ /* === membership (Mitglied werden) === */
.membership-signup__inner { .membership-signup__inner {
max-width: 820px; max-width: 720px;
margin-inline: auto; margin-inline: auto;
} }
@@ -2359,43 +2409,78 @@
color: var(--clr-text-muted); color: var(--clr-text-muted);
} }
/* Beitragstabelle und Formular mittig, schlanke Spaltenbreite für Lesbarkeit */ /* Eine einzige Karte: Beitragsinfo + Formular zusammen, durch Hairline getrennt. */
.membership-signup .fee-table { .membership-card {
max-width: 560px; padding: clamp(1.5rem, 1rem + 2.5vw, 2.5rem);
margin-inline: auto; background: var(--clr-glass-bg);
border: 1px solid var(--clr-glass-border);
border-radius: var(--radius-card);
} }
.membership-signup .form { /* Verschachtelte Bausteine verlieren ihre eigene Karten-Optik (eine Karte, ein Rahmen). */
margin-inline: auto; .membership-card .fee-table,
.membership-card .form {
max-width: none;
margin: 0;
padding: 0;
background: none;
border: 0;
border-radius: 0;
} }
.membership-signup .fee-table + .form { .membership-card .fee-table {
margin-top: 2rem; padding-bottom: 1.75rem;
margin-bottom: 1.75rem;
border-bottom: 1px solid var(--clr-glass-border);
} }
/* Turnen-Hinweise: ruhige Liste, Icon links, dezente Hairline als Trenner. */
.membership-notes { .membership-notes {
display: grid;
gap: 1.5rem;
margin: 0 0 2.5rem; margin: 0 0 2.5rem;
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
@media (min-width: 768px) { .membership-notes__item {
.membership-notes { display: flex;
grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem;
padding: 1.25rem 0;
border-bottom: 1px solid var(--clr-glass-border);
} }
.membership-notes__item:first-child {
padding-top: 0;
}
.membership-notes__item:last-child {
padding-bottom: 0;
border-bottom: 0;
}
.membership-notes__icon {
flex: none;
margin-top: 0.15rem;
font-size: 1.5rem;
line-height: 1;
color: var(--clr-accent);
} }
.membership-notes__title { .membership-notes__title {
font-size: var(--fs-650); font-size: var(--fs-650);
line-height: var(--lh-heading); line-height: var(--lh-heading);
margin-bottom: 0.4rem; margin-bottom: 0.25rem;
padding-top: 0.75rem;
border-top: 2px solid var(--clr-accent);
} }
.membership-notes__body p {
color: var(--clr-text-muted);
}
/* Download-Block: alles sauber mittig, Texte in lesbarer Breite. */
.membership-download { .membership-download {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
text-align: center; text-align: center;
} }
@@ -2411,7 +2496,8 @@
.membership-download__hint, .membership-download__hint,
.membership-download__contact { .membership-download__contact {
margin-top: 0.75rem; margin: 0;
max-width: 48ch;
font-size: var(--fs-400); font-size: var(--fs-400);
color: var(--clr-text-muted); color: var(--clr-text-muted);
} }

View File

@@ -42,7 +42,8 @@
function activate(i) { function activate(i) {
index = (i % slides.length + slides.length) % slides.length; index = (i % slides.length + slides.length) % slides.length;
slides.forEach(function (slide, n) { slides.forEach(function (slide, n) {
slide.hidden = n !== index; // Sichtbarkeit per Klasse → CSS blendet über (siehe .is-enhanced).
slide.classList.toggle('is-active', n === index);
}); });
dots.forEach(function (dot, n) { dots.forEach(function (dot, n) {
var on = n === index; var on = n === index;
@@ -110,6 +111,13 @@
}); });
} }
// Crossfade aktivieren: Slides stapeln (CSS) und Sichtbarkeit per Klasse
// steuern. Vorher gesetzte hidden-Attribute (No-JS-Fallback) entfernen.
root.classList.add('is-enhanced');
slides.forEach(function (slide) {
slide.hidden = false;
});
activate(0); activate(0);
syncPaused(); syncPaused();
})(); })();