Zum Inhalt
← Hero & Section-Layouts

Produkt-UI-Hero mit Perspektiv-Tilt (Screenshot richtet sich beim Scrollen auf)

Das Dashboard eines Flussvereins liegt gekippt unter der Mission und richtet sich beim Scrollen auf, bis es frontal und lesbar steht.

Unter Headline und Call-to-Action liegt ein großer Screenshot der Produkt-Oberfläche in einem Rahmen, etwa ein Dashboard, Editor oder App-Fenster. Zu Beginn ist er um die X-Achse nach hinten gekippt (rotateX um 20°, als Klappe bis 90°). Die Oberkante liegt weiter weg als die Unterkante, dazu kommt ein leichter Zoom. Beim Scrollen richtet sich die Fläche scroll-gekoppelt auf und skaliert auf 1. Die Headline wandert dabei nach oben weg. Am Ende steht das UI frontal und lesbar im Viewport, oft mit gestaffeltem Schatten für Tiefe.

Das Aufrichten inszeniert den ersten Blick ins Produkt wie einen Vorhang, der sich hebt. Der Besucher holt sich das UI mit dem eigenen Scroll heran. Das weckt Neugier und gibt ihm Kontrolle. Die Perspektive macht einen flachen Screenshot räumlich, ganz ohne WebGL.

Wann einsetzen

Landingpages für SaaS, Apps und Tools, deren Oberfläche das stärkste Argument ist. Template-Heros für Software-Kunden. Case Studies, die ein Interface vorstellen, etwa ein Shop-Backend oder ein Buchungs-Tool. Auch Vereine und Verwaltungen mit einem öffentlichen Daten- oder Transparenz-Dashboard.

Wann nicht

Wenn es kein echtes, ansehnliches UI gibt; Mock-Screenshots wirken austauschbar. Bei Portfolios ohne Produkt. Wenn der Screenshot kleinteiligen Text zeigt, der im gekippten Zustand flimmert. Wenn der Hero schon schwer ist, etwa mit Video oder 3D.

Varianten

  • Klassisch: rotateX 20° auf 0°, scale 1.05 auf 1, Headline translateY 0 auf −100px (Aceternity Container Scroll).
  • Gepinnte Bühne: Die Headline hebt ab, der Screenshot richtet sich auf und fährt in die Mitte (Motion Screenshot Scroll Reveal).
  • Klappe: Die Fläche klappt aus 90° hoch, transform-origin oben. Bei Pitch als Sektionsübergang mit Farbverlauf statt Screenshot.
  • Nur beim Laden: einmalige Animation von gekippt auf flach, ohne Scroll-Kopplung.
  • Mobil: kleinere Skalierung (0.7 auf 0.9) und eine mobile Ansicht des Produkts statt des Desktop-Screenshots.

Umsetzung

Markup: <section class="tilt-hero"> mit Headline und <figure class="tilt-frame">. Das Elternelement bekommt perspective: 1000px (gängig sind 800 bis 1200px). Der Rahmen dreht um die Mitte, für die Klappe um die Oberkante (transform-origin: 50% 0). Nativ ohne JS: .tilt-frame { animation: aufrichten linear both; animation-timeline: view(); animation-range: entry 0% cover 50%; } @keyframes aufrichten { from { transform: rotateX(20deg) scale(1.05); } to { transform: none; } }. Mit Motion: const { scrollYProgress } = useScroll({ target: ref }); rotateX = useTransform(scrollYProgress, [0, 1], [20, 0]); scale = useTransform(scrollYProgress, [0, 1], [1.05, 1]). Mit GSAP: gsap.fromTo(frame, { rotateX: 20, scale: 1.05 }, { rotateX: 0, scale: 1, ease: 'none', scrollTrigger: { trigger: section, start: 'top top', end: 'bottom bottom', scrub: true } }). Die Demo rechnet den Fortschritt in wenigen Zeilen Vanilla-JS aus der Scroll-Position und setzt die Transforms direkt. Fallstricke: Den Screenshot in 2× Auflösung liefern, weil die Perspektive ihn dehnt. will-change: transform nur während der Animation. Text im Bild nicht als Ersatz für echten Text nutzen. Die Scroll-Strecke großzügig wählen (150 bis 200vh), sonst ist die Bewegung zu schnell.

Robust umsetzen

Telefon. Kippung auf kleinen Screens reduzieren oder durch reine Skalierung ersetzen (Aceternity: 0.7 auf 0.9 unter 768px). Den Screenshot gegen eine mobile Ansicht des Produkts tauschen. Die Scroll-Strecke verkürzen.

Ohne Bewegung. Screenshot flach und unskaliert zeigen (transform: none). Headline statisch, keine gepinnte Strecke.

Barrierefrei. Der Screenshot braucht einen aussagekräftigen Alt-Text, der sagt, was das UI zeigt. Keine Kerninformation nur im Bild. Die Fokusreihenfolge bleibt gleich, weil nur transform animiert wird.

Code

<!DOCTYPE html>
<!--
  Produkt-UI-Hero mit Perspektiv-Tilt (Screenshot richtet sich beim Scrollen auf)
  Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/produkt-ui-hero-perspektiv-tilt/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Produkt-UI-Hero mit Perspektiv-Tilt · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Big Shoulders'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/big-shoulders:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Welt: Zusammenfluss EO, Verein für lebendige Flüsse in Bozen (Rezept verein-nonprofit-wirkung) */
