Design rework

pull/6/head
Justin Muravjev 2026-09-29 13:47:23 +02:00
parent 9066763ec6
commit 8b906202b7
11 changed files with 898 additions and 10622 deletions

View File

@ -1,4 +1,35 @@
# PM_Project
# PM_Project – vollständige TriHub-Ernährungsseite
Project for ProjectManagement in IB.4
`ernaehrungsberatung.html` im Browser öffnen. Kein Build und keine Installation nötig.
Die vollständige Seite aus TriHub-Design-Original ist integriert: Header, Desktop-/Mobilnavigation,
Hero, Zielgruppenleiste, eigener Vorteilsbereich, drei Pakete, Coaching-Rabatte,
zentrierter Detaildialog, vier Ablaufschritte und Footer.
Animationen: Scroll-Einblendungen mit 720 ms und maximal 240 ms Staffelung, Bild-/Karten-Hover,
sanfte Ankerlinks und Dialog-/Backdrop-Einblendung. `prefers-reduced-motion` wird berücksichtigt.
Inhalte bleiben ohne JavaScript sichtbar; Menü und Dialog benötigen JavaScript.
## Dateien und Zuständigkeiten
- ernaehrungsberatung.html: vollständige Seitenstruktur und Inhalte.
- css/assets.css: Original-CSS der Vorlage mit lokalen Schriftpfaden.
- css/base.css: Grundlagen, Header, Buttons, Footer.
- css/sections/hero.css: Hero, Zielgruppenleiste, Vorteile, gemeinsame Sektionsüberschriften.
- css/sections/angebote.css: Angebote, Rabattbanner und Detaildialog.
- css/sections/ablauf.css: Programmablauf.
- css/responsive.css (neu): responsive Anpassungen und Dialoganimationen; zuletzt eingebunden.
- js/angebote.js: Paketdetails und Rabattauswahl.
- js/main.js: Mobilnavigation und Scrollanimationen.
- assets/fonts/ (neu): 16 lokale Originalschriftdateien für Sora und DM Sans.
- assets/images/: Bilder in den vorhandenen Unterordnern angebote/, hero/ und icons/.
Bestehende Git- und IDE-Dateien wurden nicht geändert. Es wurde kein Commit oder Push ausgeführt.
Keine zusätzlichen index.html-/projekt.html-Kopien: Der Einstieg bleibt ernaehrungsberatung.html.
## Abgleich
HTML-Inhalte und Reihenfolge entsprechen der vollständigen Vorlage. CSS-Regeln wurden auf die
Projektdateien verteilt, JavaScript wurde in Navigation/Animationen und Angebotsdialog aufgeteilt.
Geprüft: vollständige HTML-Struktur und Texte gegen die Vorlage, lokale Dateiverweise, JavaScript-Syntax, responsive CSS sowie DOM-Interaktionen für alle Pakete, Rabatte, Menü und 22 Animationselemente einschließlich reduzierter Bewegung.
Eine visuelle Browserprüfung ist nicht Bestandteil dieser Prüfung.
Angebote bleiben Konzeptvorschläge ohne Buchung, Datenübermittlung oder erfundene Preise.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,95 +1,179 @@
/* =========================================================
base.css – gemeinsame Basis für alle Features
Änderungen hier nur über eigenen Branch (betrifft alle!)
========================================================= */
/* ---------- Design-Tokens laut tri-hub Design-DNA ---------- */
/* Gemeinsame Basis, Header, Buttons und Footer aus TriHub Design Original. */
/* 1. Grundlagen und Typografie */
:root {
/* Hintergrund: tiefes Dunkelblau / Schwarz */
--th-bg: #0d0d1a;
--th-bg-elevated: #1a1a2e;
/* Akzent: Teal / Emerald Green – CTAs, Icons, Highlights */
--th-accent: #10b981;
/* Text: Weiß / Hellgrau */
--th-text: #ffffff;
--th-text-soft: #f8fafc;
--th-text-muted: #94a3b8;
/* Typografie: Serif Display + Sans-Serif Body */
--th-font-display: "Playfair Display", Georgia, serif;
--th-font-body: "Inter", system-ui, sans-serif;
/* Lesbarkeit Zielgruppe 50+ (Definition of Done) */
--th-font-size-body: 1.125rem; /* 18px */
--th-line-height: 1.65;
/* Abstände / Radien für alle Sektionen */
--th-section-space: 5rem;
--th-radius: 1.4rem;
--page: var(--background);
--text: #fff;
--muted: rgb(255 255 255 / 70%);
--quiet: rgb(255 255 255 / 56%);
--line: rgb(255 255 255 / 10%);
--panel: linear-gradient(#ffffff14, #ffffff08);
--ease: cubic-bezier(.22, 1, .36, 1);
color-scheme: dark;
}
/* ---------- Grundlayout ---------- */
* { box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 120px; }
body {
background: var(--th-bg);
color: var(--th-text-soft);
font-family: var(--th-font-body);
font-size: var(--th-font-size-body);
line-height: var(--th-line-height);
margin: 0;
background: var(--background);
color: var(--text);
font-family: var(--font-sans), sans-serif;
font-size: 1.0625rem;
line-height: 1.7;
-webkit-font-smoothing: antialiased;
}
main { overflow: clip; }
h1, h2, h3, p, figure { margin: 0; }
h1, h2, h3 {
font-family: var(--th-font-display);
color: var(--th-text);
letter-spacing: -0.01em;
font-family: var(--font-display), sans-serif;
font-weight: 700;
letter-spacing: -.04em;
text-wrap: balance;
}
main > section {
padding-block: var(--th-section-space);
h1, h2 { line-height: 1.12; }
h3 { line-height: 1.35; }
a { color: inherit; text-decoration: none; }
button, select { font: inherit; }
button { cursor: pointer; border: 0; }
button, a, select { -webkit-tap-highlight-color: transparent; }
a, button, select { touch-action: manipulation; }
:where(button, a, select):focus-visible {
outline: 2px solid var(--accent-strong);
outline-offset: 5px;
}
img { display: block; max-width: 100%; object-fit: cover; }
button:disabled { cursor: default; }
[hidden] { display: none !important; }
.shell { width: min(1280px, calc(100% - 128px)); margin-inline: auto; }
/* Anker nicht unter dem Header verstecken */
section[id] {
scroll-margin-top: 5rem;
}
/* ---------- Header ---------- */
/* 2. Kopfbereich – Website-Logo ganz links, wie auf der Referenzseite */
.site-header {
position: sticky;
top: 0;
z-index: 40;
background: rgb(13 13 26 / 0.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgb(255 255 255 / 0.08);
z-index: 20;
border-bottom: 1px solid var(--line);
background: linear-gradient(
color-mix(in oklab, var(--background) 95%, transparent),
color-mix(in oklab, var(--background) 80%, transparent)
);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
}
.site-header .container {
.header-inner {
width: calc(100% - 64px);
min-height: 98px;
display: flex;
align-items: center;
justify-content: space-between;
padding-block: 1rem;
gap: 24px;
padding-block: 16px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.brand-mark {
display: flex;
align-items: center;
justify-content: center;
width: 74px;
height: 66px;
padding: 8px 12px;
overflow: hidden;
border: 1px solid rgb(255 255 255 / 12%);
border-radius: 1.1rem;
background: #fff;
box-shadow: 0 14px 34px rgb(0 0 0 / 22%);
}
.brand-logo img { width: 48px; height: 48px; object-fit: contain; }
.brand-copy { display: grid; gap: 3px; }
.brand-name {
color: var(--quiet);
font-size: .75rem;
letter-spacing: .28em;
text-transform: uppercase;
}
.brand-sub { color: var(--muted); font-size: .875rem; line-height: 1.5; }
.header-inner nav { display: flex; align-items: center; gap: 5px; }
.header-inner nav a {
padding: 9px 14px;
border: 1px solid transparent;
border-radius: 999px;
color: var(--muted);
font-size: .875rem;
white-space: nowrap;
transition: color .2s, background .2s, border-color .2s;
}
.header-inner nav a:hover,
.header-inner nav a[aria-current] {
border-color: var(--line);
background: rgb(255 255 255 / 5%);
color: #fff;
}
.menu-toggle { display: none; }
.skip {
position: fixed;
left: 16px;
top: -100px;
z-index: 100;
padding: 12px 20px;
border-radius: 999px;
background: #fff;
color: var(--ink-strong);
}
.skip:focus { top: 12px; }
.site-logo {
font-family: var(--th-font-display);
font-size: 1.5rem;
font-weight: 700;
color: var(--th-text);
/* 3. Buttons – weiße Primär-CTAs, zurückhaltende Konturen */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 50px;
padding: 14px 24px;
border: 1px solid transparent;
border-radius: 999px;
background: #fff;
color: var(--ink-strong);
font-size: .9375rem;
font-weight: 600;
line-height: 1.45;
text-align: center;
transition: background .25s, border-color .25s, transform .25s var(--ease);
}
.button:hover { background: #e9edf0; transform: translateY(-2px); }
.button.small { min-height: 42px; padding: 10px 20px; font-size: .875rem; }
.button.outline, .header-cta {
border-color: rgb(255 255 255 / 16%);
background: transparent;
color: #fff;
}
.button.outline:hover, .header-cta:hover {
border-color: rgb(255 255 255 / 28%);
background: rgb(255 255 255 / 7%);
}
.text-link {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 8px 0;
border-bottom: 1px solid rgb(255 255 255 / 28%);
background: transparent;
color: rgb(255 255 255 / 86%);
font-size: .9375rem;
font-weight: 500;
line-height: 1.4;
transition: color .2s, border-color .2s;
}
.text-link:hover { color: #fff; border-color: #fff; }
/* ---------- Barrierefreiheit ---------- */
a:focus-visible,
button:focus-visible {
outline: 3px solid var(--th-accent);
outline-offset: 4px;
border-radius: 0.5rem;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
/* Footer */
footer { border-top: 1px solid var(--line); background: var(--sidebar); }
.footer-main { display: flex; align-items: center; gap: 24px; padding-block: 32px; }
.footer-main p { color: var(--muted); font-size: .9375rem; }
.footer-main > a:last-child { margin-left: auto; color: var(--muted); font-size: .875rem; }
.footer-main > a:last-child:hover { color: #fff; }
.footer-note { display: flex; justify-content: space-between; gap: 24px; padding-block: 20px; border-top: 1px solid var(--line); color: var(--quiet); font-size: .75rem; }

View File

@ -1,142 +1,8 @@
/* =========================================================
US01-03: Ablauf sehen
Branch: feature_US01-03
Nur Selektoren unter #ablauf verwenden.
Farben/Schriften über var(--th-...) aus base.css.
Vorlage: Prototyp (Programmablauf)
========================================================= */
#ablauf {
--abl-mint: #56d9b1;
--abl-line: rgb(255 255 255 / 0.11);
}
/* ---------- Überschrift ---------- */
#ablauf .ablauf-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 3.5rem;
margin-bottom: 2.75rem;
}
#ablauf .ablauf-eyebrow {
margin: 0;
font-size: 0.875rem;
font-weight: 650;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--abl-mint);
}
#ablauf h2 {
margin: 0.9rem 0 0;
font-size: clamp(2.2rem, 3.4vw, 2.8rem);
font-weight: 400;
line-height: 1.12;
letter-spacing: -0.03em;
}
#ablauf .ablauf-heading > p {
max-width: 24rem;
margin: 0;
color: var(--th-text-muted);
}
/* ---------- Schritte ---------- */
#ablauf .steps {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2rem;
margin: 0;
padding: 0;
list-style: none;
}
#ablauf .step-number {
display: block;
margin-bottom: 1.4rem;
padding: 0.5rem 0 1.1rem;
border-bottom: 1px solid var(--abl-line);
font-family: var(--th-font-display);
font-size: 2rem;
line-height: 1;
color: var(--abl-mint);
position: relative;
}
/* grüner Fortschrittsstrich unter der Nummer zeigt die Reihenfolge */
#ablauf .step-number::after {
content: "";
position: absolute;
left: 0;
bottom: -1px;
width: 2.5rem;
height: 2px;
background: var(--th-accent);
}
#ablauf .step h3 {
margin: 0 0 0.75rem;
font-family: var(--th-font-body);
font-size: 1.25rem;
font-weight: 600;
line-height: 1.3;
}
#ablauf .step p {
margin: 0;
color: var(--th-text-muted);
}
/* ---------- Abschluss ---------- */
#ablauf .process-end {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
margin-top: 2.75rem;
padding-top: 1.9rem;
border-top: 1px solid var(--abl-line);
}
#ablauf .process-end p {
margin: 0;
font-family: var(--th-font-display);
font-size: 1.45rem;
font-style: italic;
color: #d3d7e1;
}
#ablauf .process-link {
display: inline-flex;
align-items: center;
gap: 0.75rem;
min-height: 2.75rem;
font-size: 1.125rem;
font-weight: 600;
color: var(--th-text-soft);
white-space: nowrap;
}
#ablauf .process-link:hover {
color: var(--abl-mint);
}
/* ---------- Tablet: 2 × 2 ---------- */
@media (max-width: 1100px) {
#ablauf .ablauf-heading { gap: 2rem; }
#ablauf .steps { grid-template-columns: repeat(2, 1fr); gap: 2rem 1.75rem; }
}
/* ---------- Mobil: untereinander ---------- */
@media (max-width: 760px) {
#ablauf .ablauf-heading { display: block; margin-bottom: 2rem; }
#ablauf .ablauf-heading > p { max-width: none; margin-top: 1.25rem; }
#ablauf .process-end { display: block; }
#ablauf .process-link { margin-top: 1rem; }
}
@media (max-width: 560px) {
#ablauf .steps { grid-template-columns: 1fr; gap: 1.75rem; }
}
/* 7. Ablauf und Fußbereich */
.steps { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 30px; margin: 0; padding: 0; list-style: none; }
.step-number { display: block; margin-bottom: 24px; padding: 8px 0 22px; border-bottom: 1px solid var(--line); color: rgb(255 255 255 / 45%); font-family: var(--font-display); font-size: 2rem; font-weight: 500; letter-spacing: -.04em; }
.steps h3 { margin-bottom: 14px; font-size: 1.0625rem; }
.steps p { color: var(--muted); font-size: 1rem; }
.process-end { display: flex; align-items: center; justify-content: space-between; gap: 24px; margin-top: 48px; padding-top: 30px; border-top: 1px solid var(--line); }
.process-end p { color: rgb(255 255 255 / 82%); font-size: 1.25rem; }
.process-end .text-link { flex-shrink: 0; }

View File

@ -1,381 +1,90 @@
/* =========================================================
US01-02: Angebote und Pakete (+ Verweis auf Coaching-Rabatte)
Branch: feature_US01-02
Nur Selektoren unter #angebote verwenden.
Farben/Schriften über var(--th-...) aus base.css.
Vorlage: Prototyp (Angebotsbereich + Detaildialog)
========================================================= */
#angebote {
--ang-mint: #56d9b1;
--ang-line: rgb(255 255 255 / 0.11);
background: linear-gradient(135deg, #161625, #11111e);
border-block: 1px solid var(--ang-line);
}
/* ---------- Überschrift ---------- */
#angebote .angebote-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 3.5rem;
margin-bottom: 1.5rem;
}
#angebote .angebote-eyebrow {
margin: 0;
font-size: 0.875rem;
font-weight: 650;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ang-mint);
}
#angebote h2 {
margin: 0.9rem 0 0;
font-size: clamp(2.2rem, 3.4vw, 2.8rem);
font-weight: 400;
line-height: 1.12;
letter-spacing: -0.03em;
}
#angebote .angebote-heading > p {
max-width: 24rem;
margin: 0;
color: var(--th-text-muted);
}
#angebote .concept-note {
display: flex;
gap: 0.5rem;
margin: 0 0 2rem;
font-size: 1rem;
color: var(--th-text-muted);
}
/* ---------- Paketkarten ---------- */
#angebote .package-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); /* minmax(0,…) verhindert Überlaufen */
gap: 1.5rem;
}
#angebote .package {
/* 6. Angebote – Bildkarten statt Vergleichstabelle */
.offers-section { border-block: 1px solid var(--line); background: var(--surface-elevated); }
.offers-section .section-heading { margin-bottom: 28px; }
.concept-note { display: flex; gap: 8px; margin-bottom: 30px; color: var(--quiet); font-size: .8125rem; }
.package-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
.package {
display: flex;
flex-direction: column;
padding: 0.75rem;
border: 1px solid var(--ang-line);
border-radius: 18px;
background: linear-gradient(140deg, #202032, #181827);
transition: border-color 0.2s;
}
#angebote .package:hover,
#angebote .package.featured {
border-color: rgb(16 185 129 / 0.45);
}
#angebote .package-image {
position: relative;
padding: 16px;
overflow: hidden;
border-radius: 10px;
border: 1px solid var(--line);
border-radius: 2rem;
background: linear-gradient(#ffffff0f, #ffffff05), linear-gradient(#08151ee6, #091822fa);
box-shadow: 0 18px 48px rgb(0 0 0 / 16%);
transition: border-color .3s, transform .3s var(--ease), box-shadow .3s;
}
#angebote .package-image img {
width: 100%;
aspect-ratio: 1.65;
object-fit: cover;
transition: transform 0.4s;
}
#angebote .package:hover img {
transform: scale(1.03);
}
#angebote .package-badge {
.package:hover { border-color: rgb(255 255 255 / 22%); transform: translateY(-4px); box-shadow: 0 24px 52px rgb(0 0 0 / 22%); }
.package.featured { border-color: color-mix(in srgb, var(--accent-muted) 35%, transparent); }
.package-image { position: relative; overflow: hidden; border-radius: 1.35rem; }
.package-image img { width: 100%; aspect-ratio: 1.65; transition: transform .65s var(--ease); }
.package:hover .package-image img { transform: scale(1.025); }
.package-badge {
position: absolute;
left: 0.75rem;
bottom: 0.75rem;
padding: 0.45rem 0.75rem;
border-radius: 5px;
background: var(--th-accent);
color: #06261a;
font-size: 0.75rem;
font-weight: 750;
letter-spacing: 0.08em;
text-transform: uppercase;
}
#angebote .package-body {
display: flex;
flex-direction: column;
flex: 1;
padding: 1.5rem 0.9rem 0.9rem;
}
#angebote .package-label {
margin: 0;
font-size: 0.875rem;
font-weight: 650;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ang-mint);
}
#angebote .package-body h3 {
margin: 0.8rem 0 1rem;
font-size: 1.9rem;
font-weight: 400;
line-height: 1.15;
}
#angebote .package-text {
margin: 0;
color: var(--th-text-muted);
}
#angebote .package-body ul {
flex: 1;
margin: 1.25rem 0 0;
padding: 1.25rem 0 1.5rem;
list-style: none;
border-top: 1px solid var(--ang-line);
}
#angebote .package-body li {
position: relative;
padding: 0.3rem 0 0.3rem 1.5rem;
}
#angebote .package-body li::before {
content: "✓";
position: absolute;
left: 0;
color: var(--ang-mint);
}
/* ---------- Buttons ---------- */
#angebote .package-button,
#angebote .dialog-done {
display: inline-flex;
align-items: center;
justify-content: space-between;
width: 100%;
min-height: 3.25rem;
padding: 0.9rem 1rem;
border: 1px solid rgb(255 255 255 / 0.15);
border-radius: 7px;
background: transparent;
color: var(--th-text-soft);
font-size: 1.0625rem;
font-weight: 650;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
#angebote .package-button:hover {
border-color: var(--th-accent);
background: rgb(16 185 129 / 0.08);
}
#angebote .package-button.is-primary,
#angebote .dialog-done {
border-color: transparent;
background: var(--th-accent);
color: #071b16;
}
#angebote .package-button.is-primary:hover,
#angebote .dialog-done:hover {
background: #37c99b;
}
/* ---------- Coaching-Rabatt ---------- */
#angebote .coaching-banner {
display: flex;
align-items: center;
gap: 1.5rem;
margin-top: 1.75rem;
padding: 1.6rem 1.9rem;
border: 1px solid rgb(16 185 129 / 0.19);
border-radius: 12px;
background: rgb(16 185 129 / 0.04);
}
#angebote .bonus-symbol {
padding-right: 1.5rem;
border-right: 1px solid rgb(16 185 129 / 0.25);
font-family: var(--th-font-display);
font-size: 2.2rem;
color: var(--ang-mint);
}
#angebote .coaching-banner h3 {
margin: 0 0 0.3rem;
font-family: var(--th-font-body);
font-size: 1.2rem;
bottom: 12px;
left: 12px;
max-width: calc(100% - 24px);
padding: 6px 12px;
border: 1px solid color-mix(in srgb, var(--accent-muted) 32%, transparent);
border-radius: 999px;
background: rgb(7 27 34 / 90%);
color: color-mix(in srgb, var(--accent-strong) 74%, white);
font-size: .6875rem;
font-weight: 600;
letter-spacing: .13em;
}
.package-body { display: flex; flex: 1; flex-direction: column; padding: 24px 8px 8px; }
.package-body h3 { margin-block: 14px 20px; font-size: clamp(1.5rem, 2.05vw, 1.875rem); line-height: 1.3; }
.package-body > p:not(.eyebrow) { color: var(--muted); font-size: 1rem; line-height: 1.8; }
.package-body ul { flex: 1; margin-top: 24px; padding: 20px 0 24px; border-top: 1px solid var(--line); list-style: none; font-size: .9375rem; }
.package-body li { position: relative; padding: 6px 0 6px 23px; color: rgb(255 255 255 / 82%); }
.package-body li::before { content: '✓'; position: absolute; left: 0; color: var(--quiet); }
.package .button { width: 100%; font-size: .9375rem; }
.coaching-banner { display: flex; align-items: center; gap: 24px; margin-top: 30px; padding: 28px 30px; border: 1px solid var(--line); border-radius: 1.5rem; background: var(--panel); }
.bonus-symbol { display: grid; flex-shrink: 0; place-items: center; width: 52px; height: 52px; border: 1px solid var(--line); border-radius: 1rem; background: rgb(255 255 255 / 3%); color: rgb(255 255 255 / 80%); font-size: 1.5rem; }
.coaching-banner h3 { margin-bottom: 6px; font-size: 1.0625rem; }
.coaching-banner p { color: var(--muted); font-size: .9375rem; }
.coaching-banner .text-link { margin-left: auto; flex-shrink: 0; font-size: .875rem; }
#angebote .coaching-banner p {
margin: 0;
color: var(--th-text-muted);
}
#angebote .bonus-button {
margin-left: auto;
min-height: 2.75rem;
border: 0;
background: none;
color: var(--ang-mint);
font-size: 1.0625rem;
font-weight: 600;
white-space: nowrap;
cursor: pointer;
}
/* ---------- Dialog ---------- */
#angebote dialog {
margin: auto; /* Kunden-CSS setzt margin:0 → Dialog wieder zentrieren */
width: min(680px, calc(100% - 2rem));
/* 8. Produktdetails – weiterhin zentriert und innerhalb des Fensters scrollbar */
dialog {
position: fixed;
inset: 0;
margin: auto;
width: min(720px, calc(100% - 32px));
max-height: 90dvh;
padding: 0;
border: 1px solid rgb(255 255 255 / 0.15);
border-radius: 20px;
background: #171726;
color: var(--th-text-soft);
box-shadow: 0 30px 100px rgb(0 0 0 / 0.55);
overflow-y: auto;
overscroll-behavior: contain;
border: 1px solid rgb(255 255 255 / 16%);
border-radius: 1.75rem;
background: linear-gradient(#ffffff07, #ffffff00), var(--surface-elevated);
color: var(--text);
box-shadow: 0 30px 100px rgb(0 0 0 / 55%);
scrollbar-color: #46545c transparent;
scrollbar-width: thin;
}
dialog::backdrop { background: rgb(2 8 12 / 78%); backdrop-filter: blur(7px); }
.dialog-inner { position: relative; padding: 42px; }
.close-dialog { position: absolute; top: 14px; right: 14px; width: 42px; height: 42px; border: 1px solid var(--line); border-radius: 50%; background: rgb(255 255 255 / 4%); color: var(--muted); font-size: 1.75rem; line-height: 1; }
.close-dialog:hover { background: rgb(255 255 255 / 10%); color: #fff; }
.dialog-inner > .eyebrow { padding-right: 24px; }
.dialog-inner h2 { margin: 16px 18px 22px 0; font-size: 2rem; line-height: 1.2; }
.dialog-inner > p:not(.eyebrow) { color: var(--muted); font-size: 1rem; }
.dialog-inner h3 { margin: 28px 0 14px; font-size: 1.125rem; }
#dialog-features { padding-left: 20px; color: rgb(255 255 255 / 82%); font-size: 1rem; }
#dialog-features li { padding-block: 5px; }
#dialog-features li::marker { color: var(--quiet); }
.detail-note, .discount-help { color: var(--quiet); font-size: .8125rem; line-height: 1.7; }
.detail-note { margin-top: 12px; }
.discount-box { margin-block: 28px; padding: 24px; border: 1px solid var(--line); border-radius: 1.25rem; background: var(--panel); }
.discount-box h3 { margin: 10px 0 22px; }
.discount-box label { display: block; margin-bottom: 10px; color: rgb(255 255 255 / 82%); font-size: .9375rem; }
.discount-box select { width: 100%; min-height: 50px; padding: 12px; border: 1px solid rgb(255 255 255 / 20%); border-radius: .75rem; background: var(--background); color: #fff; font-size: .9375rem; }
#discount-result { padding-block: 20px; color: rgb(255 255 255 / 88%); font-size: 1.0625rem; }
#discount-result strong { color: color-mix(in srgb, var(--accent-strong) 70%, white); font-size: 1.5rem; font-weight: 600; }
.discount-box a { display: inline-block; margin-top: 16px; color: rgb(255 255 255 / 84%); font-size: .875rem; text-decoration: underline; text-underline-offset: 4px; }
.discount-box a:hover { color: #fff; }
.dialog-done { width: 100%; }
body:has(dialog[open]) { overflow: hidden; }
#angebote dialog::backdrop {
background: rgb(5 5 11 / 0.8);
backdrop-filter: blur(6px);
}
#angebote .dialog-inner {
position: relative;
padding: 2.5rem;
}
#angebote .close-dialog {
position: absolute;
top: 0.75rem;
right: 1rem;
width: 2.75rem;
height: 2.75rem;
border: 0;
background: transparent;
color: var(--th-text-muted);
font-size: 1.9rem;
cursor: pointer;
}
#angebote .dialog-inner h2 {
margin: 0.8rem 2.2rem 1.1rem 0;
font-size: 2.2rem;
}
#angebote #dialog-description {
margin: 0;
color: var(--th-text-muted);
}
#angebote .dialog-inner h3 {
margin: 1.6rem 0 0.75rem;
font-family: var(--th-font-body);
font-size: 1.2rem;
font-weight: 600;
}
#angebote #dialog-features {
margin: 0;
padding-left: 1.25rem;
}
#angebote #dialog-features li {
padding: 0.25rem 0;
}
#angebote .dialog-hint {
margin: 0.75rem 0 0;
font-size: 1rem;
color: var(--th-text-muted);
}
#angebote .discount-box {
margin: 1.6rem 0;
padding: 1.4rem;
border: 1px solid rgb(16 185 129 / 0.27);
border-radius: 12px;
background: rgb(16 185 129 / 0.035);
}
#angebote .discount-box h3 {
margin-top: 0.4rem;
}
#angebote .discount-box label {
display: block;
margin-bottom: 0.5rem;
}
#angebote .discount-box select {
width: 100%;
padding: 0.75rem;
border: 1px solid rgb(255 255 255 / 0.25);
border-radius: 7px;
background: #11111f;
color: var(--th-text-soft);
font-size: 1.0625rem;
}
#angebote #discount-result {
margin: 0;
padding: 1rem 0 0;
color: var(--ang-mint);
}
#angebote #discount-result strong {
font-size: 1.6rem;
}
#angebote .discount-box a {
display: inline-block;
margin-top: 0.9rem;
color: var(--ang-mint);
}
#angebote select:focus-visible {
outline: 3px solid var(--th-accent);
outline-offset: 3px;
}
/* ---------- Tablet ---------- */
@media (max-width: 1100px) {
#angebote .angebote-heading { gap: 2rem; }
#angebote .package-body h3 { font-size: 1.7rem; }
#angebote .coaching-banner { flex-wrap: wrap; }
#angebote .bonus-button { margin-left: 0; }
}
/* ---------- schmale Tablets: 2 Karten nebeneinander ---------- */
@media (max-width: 1000px) {
#angebote .package-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* ---------- Mobil ---------- */
@media (max-width: 760px) {
#angebote .angebote-heading { display: block; }
#angebote .angebote-heading > p { max-width: none; margin-top: 1.25rem; }
#angebote .package-grid { grid-template-columns: 1fr; }
#angebote .package-image img { aspect-ratio: 1.8; }
#angebote .coaching-banner { padding: 1.4rem; gap: 1rem; }
#angebote .coaching-banner > div:nth-child(2) { flex: 1; min-width: 12rem; }
#angebote .bonus-symbol { display: none; }
#angebote .dialog-inner { padding: 2rem 1.4rem; }
#angebote .dialog-inner h2 { font-size: 1.9rem; }
}
@media (prefers-reduced-motion: reduce) {
#angebote .package img { transition: none; }
}

View File

@ -1,264 +1,107 @@
/* =========================================================
US01-01: Angebot und Zielgruppe sofort sehen (Hero)
Branch: feature_US01-01
Nur Selektoren unter #hero verwenden.
Farben/Schriften über var(--th-...) aus base.css.
Vorlage: Prototyp (Hero-Bereich)
========================================================= */
#hero {
--hero-mint: #56d9b1; /* helle Akzentvariante aus dem Prototyp */
--hero-line: rgb(255 255 255 / 0.11);
padding-block: clamp(1.5rem, 4svh, 3.5rem); /* überschreibt Sektionsabstand aus base.css */
min-height: calc(100svh - 4.5rem); /* ohne Scrollen sichtbar (minus Header) */
display: flex;
align-items: center;
}
#hero .hero-grid {
/* 4. Hero – Sans Serif, Original-Hintergrund und gerahmtes Foto */
.hero {
position: relative;
isolation: isolate;
display: grid;
grid-template-columns: 1.08fr 1fr;
gap: 4rem;
gap: 56px;
align-items: center;
width: 100%;
padding-block: 64px 72px;
}
/* ---------- Text ---------- */
#hero h1 {
margin: 0;
font-weight: 400;
.hero::before {
content: '';
position: absolute;
z-index: -1;
inset: 0 calc((100vw - 100%) / -2);
pointer-events: none;
background: linear-gradient(rgb(4 14 20 / 40%), rgb(4 14 20 / 82%)),
url('../../assets/images/hero/trihub-ribbon.webp') center / cover;
opacity: .2;
}
#hero .hero-kicker {
display: flex;
align-items: center;
gap: 0.75rem;
font-family: var(--th-font-body);
font-size: 1rem;
font-weight: 650;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--hero-mint);
line-height: 1.5;
}
#hero .hero-line {
flex: 0 0 1.5rem;
height: 1px;
background: var(--th-accent);
}
#hero .hero-headline {
display: block;
margin-top: 1.5rem;
font-family: var(--th-font-display);
font-size: clamp(2.6rem, 4.8vw, 4.5rem);
line-height: 1.1;
letter-spacing: -0.03em;
color: var(--th-text);
}
#hero .hero-headline em {
color: var(--hero-mint);
}
#hero .hero-intro {
margin-top: 1.5rem;
max-width: 32rem;
font-size: 1.1875rem;
line-height: 1.75;
color: var(--th-text-muted);
}
/* ---------- Aktionen ---------- */
#hero .hero-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1.75rem;
margin-top: 2rem;
}
#hero .hero-button {
display: inline-flex;
align-items: center;
gap: 1.25rem;
min-height: 3.25rem; /* große Klickfläche */
padding: 0.9rem 1.6rem;
border-radius: 7px;
background: var(--th-accent);
color: #071b16;
font-size: 1.125rem;
font-weight: 650;
transition: background 0.2s, transform 0.2s;
}
#hero .hero-button:hover {
background: #37c99b;
transform: translateY(-2px);
}
#hero .hero-link {
display: inline-flex;
align-items: center;
gap: 0.75rem;
min-height: 2.75rem;
font-size: 1.125rem;
.eyebrow {
color: var(--quiet);
font-size: .75rem;
font-weight: 600;
color: var(--th-text-soft);
letter-spacing: .22em;
line-height: 1.65;
}
#hero .hero-link:hover {
color: var(--hero-mint);
.hero-copy > .eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgb(255 255 255 / 4%);
color: var(--muted);
font-size: .6875rem;
letter-spacing: .16em;
}
/* ---------- Bild ---------- */
#hero .hero-visual {
.tiny-line { width: 16px; height: 1px; flex-shrink: 0; background: var(--accent-warm); }
h1 {
margin-block: 28px;
font-size: clamp(2.75rem, 4.65vw, 4.5rem);
font-weight: 600;
letter-spacing: -.05em;
line-height: 1.06;
}
h1 em { color: inherit; font: inherit; }
.hero-intro { max-width: 510px; color: rgb(255 255 255 / 74%); font-size: 1.125rem; line-height: 1.8; }
.hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 24px; margin-top: 32px; }
.hero-note { margin-top: 26px; color: var(--quiet); font-size: .875rem; }
.check { margin-right: 7px; color: rgb(255 255 255 / 72%); }
.hero-visual {
position: relative;
margin: 0;
height: clamp(320px, 52svh, 540px);
border-radius: 15px;
height: 570px;
padding: 12px;
overflow: hidden;
background: var(--th-bg-elevated);
border: 1px solid rgb(255 255 255 / 12%);
border-radius: 2rem;
background: var(--panel);
box-shadow: 0 24px 60px rgb(0 0 0 / 22%);
}
#hero .hero-visual img {
width: 100%;
height: 100%;
object-fit: cover;
}
#hero .hero-visual::after { /* Verlauf für lesbare Bildunterschrift */
content: "";
.hero-visual > img { width: 100%; height: 100%; border-radius: 1.4rem; object-position: center; }
.hero-visual figcaption {
position: absolute;
inset: 35% 0 0;
background: linear-gradient(transparent, rgb(5 15 18 / 0.94));
right: 28px;
bottom: 28px;
left: 28px;
padding: 22px 24px;
border: 1px solid rgb(255 255 255 / 14%);
border-radius: 1.4rem;
background: rgb(3 13 18 / 84%);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
#hero figcaption {
.hero-visual figcaption span { color: var(--quiet); font-size: .75rem; letter-spacing: .19em; }
.hero-visual figcaption p { margin-top: 10px; font-size: 1.25rem; font-weight: 500; line-height: 1.5; }
.image-tag {
position: absolute;
left: 2rem;
bottom: 2rem;
z-index: 1;
}
#hero .hero-caption-label {
display: block;
font-size: 0.8rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #b5efda;
}
#hero .hero-caption-text {
display: block;
margin-top: 0.6rem;
font-family: var(--th-font-display);
font-size: 1.75rem;
line-height: 1.3;
color: var(--th-text);
}
#hero .hero-tag {
position: absolute;
top: 1.25rem;
right: 1.25rem;
padding: 0.75rem 1rem;
border: 1px solid rgb(255 255 255 / 0.19);
border-radius: 8px;
background: rgb(13 13 26 / 0.8);
top: 28px;
right: 28px;
padding: 10px 16px;
border: 1px solid rgb(255 255 255 / 18%);
border-radius: 1rem;
background: rgb(3 13 18 / 78%);
backdrop-filter: blur(12px);
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
font-size: .6875rem;
letter-spacing: .13em;
text-align: center;
color: var(--th-text-soft);
}
.image-tag b { display: block; margin-top: 2px; color: #fff; font-family: var(--font-display); font-size: 1.8rem; font-weight: 600; letter-spacing: -.04em; line-height: 1.2; }
.values-strip { border-block: 1px solid var(--line); background: rgb(255 255 255 / 2%); }
.values-strip .shell { display: flex; justify-content: center; align-items: center; gap: 60px; padding-block: 22px; color: var(--muted); font-size: .875rem; }
.values-strip i { color: rgb(255 255 255 / 24%); font-size: .75rem; font-style: normal; }
#hero .hero-tag b {
display: block;
margin-top: 0.25rem;
font-family: var(--th-font-display);
font-size: 2rem;
line-height: 1.1;
letter-spacing: 0;
color: var(--hero-mint);
}
/* 5. Vorteile und wiederkehrende Sektionsüberschriften */
.section { padding-block: 96px; }
.section-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 56px; margin-bottom: 44px; }
.section-heading h2 { margin-top: 16px; font-size: clamp(2rem, 3.1vw, 3rem); }
.section-heading > p { max-width: 350px; color: var(--muted); font-size: 1rem; line-height: 1.85; }
.benefit-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 40px; }
.benefit-grid article { padding-top: 28px; border-top: 1px solid var(--line); }
.benefit-icon { display: flex; align-items: center; justify-content: center; width: 44px; height: 44px; margin-bottom: 22px; border: 1px solid var(--line); border-radius: .875rem; background: rgb(255 255 255 / 4%); color: rgb(255 255 255 / 74%); font-size: 1.5rem; }
.benefit-grid h3 { margin-bottom: 14px; font-size: 1.25rem; }
.benefit-grid p { max-width: 365px; color: var(--muted); font-size: 1rem; }
/* ---------- 3 Vorteile unter dem Hero ---------- */
#hero .hero-benefits {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
margin: 2.5rem 0 0;
padding: 1.5rem 0 0;
list-style: none;
border-top: 1px solid var(--hero-line);
}
#hero .hero-benefits li {
display: flex;
gap: 1rem;
align-items: flex-start;
}
#hero .hero-benefit-icon {
flex: 0 0 2.5rem;
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
border-radius: 10px;
background: rgb(16 185 129 / 0.1);
color: var(--hero-mint);
font-size: 1.3rem;
}
#hero .hero-benefits h2 {
margin: 0;
font-family: var(--th-font-body);
font-size: 1.125rem;
font-weight: 600;
line-height: 1.35;
letter-spacing: -0.01em;
color: var(--th-text);
}
#hero .hero-benefits p {
margin: 0.3rem 0 0;
font-size: 1rem;
line-height: 1.55;
color: var(--th-text-muted);
}
/* ---------- Tablet ---------- */
@media (max-width: 1100px) {
#hero .hero-grid { gap: 2rem; }
#hero .hero-benefits { gap: 1.25rem; }
}
/* ---------- Mobil: Text zuerst, Bild darunter ---------- */
@media (max-width: 760px) {
#hero {
min-height: 0;
padding-block: 2rem 3rem;
}
#hero .hero-grid {
grid-template-columns: 1fr;
gap: 2rem;
}
#hero .hero-kicker { font-size: 0.875rem; }
#hero .hero-headline { margin-top: 1rem; font-size: clamp(2.3rem, 10vw, 3.2rem); }
#hero .hero-intro { margin-top: 1rem; font-size: 1.125rem; }
#hero .hero-actions { margin-top: 1.5rem; gap: 1.25rem; }
#hero .hero-visual { height: 340px; }
#hero figcaption { left: 1.25rem; bottom: 1.25rem; }
#hero .hero-caption-text { font-size: 1.4rem; }
#hero .hero-benefits { grid-template-columns: 1fr; gap: 1.25rem; margin-top: 2rem; }
}
@media (max-width: 390px) {
#hero .hero-button { width: 100%; justify-content: space-between; }
}