:root {
  --marke: #002E6D; --marke-tief: #00224f; --flaeche: #EDF5FA; --grau: #DFDFD7; --text: #222520;
  --weiss: #ffffff; --leise: rgba(255, 255, 255, .66); --linie: rgba(255, 255, 255, .16);
  --spenden: #FFCA0A;                                  /* nur der Spenden-Button */
  --blau-2: #1F5FA8; --blau-3: #5B9BD5; --blau-4: #A9CCEA; --pruefen: #C8442F;
  --kurve-ui: cubic-bezier(0.645, 0.045, 0.355, 1);   /* Rezept: InOut für Zustandswechsel */
  --kopf: 64px; --rand: clamp(16px, 3.4vw, 48px); --unten: 64px;
  --perspektive: 1000px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--marke); color: var(--weiss); font-family: 'Archivo', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { background: var(--weiss); overflow-x: clip; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

/* Kopf */
.kopf { position: fixed; inset: 0 0 auto; z-index: 20; height: var(--kopf); display: flex; align-items: center; gap: 28px; padding: 0 var(--rand); background: var(--marke); border-bottom: 1px solid var(--linie); }
.wortmarke { display: flex; align-items: center; gap: 10px; color: var(--weiss); text-decoration: none; margin-right: auto; }
.wortmarke svg { width: 26px; height: 26px; }
.wortmarke b { font: 800 22px/1 'Big Shoulders', sans-serif; text-transform: uppercase; letter-spacing: .01em; }
.wortmarke small { font: 500 10px/1.2 'Geist Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); border-left: 1px solid var(--linie); padding-left: 10px; }
.kopf nav { display: flex; gap: 26px; }
.kopf nav a { color: var(--leise); text-decoration: none; font-size: 15px; font-weight: 500; transition: color .15s var(--kurve-ui); }
.kopf nav a:hover, .kopf nav a[aria-current] { color: var(--weiss); }
.sprache { color: var(--leise); }
.spenden { display: inline-flex; align-items: center; height: 40px; padding: 0 18px; border-radius: 6px; background: var(--spenden); color: var(--text); font-weight: 600; font-size: 15px; text-decoration: none; transition: transform .15s var(--kurve-ui), background-color .15s var(--kurve-ui); }
.spenden:hover { background: #ffd43b; }
.spenden:active { transform: translateY(1px); }

/* Hero: gepinnte Bühne */
.hero { position: relative; height: 200vh; background: var(--marke); }
.buehne { position: sticky; top: 0; height: 100vh; height: 100svh; overflow: hidden; padding-top: var(--kopf); }
.fluesse { position: absolute; inset: var(--kopf) 0 0; width: 100%; height: calc(100% - var(--kopf)); pointer-events: none; }
.fluesse path { fill: none; stroke: rgba(255, 255, 255, .075); stroke-width: 1.2; }
.fluesse path.haupt { stroke: rgba(169, 204, 234, .16); stroke-width: 2; }

.hero-text { position: relative; z-index: 1; display: grid; justify-items: center; text-align: center; padding: clamp(28px, 5.5vh, 64px) var(--rand) 0; will-change: transform, opacity; }
.eyebrow { color: var(--blau-4); display: inline-flex; align-items: center; gap: 10px; }
.eyebrow::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: #7fd1a8; box-shadow: 0 0 0 4px rgba(127, 209, 168, .18); }
h1 { margin-top: 18px; font: 800 clamp(2.75rem, 1.6rem + 5vw, 6rem)/.95 'Big Shoulders', sans-serif; text-transform: uppercase; letter-spacing: .005em; text-wrap: balance; }
h1 span { color: var(--blau-4); }
.lead { margin-top: 18px; max-width: 54ch; font-size: 1.0625rem; line-height: 1.6; color: rgba(255, 255, 255, .8); text-wrap: pretty; }
.ctas { margin-top: 26px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.knopf { display: inline-flex; align-items: center; gap: 8px; height: 46px; padding: 0 20px; border-radius: 6px; font-weight: 600; font-size: 1rem; text-decoration: none; transition: background-color .15s var(--kurve-ui), color .15s var(--kurve-ui), border-color .15s var(--kurve-ui); }
.knopf--voll { background: var(--weiss); color: var(--marke); }
.knopf--voll:hover { background: var(--flaeche); }
.knopf--rand { border: 1px solid rgba(255, 255, 255, .34); color: var(--weiss); }
.knopf--rand:hover { border-color: var(--weiss); }

/* Szene (Perspektive) und Rahmen */
.szene { position: relative; z-index: 2; margin: clamp(32px, 6vh, 56px) auto 0; width: min(1180px, calc(100% - 2 * var(--rand))); height: calc(100svh - var(--kopf) - 24px - var(--unten)); max-height: 820px; perspective: var(--perspektive); container-type: inline-size; will-change: transform; }
.rahmen { width: 100%; height: 100%; padding: 8px; border-radius: 20px; background: #0a3a7d; border: 1px solid rgba(255, 255, 255, .18); transform-origin: 50% 50%;
  box-shadow: 0 0 #0000004d, 0 9px 20px #0010304a, 0 37px 37px #00103042, 0 84px 50px #00103026, 0 149px 60px #0010300a, 0 233px 65px #00103003; }
.rahmen { position: relative; }
.rahmen::after { content: ''; position: absolute; inset: 0; border-radius: inherit; background: linear-gradient(180deg, #A9CCEA 0%, #5B9BD5 38%, #1F5FA8 72%, #002E6D 100%); opacity: var(--schleier, 0); pointer-events: none; }
.rahmen.klappe { transform-origin: 50% 0; }

/* Flussbuch-Oberfläche (eigenes UI, als HTML gebaut) */
.ui { --e: clamp(9px, 1.12cqi, 13.5px); font-size: var(--e); height: 100%; border-radius: 13px; overflow: hidden; background: var(--weiss); color: var(--text); display: grid; grid-template-columns: 16em 1fr; font-variant-numeric: tabular-nums; }
.ui-tel { display: none; }
.ui-seite { background: var(--flaeche); border-right: 1px solid #d6e5ef; padding: 1.6em 1.1em; display: flex; flex-direction: column; gap: 1.8em; }
.ui-marke { display: flex; align-items: center; gap: .7em; color: var(--marke); }
.ui-marke svg { width: 1.9em; height: 1.9em; }
.ui-marke b { font: 800 1.55em/1 'Big Shoulders', sans-serif; text-transform: uppercase; }
.ui-nav { display: grid; gap: .2em; }
.ui-nav span { display: flex; align-items: center; gap: .75em; padding: .62em .8em; border-radius: .5em; color: #4a5560; font-weight: 500; }
.ui-nav span i { width: .95em; height: .95em; border: 1.5px solid currentColor; border-radius: .22em; opacity: .7; }
.ui-nav span em { margin-left: auto; font-style: normal; font-size: .85em; color: #5f6a77; }
.ui-nav .an { background: var(--weiss); color: var(--marke); font-weight: 600; box-shadow: 0 1px 2px rgba(0, 46, 109, .08); }
.ui-nav .an i { background: var(--marke); opacity: 1; }
.ui-seite-fuss { margin-top: auto; font-size: .82em; line-height: 1.5; color: #5f6a77; border-top: 1px solid #d6e5ef; padding-top: 1em; }
.ui-haupt { padding: 1.5em 1.7em; display: grid; grid-template-rows: auto auto 1fr auto; gap: 1.1em; min-height: 0; }
.ui-kopf { display: flex; align-items: flex-end; gap: 1.4em; }
.ui-kopf small { display: block; font-size: .85em; color: #5f6a77; margin-bottom: .2em; }
.ui-kopf h3 { font: 800 2.1em/1 'Big Shoulders', sans-serif; text-transform: uppercase; color: var(--marke); }
.ui-seg { margin-left: auto; display: flex; background: var(--flaeche); border-radius: .55em; padding: .22em; font-size: .9em; }
.ui-seg span { padding: .42em .9em; border-radius: .4em; color: #4a5560; }
.ui-seg .an { background: var(--weiss); color: var(--marke); font-weight: 600; box-shadow: 0 1px 2px rgba(0, 46, 109, .1); }
.ui-stand { font: 500 .78em/1.3 'Geist Mono', monospace; letter-spacing: .08em; color: #5f6a77; text-transform: uppercase; }
.ui-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: .9em; }
.kpi { border: 1px solid #e3ebf1; border-radius: .7em; padding: .95em 1em .8em; display: grid; gap: .2em; position: relative; }
.kpi small { font-size: .85em; color: #5f6a77; }
.kpi b { font: 800 2.35em/1.05 'Big Shoulders', sans-serif; color: var(--marke); letter-spacing: .01em; }
.kpi em { font-style: normal; font-size: .8em; color: #2f7a56; }
.kpi svg { position: absolute; right: .9em; top: 1.1em; width: 4.6em; height: 1.8em; }
.kpi svg polyline { fill: none; stroke: var(--blau-3); stroke-width: 1.6; }
.ui-reihe { display: grid; grid-template-columns: 1.45fr 1fr; gap: .9em; min-height: 0; }
.karte { border: 1px solid #e3ebf1; border-radius: .7em; padding: 1em 1.1em; min-height: 0; display: flex; flex-direction: column; gap: .7em; }
.karte header { display: flex; align-items: baseline; justify-content: space-between; gap: 1em; }
.karte h4 { font-size: 1.02em; font-weight: 600; color: var(--marke); }
.karte header span { font-size: .8em; color: #5f6a77; display: flex; gap: 1em; }
.punkt { display: inline-block; width: .65em; height: .65em; border-radius: 50%; background: var(--marke); margin-right: .35em; vertical-align: -.02em; }
.punkt.p { background: var(--pruefen); }
.karte-map { background: linear-gradient(var(--flaeche), var(--flaeche)) padding-box; }
.karte-map svg { flex: 1; min-height: 0; width: 100%; }
.karte-map .stadt { fill: #dcebf5; }
.karte-map .fluss { fill: none; stroke: var(--blau-3); stroke-linecap: round; }
.karte-map text { font: 600 9px/1 'Archivo', sans-serif; fill: #3d4955; }
.karte-map text.fn { font: 500 7.5px/1 'Geist Mono', monospace; letter-spacing: .12em; fill: var(--blau-2); text-transform: uppercase; }
.karte-map .mp { fill: var(--marke); stroke: #fff; stroke-width: 2; }
.karte-map .mp.p { fill: var(--pruefen); }
.karte-map .ring { fill: none; stroke: var(--pruefen); stroke-width: 1.2; opacity: .45; }
.geld-zahl { display: flex; align-items: baseline; gap: .6em; flex-wrap: wrap; }
.geld-zahl b { font: 800 2.4em/1 'Big Shoulders', sans-serif; color: var(--marke); }
.geld-zahl span { font-size: .85em; color: #5f6a77; }
.balken { display: flex; height: .9em; border-radius: .3em; overflow: hidden; gap: 2px; }
.balken i { display: block; }
.legende { display: grid; gap: .5em; font-size: .9em; }
.legende div { display: grid; grid-template-columns: .8em 1fr auto auto; align-items: center; gap: .6em; }
.legende i { width: .8em; height: .8em; border-radius: .2em; }
.legende span:last-child { color: #5f6a77; width: 3em; text-align: right; }
.karte-tab { padding-bottom: .6em; }
table { width: 100%; border-collapse: collapse; font-size: .92em; }
th { text-align: left; font: 500 .82em/1 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: #5f6a77; padding: 0 .6em .6em 0; border-bottom: 1px solid #e3ebf1; }
td { padding: .62em .6em .62em 0; border-bottom: 1px solid #eef3f7; white-space: nowrap; }
td:first-child { font-weight: 600; color: var(--marke); }
.status { display: inline-flex; align-items: center; gap: .4em; padding: .2em .6em; border-radius: 99px; background: #e6f3ec; color: #2f7a56; font-size: .88em; font-weight: 600; }
.status.p { background: #f8e6e2; color: #b33a26; }   /* Text eine Stufe dunkler als der Punkt (4,9:1) */

/* Telefon-Ansicht des Produkts (ersetzt das Dashboard unter 768 px) */
@media (max-width: 768px) {
  :root { --unten: 76px; }
  .hero { height: 170vh; }
  .kopf nav, .sprache, .wortmarke small { display: none; }
  .szene { width: min(320px, 82vw); height: min(620px, calc(100svh - var(--kopf) - 24px - var(--unten))); margin-top: -36px; }
  .rahmen { border-radius: 38px; padding: 7px; background: #0b1e3d; }
  .ui { display: none; }
  .ui-tel { --e: 12px; font-size: var(--e); display: flex; flex-direction: column; gap: 1em; height: 100%; border-radius: 31px; overflow: hidden; background: var(--weiss); color: var(--text); padding: 1.1em 1.1em 0; font-variant-numeric: tabular-nums; position: relative; }
  .tel-status { display: flex; justify-content: space-between; font: 600 .9em/1 'Archivo', sans-serif; padding: .2em .6em 0; }
  .tel-status i { width: 5.5em; height: 1.5em; border-radius: 1em; background: #0b1e3d; margin-top: -.3em; }
  .tel-kopf { display: flex; align-items: center; gap: .6em; color: var(--marke); }
  .tel-kopf svg { width: 1.7em; height: 1.7em; }
  .tel-kopf b { font: 800 1.4em/1 'Big Shoulders', sans-serif; text-transform: uppercase; }
  .tel-kopf span { margin-left: auto; font: 500 .75em/1 'Geist Mono', monospace; letter-spacing: .1em; color: #5f6a77; }
  .tel-titel h3 { font: 800 2.1em/1 'Big Shoulders', sans-serif; text-transform: uppercase; color: var(--marke); }
  .tel-titel small { font: 500 .72em/1.4 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: #5f6a77; }
  .tel-kpis { display: grid; grid-template-columns: 1fr 1fr; gap: .6em; }
  .tel-kpis .kpi { padding: .7em .8em; }
  .tel-kpis .kpi b { font-size: 2em; }
  .tel-kpis .kpi svg { display: none; }
  .ui-tel .karte { padding: .8em .9em; }
  .ui-tel .karte-map svg { height: 8.5em; flex: none; }
  .tel-liste { display: grid; gap: 0; }
  .tel-liste div { display: grid; grid-template-columns: 1fr auto; gap: .1em .6em; padding: .65em 0; border-bottom: 1px solid #eef3f7; }
  .tel-liste b { color: var(--marke); }
  .tel-liste small { color: #5f6a77; font-size: .85em; }
  .tel-tabs { position: absolute; inset: auto 0 0; display: flex; justify-content: space-around; padding: .8em .6em 1.4em; background: rgba(255, 255, 255, .94); border-top: 1px solid #e3ebf1; font-size: .8em; color: #5f6a77; }
  .tel-tabs span.an { color: var(--marke); font-weight: 700; }
}
@media (max-width: 768px) { .eyebrow .lang { display: none; } .knopf { padding: 0 15px; font-size: .95rem; height: 44px; } .ctas { gap: 8px; } }
@media (max-width: 960px) { .kopf nav { gap: 18px; } .kopf nav a:nth-child(4) { display: none; } }

/* Wirkung */
.wirkung { background: var(--weiss); color: var(--text); padding: clamp(64px, 10vw, 128px) var(--rand); }
.wirkung-kopf { display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; flex-wrap: wrap; max-width: 1180px; margin: 0 auto; }
.wirkung h2 { font: 700 clamp(1.75rem, 1.2rem + 2vw, 3rem)/1 'Big Shoulders', sans-serif; text-transform: uppercase; color: var(--marke); }
.wirkung .mono { color: #5f6a77; }
.zahlen { list-style: none; display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px; max-width: 1180px; margin: 40px auto 0; border-top: 2px solid var(--marke); }
.zahlen li { padding-top: 20px; }
.zahlen b { display: block; font: 800 clamp(3rem, 2rem + 4vw, 5.5rem)/1 'Big Shoulders', sans-serif; color: var(--marke); font-variant-numeric: tabular-nums; }
.zahlen span { display: block; margin-top: 10px; font-size: 1.0625rem; line-height: 1.5; max-width: 22ch; }
.fuss { background: var(--marke); color: var(--leise); padding: 28px var(--rand) 96px; display: flex; flex-wrap: wrap; gap: 8px 24px; justify-content: space-between; }
@media (max-width: 768px) { .zahlen { grid-template-columns: 1fr 1fr; } }

/* Fundus-Leiste: Variante und tatsächlich verwendete Werte */
.leiste { position: fixed; z-index: 30; left: var(--rand); right: var(--rand); bottom: 14px; display: flex; justify-content: space-between; align-items: center; gap: 10px; pointer-events: none; color: var(--leise); }
.leiste > * { pointer-events: auto; background: rgba(0, 25, 61, .84); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid var(--linie); border-radius: 999px; }
.werte { white-space: nowrap; font-variant-numeric: tabular-nums; padding: 0 16px; line-height: 38px; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 1180px) { .werte .breit { display: none; } }
.werte b { color: var(--weiss); font-weight: 500; }
.varianten { display: flex; gap: 2px; padding: 4px; flex: none; }
.varianten button { height: 28px; padding: 0 12px; border-radius: 999px; white-space: nowrap; color: var(--leise); letter-spacing: inherit; text-transform: inherit; font: inherit; transition: background-color .15s var(--kurve-ui), color .15s var(--kurve-ui); }
.varianten button:hover { color: var(--weiss); }
.varianten button[aria-pressed='true'] { background: var(--weiss); color: var(--marke); }
@media (max-width: 768px) {
  .leiste { flex-direction: column; gap: 6px; bottom: 10px; }
  .werte { line-height: 26px; font-size: 10px; letter-spacing: .1em; padding: 0 12px; }
  .varianten button { padding: 0 10px; font-size: 10px; height: 26px; }
}

/* Variante „Beim Laden“: keine Scroll-Strecke, Hero ist ein Bildschirm hoch */
.hero.statisch { height: 100vh; height: 100svh; }

/* Reduzierte Bewegung: Screenshot flach, Headline statisch, keine Scroll-Strecke */
.reduziert .hero { height: auto; }
.reduziert .buehne { position: relative; height: auto; overflow: visible; padding-bottom: 72px; }
.reduziert .szene { height: min(760px, 78vh); }
.reduziert .szene, .reduziert .rahmen, .reduziert .hero-text { transform: none !important; opacity: 1 !important; will-change: auto; }
.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }

/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }

@media (hover: none) { .kopf nav a:hover { color: var(--leise); } .spenden:hover { background: var(--spenden); } }
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<header class="kopf">
  <a class="wortmarke" href="#" aria-label="Zusammenfluss, Startseite">
    <svg viewBox="0 0 26 26" aria-hidden="true"><path d="M3 2c0 8 5 11 10 13M13 1v14M23 2c0 8-5 11-10 13M13 15v10" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/></svg>
    <b>Zusammenfluss</b><small>EO · Bozen</small>
  </a>
  <nav aria-label="Hauptnavigation">
    <a href="#">Projekte</a><a href="#" aria-current="page">Flussbuch</a><a href="#">Mitmachen</a><a href="#">Über uns</a>
  </nav>
  <span class="sprache mono">DE · IT</span>
  <a class="spenden" href="#">Spenden</a>
</header>

<section class="hero" data-hero aria-labelledby="titel">
  <div class="buehne">
    <svg class="fluesse" viewBox="0 0 1440 800" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <path class="haupt" d="M-40 60C180 160 300 300 420 420S640 700 760 860"/>
      <path d="M-40 100C170 196 284 330 400 446S612 720 728 870"/>
      <path d="M-40 140C160 232 268 360 380 472S584 740 696 880"/>
      <path class="haupt" d="M1480 120C1260 210 1080 300 900 360S560 420 430 430"/>
      <path d="M1480 160C1262 246 1090 332 914 390S576 456 446 462"/>
      <path class="haupt" d="M880 -40C878 80 890 220 905 358"/>
      <path d="M840 -40C840 86 852 226 866 368"/>
    </svg>

    <div class="hero-text" data-text>
      <p class="eyebrow mono">Flussbuch 2026 · offene Messdaten<span class="lang"> aus Bozen</span></p>
      <h1 id="titel">Drei Flüsse.<br><span>Ein offenes Buch.</span></h1>
      <p class="lead">Seit 2019 messen 214 Ehrenamtliche an Talfer, Eisack und Etsch, wie es dem Wasser geht. Im Flussbuch steht jede Probe. Und wofür jeder gespendete Euro ausgegeben wurde.</p>
      <div class="ctas">
        <a class="knopf knopf--voll" href="#">Flussbuch ansehen <span aria-hidden="true">→</span></a>
        <a class="knopf knopf--rand" href="#">So messen wir</a>
      </div>
    </div>

    <div class="szene" data-szene>
      <figure class="rahmen" data-rahmen role="img" aria-label="Flussbuch-Übersicht 2026: 1.284 Wasserproben an 12 Messstellen, 3,2 Tonnen geborgener Müll, 84.310 Euro Spenden verwendet. Karte der Messstellen an Talfer, Eisack und Etsch, Mittelverwendung und letzte Messungen.">
        <div class="ui" aria-hidden="true">
          <aside class="ui-seite">
            <div class="ui-marke"><svg viewBox="0 0 26 26"><path d="M3 2c0 8 5 11 10 13M13 1v14M23 2c0 8-5 11-10 13M13 15v10" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg><b>Flussbuch</b></div>
            <div class="ui-nav">
              <span class="an"><i></i>Übersicht</span>
              <span><i></i>Messstellen<em>12</em></span>
              <span><i></i>Uferputz-Einsätze<em>28</em></span>
              <span><i></i>Mittelverwendung</span>
              <span><i></i>Ehrenamt<em>214</em></span>
              <span><i></i>Berichte</span>
            </div>
            <div class="ui-seite-fuss">Öffentliche Ansicht<br>Daten frei nutzbar (CC BY 4.0)</div>
          </aside>
          <div class="ui-haupt">
            <div class="ui-kopf">
              <div><small>Übersicht</small><h3>Flussjahr 2026</h3></div>
              <div class="ui-seg"><span>Monat</span><span class="an">Jahr</span><span>Seit 2019</span></div>
              <span class="ui-stand">Stand 31.10.2026<br>18:00 Uhr</span>
            </div>
            <div class="ui-kpis">
              <div class="kpi"><small>Wasserproben</small><b>1.284</b><em>+11 % zu 2025</em><svg viewBox="0 0 46 18"><polyline points="1,15 8,12 15,13 22,9 29,10 36,5 45,3"/></svg></div>
              <div class="kpi"><small>Messstellen</small><b>12</b><em>an 3 Flüssen</em><svg viewBox="0 0 46 18"><polyline points="1,10 8,10 15,8 22,8 29,6 36,6 45,6"/></svg></div>
              <div class="kpi"><small>Müll geborgen</small><b>3,2 t</b><em>bei 28 Einsätzen</em><svg viewBox="0 0 46 18"><polyline points="1,14 8,13 15,9 22,11 29,7 36,8 45,4"/></svg></div>
              <div class="kpi"><small>Spenden verwendet</small><b>84.310 €</b><em>von 97.500 € eingenommen</em></div>
            </div>
            <div class="ui-reihe">
              <div class="karte karte-map">
                <header><h4>Messstellen</h4><span><span><i class="punkt"></i>unauffällig</span><span><i class="punkt p"></i>prüfen</span></span></header>
                <svg viewBox="0 0 400 220" preserveAspectRatio="xMidYMid meet">
                  <path class="stadt" d="M150 70c40-18 120-14 160 6 22 12 20 44-6 58-40 22-118 24-158 6-28-13-26-56 4-70z"/>
                  <path class="fluss" stroke-width="5" d="M18 8C58 54 70 96 96 132S148 196 172 216"/>
                  <path class="fluss" stroke-width="4" d="M392 30C334 58 302 84 252 102S142 124 98 134"/>
                  <path class="fluss" stroke-width="3" d="M226 0C226 34 234 70 246 103"/>
                  <text class="fn" x="34" y="66" transform="rotate(52 34 66)">Etsch</text>
                  <text class="fn" x="322" y="50" transform="rotate(-24 322 50)">Eisack</text>
                  <text class="fn" x="218" y="4" transform="rotate(84 218 4)">Talfer</text>
                  <circle class="mp" cx="44" cy="44" r="5"/><text x="54" y="47">Terlan</text>
                  <circle class="ring" cx="100" cy="136" r="11"/><circle class="mp p" cx="100" cy="136" r="5"/><text x="90" y="156" text-anchor="end">Sigmundskron</text>
                  <circle class="mp" cx="160" cy="204" r="5"/><text x="170" y="207">Pfatten</text>
                  <circle class="mp" cx="362" cy="45" r="5"/><text x="334" y="70">Kardaun</text>
                  <circle class="mp" cx="296" cy="88" r="5"/><text x="282" y="110">Kampill</text>
                  <circle class="mp" cx="229" cy="40" r="5"/><text x="240" y="30">Talferbrücke</text>
                  <circle class="mp" cx="238" cy="76" r="5"/><text x="228" y="80" text-anchor="end">Wassermauer</text>
                  <circle class="mp" cx="190" cy="114" r="5"/><text x="164" y="100">Bozen Süd</text>
                </svg>
              </div>
              <div class="karte">
                <header><h4>Mittelverwendung 2026</h4><span>Belege im Bericht</span></header>
                <div class="geld-zahl"><b>84.310 €</b><span>verwendet · 86 % der Einnahmen</span></div>
                <div class="balken"><i style="flex:34;background:var(--marke)"></i><i style="flex:22;background:var(--blau-2)"></i><i style="flex:19;background:var(--blau-3)"></i><i style="flex:16;background:var(--blau-4)"></i><i style="flex:9;background:var(--grau)"></i></div>
                <div class="legende">
                  <div><i style="background:var(--marke)"></i><span>Messkits und Labor</span><span>28.665 €</span><span>34 %</span></div>
                  <div><i style="background:var(--blau-2)"></i><span>Uferputz-Einsätze</span><span>18.548 €</span><span>22 %</span></div>
                  <div><i style="background:var(--blau-3)"></i><span>Schulprogramm Flusskinder</span><span>16.019 €</span><span>19 %</span></div>
                  <div><i style="background:var(--blau-4)"></i><span>Renaturierung Kampill</span><span>13.490 €</span><span>16 %</span></div>
                  <div><i style="background:var(--grau)"></i><span>Verwaltung</span><span>7.588 €</span><span>9 %</span></div>
                </div>
              </div>
            </div>
            <div class="karte karte-tab">
              <header><h4>Letzte Messungen</h4><span>alle 1.284 anzeigen →</span></header>
              <table>
                <thead><tr><th>Messstelle</th><th>Fluss</th><th>pH</th><th>Nitrat</th><th>Trübung</th><th>Status</th><th>Gemessen von</th><th>Zeit</th></tr></thead>
                <tbody>
                  <tr><td>Wassermauer</td><td>Talfer</td><td>7,9</td><td>3,1 mg/l</td><td>klar</td><td><span class="status">unauffällig</span></td><td>Lena M.</td><td>heute, 11:42</td></tr>
                  <tr><td>Sigmundskron</td><td>Etsch</td><td>7,7</td><td>6,8 mg/l</td><td>trüb</td><td><span class="status p">prüfen</span></td><td>Andreas K.</td><td>heute, 09:15</td></tr>
                  <tr><td>Kardaun</td><td>Eisack</td><td>8,1</td><td>4,6 mg/l</td><td>leicht trüb</td><td><span class="status">unauffällig</span></td><td>Klasse 3B, Gries</td><td>gestern</td></tr>
                  <tr><td>Pfatten</td><td>Etsch</td><td>7,8</td><td>5,2 mg/l</td><td>klar</td><td><span class="status">unauffällig</span></td><td>Giulia R.</td><td>29.10.</td></tr>
                </tbody>
              </table>
            </div>
          </div>
        </div>

        <div class="ui-tel" aria-hidden="true">
          <div class="tel-status"><span>9:41</span><i></i><span>5G</span></div>
          <div class="tel-kopf"><svg viewBox="0 0 26 26"><path d="M3 2c0 8 5 11 10 13M13 1v14M23 2c0 8-5 11-10 13M13 15v10" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg><b>Flussbuch</b><span>DE</span></div>
          <div class="tel-titel"><h3>Flussjahr 2026</h3><small>Stand 31.10.2026 · 18:00</small></div>
          <div class="tel-kpis">
            <div class="kpi"><small>Wasserproben</small><b>1.284</b><em>+11 %</em></div>
            <div class="kpi"><small>Müll geborgen</small><b>3,2 t</b><em>28 Einsätze</em></div>
          </div>
          <div class="karte">
            <header><h4>Mittelverwendung</h4><span>84.310 €</span></header>
            <div class="balken"><i style="flex:34;background:var(--marke)"></i><i style="flex:22;background:var(--blau-2)"></i><i style="flex:19;background:var(--blau-3)"></i><i style="flex:16;background:var(--blau-4)"></i><i style="flex:9;background:var(--grau)"></i></div>
            <div class="legende"><div><i style="background:var(--marke)"></i><span>Messkits und Labor</span><span></span><span>34 %</span></div><div><i style="background:var(--blau-2)"></i><span>Uferputz</span><span></span><span>22 %</span></div></div>
          </div>
          <div class="tel-liste">
            <div><b>Wassermauer · Talfer</b><span class="status">unauffällig</span><small>pH 7,9 · Nitrat 3,1 mg/l · Lena M.</small></div>
            <div><b>Sigmundskron · Etsch</b><span class="status p">prüfen</span><small>pH 7,7 · Nitrat 6,8 mg/l · Andreas K.</small></div>
            <div><b>Kardaun · Eisack</b><span class="status">unauffällig</span><small>pH 8,1 · Nitrat 4,6 mg/l · Klasse 3B</small></div>
          </div>
          <nav class="tel-tabs"><span class="an">Übersicht</span><span>Karte</span><span>Spenden</span><span>Ich</span></nav>
        </div>
      </figure>
    </div>
  </div>
</section>

<section class="wirkung" aria-labelledby="wirkung-titel">
  <div class="wirkung-kopf">
    <h2 id="wirkung-titel">Wirkung 2025</h2>
    <p class="mono">Stand: Jahresbericht 2025</p>
  </div>
  <ul class="zahlen">
    <li><b>1.106</b><span>Wasserproben an 12 Messstellen</span></li>
    <li><b>214</b><span>Ehrenamtliche an Talfer, Eisack und Etsch</span></li>
    <li><b>41</b><span>Schulklassen im Programm Flusskinder</span></li>
    <li><b>2,7 t</b><span>Müll aus Ufer und Wasser geborgen</span></li>
  </ul>
</section>
<footer class="fuss mono"><span>Zusammenfluss EO · Verein für lebendige Flüsse · Bozen</span><span>Erfundene Organisation · Fundus-Demo</span></footer>

<div class="leiste mono" data-leiste>
  <p class="werte" data-werte>rotateX <b>20°</b></p>
  <div class="varianten" role="group" aria-label="Variante">
    <button type="button" data-variante="klassisch" aria-pressed="true">Klassisch</button>
    <button type="button" data-variante="klappe" aria-pressed="false">Klappe 90°</button>
    <button type="button" data-variante="laden" aria-pressed="false">Beim Laden</button>
  </div>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const hero = document.querySelector('[data-hero]');
  const text = document.querySelector('[data-text]');
  const szene = document.querySelector('[data-szene]');
  const rahmen = document.querySelector('[data-rahmen]');
  const werte = document.querySelector('[data-werte]');
  const mobil = matchMedia('(max-width: 768px)');

  // Werte aus den Messwerten (Aceternity Container Scroll, Pitch ScrollGradient)
  const VARIANTEN = {
    klassisch: { rot: 20, s0: 1.05, s1: 1, s0m: 0.7, s1m: 0.9, origin: '50% 50%' },
    klappe:    { rot: 90, s0: 1,    s1: 1, s0m: 1,   s1m: 1,   origin: '50% 0%' },
    laden:     { rot: 20, s0: 1.05, s1: 1, s0m: 1.05, s1m: 1,  origin: '50% 50%', dauer: 1000, kurve: 'cubic-bezier(0.645, 0.045, 0.355, 1)' }
  };
  const TEXT_Y = 100;               // Headline translateY 0 → −100px
  const ABSTAND_OBEN = 24;          // Endlage: 24 px unter dem Kopf
  let modus = 'klassisch', hub = 0, p = 0, offen = false, ani = null;

  const fix = (n, d = 1) => n.toFixed(d).replace('.', ',');
  const klemm = n => Math.min(1, Math.max(0, n));

  function messen() {
    // Hub = Weg vom Startplatz unter dem Text bis zur Endlage unter dem Kopf
    szene.style.transform = 'none';
    const kopf = parseFloat(getComputedStyle(html).getPropertyValue('--kopf')) || 64;
    hub = Math.max(0, szene.offsetTop - kopf - ABSTAND_OBEN);
    zeichnen();
  }

  function fortschritt() {
    const strecke = hero.offsetHeight - innerHeight;
    return strecke > 0 ? klemm(-hero.getBoundingClientRect().top / strecke) : 0;
  }

  function zeichnen() {
    const v = VARIANTEN[modus];
    rahmen.classList.toggle('klappe', modus === 'klappe');
    if (reduziert()) {
      szene.style.transform = rahmen.style.transform = text.style.transform = text.style.opacity = '';
      rahmen.style.setProperty('--schleier', '0');
      werte.innerHTML = 'Reduzierte Bewegung · <b>transform: none</b>';
      return;
    }
    if (modus === 'laden') {
      szene.style.transform = text.style.transform = text.style.opacity = '';
      rahmen.style.setProperty('--schleier', '0');
      werte.innerHTML = `Einmalig · rotateX <b>20° → 0°</b> · scale <b>1,05 → 1</b> · <b>${v.dauer} ms</b><span class="breit"> · <b>cubic-bezier(.645, .045, .355, 1)</b></span> · InOut`;
      return;
    }
    p = fortschritt();
    const m = mobil.matches;
    const s0 = m ? v.s0m : v.s0, s1 = m ? v.s1m : v.s1;
    const rot = v.rot * (1 - p), s = s0 + (s1 - s0) * p;
    const ty = -TEXT_Y * p;
    const deck = 1 - klemm((p - 0.2) / 0.55);
    text.style.transform = `translate3d(0, ${ty.toFixed(1)}px, 0)`;
    text.style.opacity = deck.toFixed(3);
    szene.style.transform = `translate3d(0, ${(-hub * p).toFixed(1)}px, 0)`;
    rahmen.style.transform = `rotateX(${rot.toFixed(2)}deg) scale(${s.toFixed(4)})`;
    // Klappe: steile Fläche trägt einen Farbverlauf (wie Pitch), das UI tritt erst beim Aufrichten hervor
    rahmen.style.setProperty('--schleier', modus === 'klappe' ? klemm((rot - 30) / 40).toFixed(3) : '0');
    werte.innerHTML = m
      ? `rotateX <b>${fix(rot)}°</b> · scale <b>${fix(s, 3)}</b> · <b>${Math.round(p * 100)} %</b>`
      : `Scroll <b>${String(Math.round(p * 100)).padStart(3, '\u2007')} %</b> · rotateX <b>${fix(rot)}°</b> · scale <b>${fix(s, 3)}</b> · Headline <b>${ty < -0.5 ? '\u2212' : ''}${Math.abs(Math.round(ty))} px</b><span class="breit"> · perspective <b>1000px</b> · linear</span>`;
  }

  let rafAn = false;
  addEventListener('scroll', () => { if (!rafAn) { rafAn = true; requestAnimationFrame(() => { rafAn = false; zeichnen(); }); } }, { passive: true });
  addEventListener('resize', messen);
  mobil.addEventListener('change', messen);
  new MutationObserver(() => { const r = reduziert(); if (r !== offen) { offen = r; messen(); } }).observe(html, { attributes: true, attributeFilter: ['class'] });
  offen = reduziert();

  function laden() {
    if (ani) ani.cancel();
    if (reduziert()) return;
    const v = VARIANTEN.laden;
    ani = rahmen.animate([{ transform: `rotateX(${v.rot}deg) scale(${v.s0})` }, { transform: 'rotateX(0deg) scale(1)' }],
      { duration: v.dauer, easing: v.kurve, fill: 'both', delay: 200 });
  }

  function setzen(neu) {
    modus = neu;
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === modus)));
    hero.classList.toggle('statisch', modus === 'laden');
    if (ani) { ani.cancel(); ani = null; }
    rahmen.style.transform = '';
    scrollTo(0, 0);
    messen();
    if (modus === 'laden') laden();
  }
  document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { const lief = !!uhr; pause(); setzen(b.dataset.variante); if (lief) vorfuehren(); }));

  // Vorführung: Selbst-Scroll durch die Strecke und zurück (bzw. Laden wiederholen)
  let uhr = null, t0 = 0;
  const RUNDE = 8400;
  const weich = q => 0.5 - 0.5 * Math.cos(klemm(q) * Math.PI);
  function vorfuehren() {
    pause(); scrollTo(0, 0); messen();
    if (modus === 'laden') { laden(); uhr = setInterval(laden, 3200); return; }
    t0 = performance.now();
    const schritt = t => {
      const r = ((t - t0) % RUNDE) / RUNDE;
      let q;
      if (r < 0.08) q = 0;
      else if (r < 0.56) q = weich((r - 0.08) / 0.48);
      else if (r < 0.72) q = 1;
      else if (r < 0.94) q = 1 - weich((r - 0.72) / 0.22);
      else q = 0;
      scrollTo(0, (hero.offsetHeight - innerHeight) * q);
      uhr = requestAnimationFrame(schritt);
    };
    uhr = requestAnimationFrame(schritt);
  }
  function pause() { cancelAnimationFrame(uhr); clearInterval(uhr); uhr = null; }
  function neustart() { pause(); setzen(modus); }
  function weiter() { if (!uhr) vorfuehren(); }
  ['wheel', 'touchstart', 'keydown'].forEach(e => addEventListener(e, () => { if (uhr && !html.classList.contains('vorschau')) pause(); }, { passive: true }));

  if (document.fonts && document.fonts.ready) document.fonts.ready.then(messen);
  messen();

})();
</script>
</body>
</html>