View File

@ -1,270 +1,389 @@
<!DOCTYPE html>
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ernährungsberatung für Triathleten 50+ | tri-hub</title>
<meta name="description" content="Ernährungsberatung für Triathletinnen und Triathleten ab 50.">
<!-- Kunden-CSS (Tailwind-Build von tri-hub.de) – NICHT verändern -->
<link rel="stylesheet" href="css/assets.css">
<!-- Schriften laut Design-DNA: Playfair Display (Überschriften) + Inter (Text) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@600;700&display=swap">
<!-- Projektweite Basis (Design-Tokens, Lesbarkeit) -->
<link rel="stylesheet" href="css/base.css">
<!-- Ein Stylesheet pro User Story – jede Datei gehört genau einem Feature-Branch -->
<link rel="stylesheet" href="css/sections/hero.css"> <!-- US01-01 -->
<link rel="stylesheet" href="css/sections/angebote.css"> <!-- US01-02 -->
<link rel="stylesheet" href="css/sections/ablauf.css"> <!-- US01-03 -->
</head>
<body>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Ernährungsberatung ab 50 | TriHub</title>
<meta
name="description"
content="Ernährung, die zu deinem Triathlon und deinem Leben passt. Entdecke drei Konzepte für persönliche Ernährungsberatung bei TriHub."
/>
<link
rel="icon"
type="image/jpeg"
href="assets/images/icons/trihub-logo.jpg"
/>
<link rel="stylesheet" href="css/assets.css" />
<link rel="stylesheet" href="css/base.css" />
<link rel="stylesheet" href="css/sections/hero.css" />
<link rel="stylesheet" href="css/sections/angebote.css" />
<link rel="stylesheet" href="css/sections/ablauf.css" />
<link rel="stylesheet" href="css/responsive.css" />
<script src="js/angebote.js" defer></script>
<script src="js/main.js" defer></script>
</head>
<body>
<a class="skip" href="#inhalt">Zum Inhalt</a>
<header class="site-header">
<div class="container">
<a href="/" class="site-logo">tri-hub</a>
<!-- US01-05 Navigation: späterer Sprint -->
<div class="shell header-inner">
<a
class="brand brand-logo"
href="https://tri-hub.de/"
aria-label="TriHub Startseite"
><span class="brand-mark"
><img
src="assets/images/icons/trihub-logo.jpg"
width="1024"
height="1024"
alt="Trihub – fit und vital ab 50!" /></span
><span class="brand-copy"
><span class="brand-name">Tri-hub</span
><span class="brand-sub"
>Ernährungsberatung für Triathlon ab 50</span
></span
></a
>
<nav aria-label="Hauptnavigation">
<a href="https://tri-hub.de/coaching-1zu1">Coaching 1zu1</a
><a href="#inhalt" aria-current="page">Ernährungsberatung</a
><a href="#ablauf">So läuft’s ab</a>
</nav>
<a class="button small header-cta" href="#angebote"
>Angebote entdecken</a
>
<button
class="menu-toggle"
aria-expanded="false"
aria-controls="mobile-nav"
aria-label="Menü öffnen"
>
☰
</button>
</div>
<nav id="mobile-nav" aria-label="Mobile Navigation" hidden>
<a href="#angebote">Angebote entdecken</a
><a href="#vorteile">Deine Vorteile</a
><a href="#ablauf">So läuft’s ab</a
><a href="https://tri-hub.de/coaching-1zu1">Coaching 1zu1</a>
</nav>
</header>
<main>
<!-- ========== US01-01: Angebot und Zielgruppe (Hero) ==========
Branch: feature/US01-01-hero · Styles: css/sections/hero.css
Nur innerhalb dieser <section> arbeiten. -->
<section id="hero" aria-labelledby="hero-title">
<div class="container">
<div class="hero-grid">
<main id="inhalt">
<section class="hero shell" aria-labelledby="hero-title">
<div class="hero-copy">
<div class="eyebrow">
<span class="tiny-line"></span> ERNÄHRUNGSBERATUNG FÜR TRIATHLON AB
50
</div>
<h1 id="hero-title">
<span class="hero-kicker"><span class="hero-line" aria-hidden="true"></span>Ernährungsberatung für Triathlon ab&nbsp;50</span>
<span class="hero-headline">Gut versorgt.<br>Für alles, was<br><em>noch vor dir liegt.</em></span>
Gut versorgt.<br />Für alles, was<br /><em>noch vor dir liegt.</em>
</h1>
<p class="hero-intro">
Dein nächstes Ziel beginnt auch auf dem Teller. Finde eine Ernährung,
die zu deinem Training, deinem Körper und deinem Leben passt.
Dein nächstes Ziel beginnt auch auf dem Teller. Finde eine
Ernährung, die zu deinem Training, deinem Körper und deinem Leben
passt.
</p>
<div class="hero-actions">
<a class="hero-button" href="#angebote">Mehr erfahren <span aria-hidden="true">↓</span></a>
<a class="hero-link" href="#ablauf">So funktioniert’s <span aria-hidden="true">→</span></a>
<a class="button" href="#angebote">Deine Begleitung finden</a
><a class="text-link" href="#ablauf">So funktioniert’s</a>
</div>
<div class="hero-note">
<span class="check" aria-hidden="true">✓</span> Persönlich.
Alltagstauglich. Auf Augenhöhe.
</div>
</div>
<figure class="hero-visual">
<img src="assets/images/hero/hero.webp" width="1200" height="800"
<img
src="assets/images/hero/hero.webp"
width="1200"
height="800"
alt="Zwei sportlich aktive Menschen ab 50 besprechen einen Plan neben einem Fahrrad am See"
fetchpriority="high">
fetchpriority="high"
/>
<figcaption>
<span class="hero-caption-label">Dein Tempo. Deine Ziele.</span>
<span class="hero-caption-text">Mehr Freude am Sport.<br>Mehr Energie fürs Leben.</span>
<span>DEIN TEMPO. DEINE ZIELE.</span>
<p>Mehr Freude am Sport.<br />Mehr Energie fürs Leben.</p>
</figcaption>
<div class="hero-tag" aria-hidden="true">Triathlon <b>50+</b></div>
<div class="image-tag">TRIATHLON <b>50+</b></div>
</figure>
</section>
<div class="values-strip">
<div class="shell">
<span>Für deinen Einstieg</span><i aria-hidden="true">✦</i
><span>Für deinen Wiedereinstieg</span><i aria-hidden="true">✦</i
><span>Für dein nächstes Ziel</span>
</div>
<!-- 3 Vorteile mit Überschrift (Checkliste US01-01) -->
<ul class="hero-benefits" aria-label="Deine Vorteile auf einen Blick">
<li>
<span class="hero-benefit-icon" aria-hidden="true">↗</span>
</div>
<section id="vorteile" class="shell section benefits">
<div class="section-heading">
<div>
<h2>Energie gezielt einplanen</h2>
<p>Mahlzeiten passend zu Ruhetagen, harten Einheiten und langen Ausfahrten.</p>
<p class="eyebrow">WAS DU DAVON HAST</p>
<h2>Dein Training verdient<br />die passende Ernährung.</h2>
</div>
</li>
<li>
<span class="hero-benefit-icon" aria-hidden="true">↻</span>
<div>
<h2>Regeneration mitdenken</h2>
<p>Ernährung, die deinen Körper nach dem Training gezielt unterstützt.</p>
<p>
Du bringst die Motivation mit. Gemeinsam schaffen wir Orientierung
für das Essen und Trinken rund um deinen Sport.
</p>
</div>
</li>
<li>
<span class="hero-benefit-icon" aria-hidden="true">✓</span>
<div>
<h2>Im Alltag dranbleiben</h2>
<p>Routinen, die zu Arbeit, Familie und deinen Vorlieben passen.</p>
</div>
</li>
</ul>
<div class="benefit-grid">
<article>
<span class="benefit-icon" aria-hidden="true">↗</span>
<h3>Energie gezielt einplanen</h3>
<p>
Verstehe, wie du deine Mahlzeiten auf ruhige Tage, intensive
Einheiten und lange Ausfahrten abstimmst.
</p>
</article>
<article>
<span class="benefit-icon" aria-hidden="true">↻</span>
<h3>Regeneration mitdenken</h3>
<p>
Gib Erholung einen festen Platz – mit einer Ernährung, die deinen
Körper nach dem Training unterstützt.
</p>
</article>
<article>
<span class="benefit-icon" aria-hidden="true">✓</span>
<h3>Im Alltag dranbleiben</h3>
<p>
Entwickle Routinen, die zu Arbeit, Familie und deinen Vorlieben
passen. Ohne komplizierte Regeln.
</p>
</article>
</div>
</section>
<!-- ========== /US01-01 ========== -->
<!-- ========== US01-02: Angebote und Pakete ==========
Branch: feature_US01-02 · Styles: css/sections/angebote.css · Script: js/angebote.js
Nur innerhalb dieser <section> arbeiten. -->
<section id="angebote" aria-labelledby="angebote-title">
<div class="container">
<div class="angebote-heading">
<section id="angebote" class="offers-section">
<div class="shell section">
<div class="section-heading">
<div>
<p class="angebote-eyebrow">Deine Ernährung. Deine Begleitung.</p>
<h2 id="angebote-title">So viel Unterstützung,<br>wie du brauchst.</h2>
<p class="eyebrow">DEINE ERNÄHRUNG. DEINE BEGLEITUNG.</p>
<h2>So viel Unterstützung,<br />wie du brauchst.</h2>
</div>
<p>Ein klarer Einstieg oder eine enge Begleitung: Wähle den Rahmen, der sich für dich richtig anfühlt.</p>
<p>
Ein klarer Einstieg oder eine enge Begleitung: Wähle den Rahmen,
der sich für dich richtig anfühlt.
</p>
</div>
<div class="concept-note">
<span aria-hidden="true">ⓘ</span> Angebotskonzept · Leistungen zur
Abstimmung. Preise und Laufzeiten sind noch offen.
</div>
<p class="concept-note"><span aria-hidden="true">ⓘ</span> Angebotskonzept · Leistungen zur Abstimmung. Preise und Laufzeiten sind noch offen.</p>
<div class="package-grid">
<article class="package">
<div class="package-image">
<img src="assets/images/angebote/starter.webp" width="700" height="420" alt="Gemeinsame Planung am See für den sportlichen Einstieg" loading="lazy">
<img
src="assets/images/angebote/starter.webp"
width="700"
height="420"
alt="Gemeinsame Planung am See für den sportlichen Einstieg"
loading="lazy"
/>
</div>
<div class="package-body">
<p class="package-label">Starter</p>
<h3>Dein erster<br>guter Schritt.</h3>
<p class="package-text">Für Einsteiger und Wiedereinsteiger, die ihre Ernährung verstehen und gezielt verändern möchten.</p>
<p class="eyebrow">STARTER</p>
<h3>Dein erster<br />guter Schritt.</h3>
<p>
Für Einsteiger und Wiedereinsteiger, die ihre Ernährung
verstehen und gezielt verändern möchten.
</p>
<ul>
<li>Persönliche Bestandsaufnahme</li>
<li>Analyse deiner Essgewohnheiten</li>
<li>Konkrete Empfehlungen für den Alltag</li>
</ul>
<button type="button" class="package-button" data-package="starter">Starter entdecken <span aria-hidden="true">↗</span></button>
<button
class="button outline package-open"
data-package="starter"
>
Starter entdecken
</button>
</div>
</article>
<article class="package featured">
<div class="package-image">
<img src="assets/images/angebote/standard.webp" width="700" height="420" alt="Sportler und Coach besprechen gemeinsam einen Plan am Fahrrad" loading="lazy">
<span class="package-badge">Mit individuellem Plan</span>
<img
src="assets/images/angebote/standard.webp"
width="700"
height="420"
alt="Sportler und Coach besprechen gemeinsam einen Plan am Fahrrad"
loading="lazy"
/><span class="package-badge">MIT INDIVIDUELLEM PLAN</span>
</div>
<div class="package-body">
<p class="package-label">Standard</p>
<h3>Aus guten Vorsätzen<br>wird dein Plan.</h3>
<p class="package-text">Für alle, die Ernährung und Training aufeinander abstimmen und neue Routinen festigen möchten.</p>
<p class="eyebrow">STANDARD</p>
<h3>Aus guten Vorsätzen<br />wird dein Plan.</h3>
<p>
Für alle, die Ernährung und Training aufeinander abstimmen und
neue Routinen festigen möchten.
</p>
<ul>
<li>Alles aus Starter</li>
<li>Individueller Ernährungsrahmen</li>
<li>Feedback und gemeinsame Anpassung</li>
</ul>
<button type="button" class="package-button is-primary" data-package="standard">Standard entdecken <span aria-hidden="true">↗</span></button>
<button class="button package-open" data-package="standard">
Standard entdecken
</button>
</div>
</article>
<article class="package">
<div class="package-image">
<img src="assets/images/angebote/premium.webp" width="700" height="420" alt="Persönliches Coachinggespräch auf einer Bank neben einem Triathlonrad" loading="lazy">
<img
src="assets/images/angebote/premium.webp"
width="700"
height="420"
alt="Persönliches Coachinggespräch auf einer Bank neben einem Triathlonrad"
loading="lazy"
/>
</div>
<div class="package-body">
<p class="package-label">Premium</p>
<h3>Dein Ziel.<br>Ganz persönlich.</h3>
<p class="package-text">Für ambitionierte Ziele und alle, die sich eine besonders enge, individuelle Begleitung wünschen.</p>
<p class="eyebrow">PREMIUM</p>
<h3>Dein Ziel.<br />Ganz persönlich.</h3>
<p>
Für ambitionierte Ziele und alle, die sich eine besonders
enge, individuelle Begleitung wünschen.
</p>
<ul>
<li>Alles aus Standard</li>
<li>Verpflegungsstrategie für Wettkämpfe</li>
<li>Engmaschige Begleitung und Anpassung</li>
</ul>
<button type="button" class="package-button" data-package="premium">Premium entdecken <span aria-hidden="true">↗</span></button>
<button
class="button outline package-open"
data-package="premium"
>
Premium entdecken
</button>
</div>
</article>
</div>
<!-- Verweis auf Rabatte aus dem Coaching 1zu1 -->
<div class="coaching-banner">
<div class="bonus-symbol" aria-hidden="true">%</div>
<div>
<h3>Schon im Coaching 1zu1? Dein Vorteil kommt mit.</h3>
<p>Mit deinem Coaching-Paket sparst du bei der Ernährungsberatung: Starter 3&nbsp;%, Standard 5&nbsp;%, Premium 10&nbsp;%.</p>
<p>
Spare mit deinem bestehenden Coaching-Paket bei der
Ernährungsberatung.
</p>
</div>
<button type="button" class="bonus-button" data-package="bonus">Rabatte ansehen <span aria-hidden="true">↗</span></button>
<button id="bonus-open" class="text-link">Rabatte ansehen</button>
</div>
</div>
<!-- Detaildialog (öffnet nur per Klick, kein Pop-up beim Laden) -->
</section>
<section id="ablauf" class="shell section process">
<div class="section-heading">
<div>
<p class="eyebrow">SO LÄUFT’S AB</p>
<h2>Vom ersten Gespräch<br />zu deiner eigenen Routine.</h2>
</div>
<p>
Du musst nicht schon alles wissen. Wir beginnen bei dir und gehen
Schritt für Schritt weiter.
</p>
</div>
<ol class="steps">
<li>
<span class="step-number">01</span>
<h3>Dich kennenlernen</h3>
<p>
Wir besprechen deine Ziele, deinen Alltag und deine sportliche
Ausgangslage.
</p>
</li>
<li>
<span class="step-number">02</span>
<h3>Gewohnheiten verstehen</h3>
<p>
Ein Ernährungsprotokoll macht sichtbar, was bereits passt und wo
du ansetzen kannst.
</p>
</li>
<li>
<span class="step-number">03</span>
<h3>Deinen Weg planen</h3>
<p>
Du erhältst konkrete Empfehlungen – je nach Paket ergänzt um einen
individuellen Ernährungsrahmen.
</p>
</li>
<li>
<span class="step-number">04</span>
<h3>Umsetzen & anpassen</h3>
<p>
Du erprobst die Empfehlungen. In Standard und Premium begleiten
Feedback und Anpassungen deinen Weg.
</p>
</li>
</ol>
<div class="process-end">
<p>Der beste Plan ist der, der in dein Leben passt.</p>
<a class="text-link" href="#angebote">Dein Angebot entdecken</a>
</div>
</section>
</main>
<footer>
<div class="shell footer-main">
<a
class="brand brand-logo"
href="https://tri-hub.de/"
aria-label="TriHub Startseite"
><span class="brand-mark"
><img
src="assets/images/icons/trihub-logo.jpg"
width="1024"
height="1024"
alt="Trihub – fit und vital ab 50!" /></span
></a>
<p>Dein sportliches Zuhause ab 50.</p>
<a href="https://tri-hub.de/">Zur TriHub-Website</a>
</div>
<div class="shell footer-note">
<span>Projektprototyp · Keine Buchung oder Datenübermittlung</span
><span
>Ernährungsangebote sind Konzeptvorschläge, keine veröffentlichten
TriHub-Tarife.</span
>
</div>
</footer>
<dialog id="package-dialog" aria-labelledby="dialog-title">
<div class="dialog-inner">
<button type="button" class="close-dialog" aria-label="Details schließen">×</button>
<p id="dialog-eyebrow" class="angebote-eyebrow"></p>
<button class="close-dialog" aria-label="Details schließen">×</button>
<p id="dialog-eyebrow" class="eyebrow"></p>
<h2 id="dialog-title"></h2>
<p id="dialog-description"></p>
<div id="detail-section">
<section id="detail-section">
<h3>Deine Begleitung im Überblick</h3>
<ul id="dialog-features"></ul>
<p class="dialog-hint">Konzeptvorschlag: Leistungsumfang, Kontaktfrequenz, Laufzeit und Preis werden noch abgestimmt.</p>
</div>
<div class="discount-box">
<p class="angebote-eyebrow">Dein Coaching-Vorteil</p>
<p class="detail-note">
Konzeptvorschlag: Leistungsumfang, Kontaktfrequenz, Laufzeit und
Preis werden noch abgestimmt.
</p>
</section>
<section class="discount-box">
<p class="eyebrow">DEIN COACHING-VORTEIL</p>
<h3>Du bist schon im Coaching 1zu1?</h3>
<label for="coaching-select">Dein bestehendes Coaching-Paket</label>
<select id="coaching-select">
<label for="coaching-select">Dein bestehendes Coaching-Paket</label
><select id="coaching-select">
<option value="0">Noch kein Coaching 1zu1</option>
<option value="3">Starter-Coaching</option>
<option value="5">Standard-Coaching</option>
<option value="10">Premium-Coaching (24 oder 52 Wochen)</option>
</select>
<p id="discount-result" aria-live="polite"></p>
<p class="dialog-hint">Der Rabatt richtet sich nach deinem bestehenden Coaching 1zu1, unabhängig vom gewählten Ernährungsangebot.</p>
<a href="https://tri-hub.de/coaching-1zu1" target="_blank" rel="noopener">Coaching 1zu1 bei TriHub ansehen ↗</a>
</div>
<button type="button" class="dialog-done">Zurück zu den Angeboten</button>
<div id="discount-result" aria-live="polite"></div>
<p class="discount-help">
Der Rabatt richtet sich nach deinem bestehenden Coaching 1zu1,
unabhängig vom gewählten Ernährungsangebot. Er gilt während der
Coaching-Laufzeit auf den aktuellen Verkaufspreis der
Ernährungsberatung.
</p>
<a
href="https://tri-hub.de/coaching-1zu1"
target="_blank"
rel="noopener"
>Coaching 1zu1 bei TriHub ansehen</a
>
</section>
<button class="button dialog-done">Zurück zu den Angeboten</button>
</div>
</dialog>
</section>
<!-- ========== /US01-02 ========== -->
<!-- ========== US01-03: Ablauf sehen ==========
Branch: feature_US01-03 · Styles: css/sections/ablauf.css
Nur innerhalb dieser <section> arbeiten. -->
<section id="ablauf" aria-labelledby="ablauf-title">
<div class="container">
<div class="ablauf-heading">
<div>
<p class="ablauf-eyebrow">So läuft’s ab</p>
<h2 id="ablauf-title">Vom ersten Gespräch<br>zu deiner eigenen Routine.</h2>
</div>
<p>Du musst nicht schon alles wissen. Wir beginnen bei dir und gehen Schritt für Schritt weiter.</p>
</div>
<ol class="steps">
<li class="step">
<span class="step-number" aria-hidden="true">01</span>
<h3><span class="sr-only">Schritt 1: </span>Dich kennenlernen</h3>
<p>Wir besprechen deine Ziele, deinen Alltag und deine sportliche Ausgangslage.</p>
</li>
<li class="step">
<span class="step-number" aria-hidden="true">02</span>
<h3><span class="sr-only">Schritt 2: </span>Gewohnheiten verstehen</h3>
<p>Ein Ernährungsprotokoll macht sichtbar, was bereits passt und wo du ansetzen kannst.</p>
</li>
<li class="step">
<span class="step-number" aria-hidden="true">03</span>
<h3><span class="sr-only">Schritt 3: </span>Deinen Weg planen</h3>
<p>Du erhältst konkrete Empfehlungen – je nach Paket ergänzt um einen individuellen Ernährungsrahmen.</p>
</li>
<li class="step">
<span class="step-number" aria-hidden="true">04</span>
<h3><span class="sr-only">Schritt 4: </span>Umsetzen &amp; anpassen</h3>
<p>Du erprobst die Empfehlungen. In Standard und Premium begleiten Feedback und Anpassungen deinen Weg.</p>
</li>
</ol>
<div class="process-end">
<p>Der beste Plan ist der, der in dein Leben passt.</p>
<a class="process-link" href="#angebote">Dein Angebot entdecken <span aria-hidden="true">↑</span></a>
</div>
</div>
</section>
<!-- ========== /US01-03 ========== -->
</main>
<script src="js/main.js" defer></script>
<script src="js/angebote.js" defer></script> <!-- US01-02 -->
</body>
</body>
</html>

View File

@ -1,105 +1,115 @@
// US01-02: Paketdetails und Coaching-Rabatt im Dialog (Vorlage: Prototyp)
'use strict';
// Vollständige Paketdetails und Rabattdialog aus TriHub Design Original.
(() => {
const section = document.querySelector('#angebote');
if (!section) return;
"use strict";
const packages = {
starter: {
name: 'Starter',
title: 'Dein erster guter Schritt.',
description: 'Du möchtest wissen, wo du stehst und was du konkret verändern kannst? Starter schafft eine persönliche Grundlage für deinen Einstieg.',
name: "Starter",
title: "Dein erster guter Schritt.",
description:
"Du möchtest wissen, wo du stehst und was du konkret verändern kannst? Starter schafft eine persönliche Grundlage für deinen Einstieg.",
features: [
'Gemeinsames Erstgespräch zu Zielen, Training, Alltag und Vorlieben.',
'Auswertung eines Ernährungsprotokolls mit Blick auf deine bisherigen Gewohnheiten.',
'Persönliche Handlungsempfehlungen für Essen und Trinken im Alltag und rund um dein Training.',
'Abschlussgespräch zur Einordnung deiner nächsten Schritte.'
]
"Gemeinsames Erstgespräch zu Zielen, Training, Alltag und Vorlieben.",
"Auswertung eines Ernährungsprotokolls mit Blick auf deine bisherigen Gewohnheiten.",
"Persönliche Handlungsempfehlungen für Essen und Trinken im Alltag und rund um dein Training.",
"Abschlussgespräch zur Einordnung deiner nächsten Schritte.",
],
},
standard: {
name: 'Standard',
title: 'Aus guten Vorsätzen wird dein Plan.',
description: 'Du möchtest aus einzelnen Empfehlungen eine verlässliche Routine machen? Standard verbindet die Bestandsaufnahme mit einem individuellen Plan und persönlichem Feedback.',
name: "Standard",
title: "Aus guten Vorsätzen wird dein Plan.",
description:
"Du möchtest aus einzelnen Empfehlungen eine verlässliche Routine machen? Standard verbindet die Bestandsaufnahme mit einem individuellen Plan und persönlichem Feedback.",
features: [
'Alle vorgeschlagenen Leistungen aus Starter.',
'Ein individueller Ernährungsrahmen für Trainings- und Ruhetage.',
'Praktische Mahlzeitenideen, abgestimmt auf deine Vorlieben und deinen Alltag.',
'Feedbackgespräche, um Erfahrungen zu besprechen und den Plan gemeinsam anzupassen.'
]
"Alle vorgeschlagenen Leistungen aus Starter.",
"Ein individueller Ernährungsrahmen für Trainings- und Ruhetage.",
"Praktische Mahlzeitenideen, abgestimmt auf deine Vorlieben und deinen Alltag.",
"Feedbackgespräche, um Erfahrungen zu besprechen und den Plan gemeinsam anzupassen.",
],
},
premium: {
name: 'Premium',
title: 'Dein Ziel. Ganz persönlich.',
description: 'Du möchtest deine Ernährung gezielt auf sportliche Herausforderungen abstimmen? Premium sieht eine besonders enge Begleitung bis in die Wettkampfvorbereitung vor.',
name: "Premium",
title: "Dein Ziel. Ganz persönlich.",
description:
"Du möchtest deine Ernährung gezielt auf sportliche Herausforderungen abstimmen? Premium sieht eine besonders enge Begleitung bis in die Wettkampfvorbereitung vor.",
features: [
'Alle vorgeschlagenen Leistungen aus Standard.',
'Eine individuelle Strategie für Essen und Trinken vor, während und nach Wettkämpfen.',
'Erprobung der Verpflegung im Training und Anpassung an deine Erfahrungen.',
'Engmaschiger persönlicher Austausch und laufende Anpassungen an deine Trainingsphasen.'
]
}
"Alle vorgeschlagenen Leistungen aus Standard.",
"Eine individuelle Strategie für Essen und Trinken vor, während und nach Wettkämpfen.",
"Erprobung der Verpflegung im Training und Anpassung an deine Erfahrungen.",
"Engmaschiger persönlicher Austausch und laufende Anpassungen an deine Trainingsphasen.",
],
},
};
const dialog = section.querySelector('#package-dialog');
const select = section.querySelector('#coaching-select');
const result = section.querySelector('#discount-result');
let trigger = null;
const dialog = document.querySelector("#package-dialog");
const select = document.querySelector("#coaching-select");
let trigger;
function updateDiscount() {
const rate = Number(select.value);
const result = document.querySelector("#discount-result");
result.replaceChildren();
if (rate) {
const strong = document.createElement('strong');
strong.textContent = `${rate} % Rabatt`;
result.append(strong, document.createTextNode(' auf deine Ernährungsberatung.'));
} else {
result.textContent = 'Mit Coaching 1zu1: Starter 3 %, Standard 5 %, Premium 10 % Rabatt.';
const strong = document.createElement("strong");
strong.textContent = rate + " % Rabatt";
result.append(
strong,
document.createTextNode(" auf deine Ernährungsberatung."),
);
} else
result.textContent =
"Mit Coaching 1zu1: Starter 3 %, Standard 5 %, Premium 10 % Rabatt.";
}
}
function openDetails(key, button) {
trigger = button;
const data = packages[key];
section.querySelector('#dialog-eyebrow').textContent =
data ? `Ernährungsberatung · ${data.name}` : 'Coaching 1zu1 · Bonusregelung';
section.querySelector('#dialog-title').textContent =
data ? data.title : 'Dein Coaching zahlt sich aus.';
section.querySelector('#dialog-description').textContent = data
document.querySelector("#dialog-eyebrow").textContent = data
? "ERNÄHRUNGSBERATUNG · " + data.name.toUpperCase()
: "COACHING 1ZU1 · BONUSREGELUNG";
document.querySelector("#dialog-title").textContent = data
? data.title
: "Dein Coaching zahlt sich aus.";
document.querySelector("#dialog-description").textContent = data
? data.description
: 'Dein bestehendes Coaching-Paket bringt dir einen Preisvorteil bei der Ernährungsberatung. Wähle es aus und sieh deinen Rabatt.';
section.querySelector('#detail-section').hidden = !data;
const list = section.querySelector('#dialog-features');
: "Dein bestehendes Coaching-Paket bringt dir einen Preisvorteil bei der Ernährungsberatung. Wähle es aus und entdecke deinen Rabatt.";
document.querySelector("#detail-section").hidden = !data;
const list = document.querySelector("#dialog-features");
list.replaceChildren();
if (data) {
if (data)
data.features.forEach((text) => {
const li = document.createElement('li');
const li = document.createElement("li");
li.textContent = text;
list.append(li);
});
}
updateDiscount();
dialog.showModal();
}
section.querySelectorAll('[data-package]').forEach((button) => {
button.addEventListener('click', () => openDetails(button.dataset.package, button));
document
.querySelectorAll(".package-open")
.forEach((button) =>
button.addEventListener("click", () =>
openDetails(button.dataset.package, button),
),
);
document
.querySelector("#bonus-open")
.addEventListener("click", (event) =>
openDetails(null, event.currentTarget),
);
select.addEventListener("change", updateDiscount);
document
.querySelectorAll(".close-dialog,.dialog-done")
.forEach((button) =>
button.addEventListener("click", () => dialog.close()),
);
dialog.addEventListener("click", (event) => {
if (event.target === dialog) {
const r = dialog.getBoundingClientRect();
if (
event.clientX < r.left ||
event.clientX > r.right ||
event.clientY < r.top ||
event.clientY > r.bottom
)
dialog.close();
}
});
select.addEventListener('change', updateDiscount);
section.querySelectorAll('.close-dialog, .dialog-done').forEach((button) => {
button.addEventListener('click', () => dialog.close());
});
// Klick auf den abgedunkelten Hintergrund schließt den Dialog
dialog.addEventListener('click', (event) => {
if (event.target === dialog) dialog.close();
});
// Fokus zurück auf den auslösenden Button (Barrierefreiheit)
dialog.addEventListener('close', () => trigger?.focus());
dialog.addEventListener("close", () => trigger?.focus());
})();

View File

@ -1,3 +1,87 @@
// Einstiegspunkt für Skripte.
// Feature-Code in eigene Dateien auslagern (z. B. js/scroll.js für US01-01)
// und hier bzw. in index.html einbinden.
// Mobile Navigation und alle Scrollanimationen aus TriHub Design Original.
"use strict";
(() => {
const menu = document.querySelector(".menu-toggle"),
nav = document.querySelector("#mobile-nav");
menu.addEventListener("click", () => {
const open = menu.getAttribute("aria-expanded") !== "true";
menu.setAttribute("aria-expanded", String(open));
menu.setAttribute("aria-label", open ? "Menü schließen" : "Menü öffnen");
nav.hidden = !open;
});
nav.querySelectorAll("a").forEach((a) =>
a.addEventListener("click", () => {
nav.hidden = true;
menu.setAttribute("aria-expanded", "false");
menu.setAttribute("aria-label", "Menü öffnen");
}),
);
// Progressive enhancement: content stays visible without JS or animation support.
(() => {
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
if (!("IntersectionObserver" in window) || !Element.prototype.animate)
return;
const running = new Map();
const played = new WeakSet();
const delays = new WeakMap();
const targets = [
...document.querySelectorAll(
".hero-copy > *, .hero-visual, .section-heading, .benefit-grid > article, " +
".concept-note, .package-grid > article, .coaching-banner, .steps > li, .process-end",
),
];
document
.querySelectorAll(".hero-copy, .benefit-grid, .package-grid, .steps")
.forEach((group) => {
[...group.children].forEach((child, index) =>
delays.set(child, Math.min(index * 80, 240)),
);
});
function reveal(element) {
if (played.has(element)) return;
played.add(element);
if (reducedMotion.matches) return;
const animation = element.animate(
[
{ opacity: 0, transform: "translateY(24px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{
duration: 720,
delay: delays.get(element) || 0,
easing: "cubic-bezier(.22, 1, .36, 1)",
fill: "backwards",
},
);
running.set(element, animation);
const cleanup = () => {
if (running.get(element) === animation) running.delete(element);
};
animation.onfinish = cleanup;
animation.oncancel = cleanup;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
observer.unobserve(entry.target);
// A direct anchor jump must not animate already-scrolled-past content.
if (entry.boundingClientRect.bottom > 0) reveal(entry.target);
});
},
{ threshold: 0, rootMargin: "0px 0px -24px 0px" },
);
targets.forEach((element) => observer.observe(element));
// Keyboard users never wait for a focused control to become visible.
document.addEventListener("focusin", (event) => {
running.forEach((animation, element) => {
if (element.contains(event.target)) animation.cancel();
});
});
reducedMotion.addEventListener("change", () => {
if (reducedMotion.matches)
running.forEach((animation) => animation.cancel());
});
})();
})();