Case-Study-Gerüst auf Award-Niveau
Eine Projektseite als feste Folge aus Hero mit sechs Metadaten, Lead, Medien-Modulen, Ergebnis, Credits und „Nächstes Projekt“, dessen Bild und Titel per View Transition zum Hero der nächsten Case Study werden.
Feste Abfolge für Projektseiten.
- Hero: Projektname groß, darunter vier bis sechs Metadaten (Kunde, Jahr, Rolle, Leistungen, Team, Link) und ein randloses Hauptbild oder Video.
- Einstieg: Aufgabe in zwei, drei Sätzen als großer Lead.
- Wechsel aus randlosen Bildern, Bildpaaren, Detailausschnitten und kurzen Textblöcken mit Kapitelnummern, dazu Einblicke in den Prozess wie Skizzen oder Varianten.
- Ergebnis mit Zahlen oder Zitat.
- Credits für Team und Partner.
- „Nächstes Projekt“ als großes Bild mit Titel, das per Klick oder Scroll direkt in die nächste Case Study führt.
Die Seite erzählt eine Geschichte statt eine Bildergalerie zu zeigen. Die Metadaten geben auf einen Blick Kontext. Weil Bild und Titel von „Nächstes Projekt“ zum Hero der nächsten Seite werden, bleibt man im Fluss und liest weiter.
Wann einsetzen
Für jede Projektseite eines Portfolios, Studios oder einer Agentur.
Wann nicht
Bei kleinen Projekten ohne Geschichte. Die gehören als Eintrag in den Projektindex oder in eine Galerie.
Varianten
- Langform-Editorial mit Kapiteln
- Bildgetriebene Galerie mit wenig Text
- Einheitliches Template für alle Projekte (gleiche Module, gleiches Raster)
- Video-Case-Study als Slider
- Endlos-Übergang: Weiterscrollen am Seitenende öffnet das nächste Projekt
Umsetzung
Ein Template mit festen Bausteinen: Hero, Meta, Lead, Medien-Module (randlos, Paar, Detail, Text mit Bild, Zitat, Zahlen), Credits und „Nächstes Projekt“. Die Module stehen in einem 12er-Raster mit wenigen festen Breiten. Den Übergang zur nächsten Case Study übernehmen Cross-Document View Transitions: @view-transition { navigation: auto; } auf beiden Seiten, dazu view-transition-name auf Bild und Titel von „Nächstes Projekt“ (alte Seite) und auf Hero-Bild und H1 (neue Seite). Das läuft in Chrome und Safari, andere Browser wechseln ohne Animation. Die Endlos-Variante misst den Restweg am Seitenende (z. B. mit ScrollTrigger) und navigiert bei 100 %. Ohne Seitenwechsel geht derselbe Effekt mit document.startViewTransition() oder GSAP Flip. Eine llms.txt (wie bei Locomotive gesehen) ist optional und ohne belegte Wirkung bei Google; für AI Overviews und AI Mode braucht es laut Google keine neuen Dateien. Für Auffindbarkeit zählen eine eigene URL je Projekt, klare Überschriften und strukturierte Daten passend zum sichtbaren Text.
Robust umsetzen
Telefon. Bildpaare und Textspalten stapeln, Metadaten als zweispaltige Liste. „Nächstes Projekt“ ist ein großer Tap-Bereich, der Übergang darf kürzer sein (z. B. 700 ms).
Ohne Bewegung. Seitenwechsel als kurzer Crossfade oder ganz ohne Animation, keine Einblendungen beim Scrollen.
Barrierefrei. Je Kapitel eine Überschrift in sauberer Hierarchie (H1 Projekt, H2 Kapitel). Bilder mit aussagekräftigem alt, das sagt, was das Detail zeigt. Videos mit Steuerung und Untertiteln. Automatisches Weiterblättern am Seitenende nur mit klarer Ankündigung, besser per Klick. Nach dem Wechsel den Fokus auf die neue H1 setzen.
Code
<!DOCTYPE html>
<!--
Case-Study-Gerüst auf Award-Niveau
Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/case-study-geruest/
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>Case-Study-Gerüst auf Award-Niveau · 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: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; 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; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
/* Echte Website (mehrere Seiten): Projektseiten tragen
@view-transition { navigation: auto; }
und view-transition-name: case-bild / case-titel auf dem Bild und Titel von „Nächstes Projekt“
(alte Seite) und auf Hero-Bild und Titel (neue Seite). Diese Demo spielt denselben Wechsel
innerhalb einer Datei mit document.startViewTransition(); die ::view-transition-Regeln gelten 1:1. */
:root {
--papier: #edeae3; --tinte: #161513; --leise: rgba(22, 21, 19, .66); --linie: rgba(22, 21, 19, .14);
--nacht: #151412; --hell: #f1eee7; --hell-leise: rgba(241, 238, 231, .56); --hell-linie: rgba(241, 238, 231, .16);
--seite-kurve: cubic-bezier(0.76, 0, 0.24, 1); /* power3.inOut: Seitenübergang */
--auf-kurve: cubic-bezier(0.16, 1, 0.3, 1); /* expo.out: Inhalte kommen an */
--morph: 900ms; /* JS setzt 700ms unter 640 px Breite */
--blende: 600ms;
--rand: clamp(16px, 3.4vw, 48px);
--spalt: clamp(12px, 1.6vw, 24px);
--abstand: clamp(80px, 13vh, 168px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; overflow-x: clip; }
a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; height: auto; }
em { font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-weight: 400; }
.mono { font: 500 11px/1.45 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.raster { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--spalt); padding-inline: var(--rand); }
/* ---------- Kopfleiste ---------- */
.nav { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; height: 64px; padding: 0 var(--rand); border-bottom: 1px solid var(--linie); }
.nav > :last-child { justify-self: end; }
.marke { font-weight: 600; font-size: 15px; letter-spacing: -.01em; display: inline-flex; align-items: center; gap: .55em; }
.marke svg { width: 15px; height: 15px; }
.nav .mono b { color: var(--tinte); font-weight: 500; }
.lesen { position: fixed; z-index: 30; inset: 0 0 auto; height: 2px; background: #fff; mix-blend-mode: difference; transform-origin: 0 50%; transform: scaleX(var(--lesen, 0)); pointer-events: none; }
/* ---------- 1) Hero: Titel, Metadaten, randloses Bild ---------- */
.held__kopf { padding-top: clamp(28px, 6vh, 72px); row-gap: 18px; align-items: end; }
.kicker { grid-column: 1 / -1; }
.titel { grid-column: 1 / 10; font-size: clamp(48px, 8.6vw, 136px); font-weight: 600; line-height: .9; letter-spacing: -.05em; text-wrap: balance; hyphens: manual; }
.titel em { letter-spacing: -.02em; }
.titel:focus { outline: none; }
.claim { grid-column: 10 / 13; font-size: clamp(16px, 1.2vw, 19px); line-height: 1.45; color: var(--leise); padding-bottom: .5em; text-wrap: pretty; }
.meta { margin-top: clamp(28px, 5vh, 56px); row-gap: 16px; }
.meta > div { grid-column: span 2; display: grid; align-content: start; gap: 7px; padding-top: 12px; border-top: 1px solid var(--linie); }
.meta dd { font: 400 15px/1.38 'Instrument Sans', system-ui, sans-serif; letter-spacing: -.005em; text-transform: none; color: var(--tinte); }
.meta .link { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.held__bild { margin-top: clamp(20px, 3.4vh, 36px); height: clamp(300px, 68svh, 760px); background: var(--farbe); overflow: hidden; }
.held__bild img, .weiter__bild img { width: 100%; height: 100%; object-fit: contain; }
.unterzeile { display: flex; justify-content: space-between; gap: 16px; padding: 12px var(--rand) 0; }
/* Ankunft: Kicker, Claim, Metadaten steigen nacheinander auf */
.held [data-an] { animation: steigen 1000ms var(--auf-kurve) both; animation-delay: calc(300ms + var(--i, 0) * 60ms); }
@keyframes steigen { from { opacity: 0; translate: 0 18px; } }
/* ---------- Kapitel und Textblöcke ---------- */
.kapitel { margin-top: var(--abstand); row-gap: 22px; align-items: start; }
.kapitel__nr { grid-column: 1 / 4; padding-top: .7em; display: flex; gap: 1.2em; }
.kapitel__nr b { color: var(--tinte); font-weight: 500; }
.lead { grid-column: 4 / 12; font-size: clamp(24px, 2.55vw, 40px); line-height: 1.16; letter-spacing: -.022em; text-wrap: pretty; }
.spalten { grid-column: 4 / 12; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px var(--spalt); font-size: clamp(16px, 1.15vw, 18px); line-height: 1.58; text-wrap: pretty; }
/* ---------- Medien-Module (wenige feste Breiten im 12er-Raster) ---------- */
.modul { margin-top: calc(var(--abstand) * .55); row-gap: 12px; }
.modul + .modul { margin-top: var(--spalt); }
figcaption { display: flex; justify-content: space-between; gap: 16px; margin-top: 12px; }
figcaption b { color: var(--tinte); font-weight: 500; }
/* Paar: Plakat + Becher */
.paar > figure { grid-column: span 6; }
.plakat { aspect-ratio: 4 / 5; background: var(--farbe); color: var(--auf); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; padding: 7%; overflow: hidden; }
.plakat__kopf { display: flex; justify-content: space-between; color: inherit; opacity: .78; }
.plakat img { width: 100%; height: 100%; object-fit: contain; }
.plakat__fuss { font-size: clamp(18px, 2vw, 30px); font-weight: 600; letter-spacing: -.03em; line-height: 1.05; }
.plakat__fuss span { display: block; margin-top: 10px; color: inherit; opacity: .7; }
.szene { position: relative; aspect-ratio: 4 / 5; background: var(--flaeche); overflow: hidden; }
.szene::before { content: ''; position: absolute; inset: 58% 0 0; background: linear-gradient(rgba(0, 0, 0, .07), rgba(0, 0, 0, 0) 40%); }
.becher { position: absolute; left: 50%; bottom: 20%; width: 38%; translate: -50% 0; }
.becher__deckel { position: relative; z-index: 1; width: 110%; margin-left: -5%; aspect-ratio: 11 / 1; border-radius: 7px 7px 3px 3px; background: color-mix(in srgb, var(--farbe) 82%, #000); box-shadow: inset 0 -3px 0 rgba(0, 0, 0, .18); }
.becher__koerper { position: relative; aspect-ratio: .8; background: var(--farbe); clip-path: polygon(0 0, 100% 0, 87% 100%, 13% 100%); overflow: hidden; }
.becher__koerper img { position: absolute; left: -10%; top: 22%; width: 120%; max-width: none; aspect-ratio: 3 / 2; object-fit: cover; }
.becher__koerper::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(0, 0, 0, .3), rgba(255, 255, 255, .1) 30%, rgba(255, 255, 255, 0) 52%, rgba(0, 0, 0, .32)); }
.becher__schatten { position: absolute; left: 50%; bottom: calc(20% - 16px); width: 52%; height: 32px; translate: -50% 0; background: radial-gradient(closest-side, rgba(0, 0, 0, .34), rgba(0, 0, 0, 0)); }
/* Skizze: Konstruktion über dem Zeichen */
.skizze > figure { grid-column: 3 / 13; }
.skizze__flaeche { position: relative; aspect-ratio: 3 / 2; background: #f6f4ef; overflow: hidden; }
.skizze__flaeche img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(1) var(--umkehr, ) contrast(1.15); opacity: .34; mix-blend-mode: multiply; }
.skizze__flaeche svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.skizze svg text { font: 500 15px 'Geist Mono', ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; fill: rgba(22, 21, 19, .62); }
/* Detail: randloser Ausschnitt */
.detail { padding-inline: 0; }
.detail > figure { grid-column: 1 / -1; }
.detail__flaeche { aspect-ratio: 21 / 9; overflow: hidden; background: var(--farbe); }
.detail__flaeche img { width: 100%; height: 100%; object-fit: cover; transform: scale(var(--zoom)); transform-origin: var(--ursprung); }
.detail figcaption { padding-inline: var(--rand); display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--spalt); }
.detail figcaption > :first-child { grid-column: 1 / 4; }
.detail figcaption > :last-child { grid-column: 4 / 10; font: 400 clamp(16px, 1.15vw, 18px)/1.5 'Instrument Sans', sans-serif; letter-spacing: 0; text-transform: none; color: var(--tinte); }
/* System: Farben und Schrift */
.system { align-items: stretch; }
.farben { grid-column: 1 / 8; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); }
.farbfeld { aspect-ratio: 3 / 4; padding: 16px; display: flex; flex-direction: column; justify-content: space-between; background: var(--f); color: var(--t); }
.farbfeld .mono { color: inherit; opacity: .8; }
.farbfeld b { font-size: clamp(17px, 1.5vw, 22px); font-weight: 600; letter-spacing: -.02em; }
.schrift { container-type: inline-size; grid-column: 8 / 13; background: #e2ded5; padding: clamp(16px, 2vw, 28px); display: flex; flex-direction: column; justify-content: space-between; gap: 24px; min-height: 100%; }
.schrift__probe { font-size: var(--gr, 20cqi); line-height: .95; white-space: nowrap; }
.schrift__abc { font-size: clamp(15px, 1.15vw, 18px); line-height: 1.5; color: var(--leise); }
/* Ergebnis: Zahlen + Zitat */
.zahlen { grid-column: 4 / 13; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--spalt); }
.zahlen > div { border-top: 1px solid var(--linie); padding-top: 14px; display: grid; gap: 10px; }
.zahlen dd { order: -1; font-size: clamp(56px, 7.2vw, 116px); font-weight: 600; letter-spacing: -.055em; line-height: .86; }
.zitat { grid-column: 4 / 11; margin-top: clamp(28px, 6vh, 64px); }
.zitat p { font: italic 400 clamp(28px, 3.3vw, 52px)/1.08 'Instrument Serif', Georgia, serif; letter-spacing: -.01em; text-wrap: balance; }
.zitat footer { margin-top: 18px; }
/* Credits */
.credits { grid-column: 4 / 12; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--spalt); }
.credits > div { display: grid; grid-template-columns: 11em 1fr; gap: 12px; padding: 11px 0; border-top: 1px solid var(--linie); }
.credits dd { font-size: 15px; }
/* Einblenden beim Scrollen */
.js:not(.reduziert) .auf { opacity: 0; translate: 0 32px; transition: opacity 1000ms var(--auf-kurve), translate 1000ms var(--auf-kurve); }
.js:not(.reduziert) .auf.da { opacity: 1; translate: 0 0; }
/* ---------- 6) Nächstes Projekt ---------- */
.weiter { margin-top: var(--abstand); background: var(--nacht); color: var(--hell); --p: 0; }
.weiter .mono { color: var(--hell-leise); }
.weiter__link { display: grid; grid-template-rows: auto auto 1fr auto; row-gap: 22px; min-height: 100svh; padding: clamp(24px, 5vh, 56px) 0 calc(var(--rand) + 104px); outline-offset: -6px; }
.weiter__link:focus-visible { outline: 2px solid var(--hell); }
.weiter__kopf { display: flex; justify-content: space-between; gap: 16px; padding-inline: var(--rand); border-top: 1px solid var(--hell-linie); padding-top: 14px; margin-inline: var(--rand); padding-inline: 0; }
.weiter__titelzeile { align-items: end; }
.weiter__titelzeile .titel { color: var(--hell); }
.weiter__bild { align-self: end; justify-self: center; width: calc(62% + 38% * var(--p)); height: clamp(220px, 46svh, 520px); background: var(--farbe); overflow: hidden; }
.weiter__bild img { transition: transform 1000ms var(--auf-kurve); }
.weiter__fuss { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px 24px; padding-inline: var(--rand); }
.weiter__fuss .pfeil { color: var(--hell); display: inline-block; transition: translate 150ms linear; }
.balken { grid-column: 1 / -1; height: 1px; background: var(--hell-linie); display: none; }
.balken i { display: block; height: 100%; background: var(--hell); transform-origin: 0 50%; transform: scaleX(var(--p)); }
@media (hover: hover) {
.weiter__link:hover .weiter__bild img { transform: scale(1.04); }
.weiter__link:hover .pfeil { translate: 6px 0; }
}
/* ---------- Varianten ---------- */
/* Galerie: Minimaltext, Bilder tragen die Geschichte */
[data-form='galerie'] .nur-text { display: none; }
[data-form='galerie'] .modul { margin-top: var(--spalt); }
[data-form='galerie'] .held + .modul { margin-top: clamp(40px, 7vh, 80px); }
[data-form='galerie'] .skizze > figure { grid-column: 1 / -1; }
/* Endlos: der Block bleibt stehen, Weiterscrollen füllt den Balken, bei 100 % öffnet die nächste Case Study */
[data-form='endlos'] .weiter { height: calc(100svh + 80svh); }
[data-form='endlos'] .weiter__link { position: sticky; top: 0; height: 100svh; min-height: 0; }
[data-form='endlos'] .balken { display: block; }
.hinweis-endlos, [data-form='endlos'] .hinweis-klick { display: none; }
[data-form='endlos'] .hinweis-endlos { display: inline; }
/* ---------- View Transition (gilt 1:1 für @view-transition zwischen echten Seiten) ---------- */
::view-transition-group(case-bild), ::view-transition-group(case-titel) { animation-duration: var(--morph); animation-timing-function: var(--seite-kurve); }
::view-transition-group(case-bild) { overflow: clip; }
::view-transition-old(case-bild), ::view-transition-new(case-bild) { height: 100%; object-fit: cover; animation-duration: var(--morph); animation-timing-function: var(--seite-kurve); }
::view-transition-old(case-titel), ::view-transition-new(case-titel) { height: 100%; animation-duration: var(--blende); animation-timing-function: var(--seite-kurve); }
::view-transition-old(root) { animation: vt-raus var(--blende) var(--seite-kurve) both; }
::view-transition-new(root) { animation: vt-rein var(--blende) var(--seite-kurve) both; }
::view-transition-group(dock), ::view-transition-group(lesen) { animation: none; }
@keyframes vt-raus { to { opacity: 0; transform: translateY(-4%); } }
@keyframes vt-rein { from { opacity: 0; transform: translateY(3%); } }
/* ---------- Demo-Steuerung ---------- */
.dock { view-transition-name: dock; position: fixed; z-index: 40; right: 16px; bottom: 16px; width: max-content; /* rechts unten statt mittig: mittig lag es auf der Wortmarke im Hero-Bild */ max-width: calc(100% - 24px);
display: grid; gap: 8px; padding: 10px 12px; border-radius: 14px; color: #f1eee7;
background: rgba(20, 19, 17, .8); -webkit-backdrop-filter: blur(14px) saturate(1.2); backdrop-filter: blur(14px) saturate(1.2); box-shadow: 0 10px 40px rgba(0, 0, 0, .22); }
.dock__zeile { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dock .mono { color: rgba(241, 238, 231, .58); }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: rgba(241, 238, 231, .72); background: none; border: 1px solid rgba(241, 238, 231, .16); border-radius: 999px; height: 28px; padding: 0 12px; cursor: pointer; transition: background-color .15s linear, color .15s linear; }
.modi button:hover { color: #f1eee7; }
.modi button[aria-pressed='true'] { background: #f1eee7; color: #121110; border-color: #f1eee7; }
.modi button:focus-visible { outline: 2px solid #f1eee7; outline-offset: 2px; }
.werte { font-size: 10px; line-height: 1.6; letter-spacing: .06em; text-transform: none; }
.werte > span { display: block; white-space: nowrap; }
.werte b { color: #f1eee7; font-weight: 500; }
/* Eingeklappt als Chip unten rechts: aufgeklappt lag das Feld auf der Wortmarke im Hero-Bild (Desktop) bzw. auf Bild und Text (Telefon) */
.dock { padding: 0; gap: 0; border-radius: 999px; }
.dock__zeile, .dock .werte { display: none; }
.dock__knopf { display: inline-flex; align-items: center; justify-self: end; gap: 10px; height: 36px; padding: 0 14px; color: rgba(241, 238, 231, .62); background: none; border: 0; border-radius: 999px; cursor: pointer; }
.dock__knopf b { color: #f1eee7; font-weight: 500; }
.dock__knopf::after { content: '+'; font-size: 14px; line-height: 1; color: #f1eee7; transition: rotate .15s linear; }
.dock__knopf:focus-visible { outline: 2px solid #f1eee7; outline-offset: 2px; }
.dock.offen { padding: 10px 12px 0; gap: 8px; border-radius: 14px; }
.dock.offen .dock__zeile { display: flex; }
.dock.offen .werte { display: block; }
.dock.offen .dock__knopf { margin-right: -12px; }
.dock.offen .dock__knopf::after { rotate: 45deg; }
.lesen { view-transition-name: lesen; }
/* Gedachter Zeiger für die Vorführung */
.zeiger { position: fixed; z-index: 50; left: 0; top: 0; width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%; border: 1.5px solid #fff; background: rgba(255, 255, 255, .22); mix-blend-mode: difference; pointer-events: none; opacity: 0; transition: opacity .15s linear; }
.zeiger.an { opacity: 1; }
/* Reduzierte Bewegung: kein Morph, Seitenwechsel per Crossfade (im JS), keine Einblendungen */
:root.reduziert * { transition-duration: .01ms !important; transition-delay: 0ms !important; animation-duration: .01ms !important; animation-delay: 0ms !important; }
/* Galerie-Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .werte { display: none; }
@media (hover: none) {
.modi button { height: 34px; }
}
@media (max-width: 900px) {
.titel { grid-column: 1 / -1; }
.claim { grid-column: 1 / 10; }
.meta > div { grid-column: span 4; }
.kapitel__nr, .lead, .spalten, .zahlen, .zitat, .credits { grid-column: 1 / -1; }
.skizze > figure { grid-column: 1 / -1; }
.farben, .schrift { grid-column: 1 / -1; }
.schrift { min-height: 0; }
.detail figcaption > :first-child { grid-column: 1 / -1; }
.detail figcaption > :last-child { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
.nav { display: flex; justify-content: space-between; }
.nav__rechts { display: none; }
.titel { font-size: clamp(42px, 13vw, 64px); }
.claim { grid-column: 1 / -1; }
.meta > div { grid-column: span 6; }
.held__bild { height: 48svh; }
.held__bild img, .weiter__bild img { object-fit: cover; }
.paar > figure { grid-column: 1 / -1; }
.spalten { grid-template-columns: minmax(0, 1fr); }
.skizze svg text { display: none; }
.detail__flaeche { aspect-ratio: 4 / 3; }
.zahlen dd { font-size: clamp(44px, 13vw, 64px); }
.credits { grid-template-columns: minmax(0, 1fr); }
.credits > div { grid-template-columns: 9.5em 1fr; }
.weiter__bild { width: calc(84% + 16% * var(--p)); height: 34svh; }
.weiter__link { padding-bottom: calc(var(--rand) + 92px); }
.werte > span { white-space: normal; }
.werte .lang { display: none; }
/* Telefon: Chip mit 12 px Rand, aufgeklappt über die volle Breite */
.dock { right: 12px; bottom: 12px; width: auto; }
.dock.offen { left: 12px; padding: 10px 10px 0; }
.dock.offen .dock__zeile { display: block; }
.dock.offen .dock__knopf { margin-right: -10px; }
.dock__zeile > span.mono { display: none; }
.modi { width: 100%; } .modi button { flex: 1; padding: 0 6px; }
}
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body data-form="editorial">
<span class="lesen" aria-hidden="true"></span>
<header class="nav">
<span class="marke"><svg viewBox="0 0 15 15" aria-hidden="true"><rect x="0" y="0" width="15" height="15" fill="currentColor"/><rect x="5" y="5" width="10" height="10" fill="var(--papier)"/><rect x="10" y="10" width="5" height="5" fill="currentColor"/></svg>Büro Feldmark</span>
<span class="mono" data-zaehler>Case Study <b>01</b> / 03</span>
<span class="mono nav__rechts">Kommunikationsdesign · Karlsruhe</span>
</header>
<main id="seite" data-seite></main>
<aside class="dock" aria-label="Demo-Steuerung">
<div class="dock__zeile">
<span class="mono">Form</span>
<div class="modi mono" role="group" aria-label="Variante der Case Study">
<button type="button" data-variante="editorial" aria-pressed="true">Editorial</button>
<button type="button" data-variante="galerie" aria-pressed="false">Galerie</button>
<button type="button" data-variante="endlos" aria-pressed="false">Endlos</button>
</div>
</div>
<p class="werte mono" data-werte></p>
<button type="button" class="dock__knopf mono" aria-expanded="false" aria-label="Demo-Steuerung öffnen">Form <b data-form-name>Editorial</b></button>
</aside>
<div class="zeiger" aria-hidden="true"></div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const seite = $('[data-seite]'), zaehler = $('[data-zaehler]'), werteFeld = $('[data-werte]'), zeiger = $('.zeiger');
const hatVT = typeof document.startViewTransition === 'function';
/* Werte: Seitenübergang power3.inOut, 600–1000 ms (Messwerte des Musters) */
const KURVE = 'cubic-bezier(0.76, 0, 0.24, 1)'; // power3.inOut
const morphDauer = () => innerWidth < 640 ? 700 : 900; // Bild und Titel wandern
const BLENDE = 600; // Rest der Seite: alte raus, neue rein
const ENDLOS_WEG = 80; // svh Restweg, bis „Endlos“ auslöst
const FADE = 200; // reduzierte Bewegung
/* ---------- Inhalte: erfundenes Büro, Arbeiten aus medien/ ---------- */
const M = 'bilder/';
const PROJEKTE = [
{ id: 'marke-krume', name: 'Krume Backstube', titel: '<em>Krume</em> Backstube', art: 'Marke & Raum',
farbe: '#edca6e', auf: '#3a2416', flaeche: '#3a2416', umkehr: false,
claim: 'Eine Backstube mit Kaffeetresen bekommt eine Marke, die so rund und satt ist wie ein frischer Laib.',
alt: 'Wortmarke krume in fetter, runder dunkelbrauner Schrift auf Sonnengelb, darüber ein Brotlaib mit drei Einschnitten, darunter Backstube, Sauerteig, Kaffee',
meta: [['Kunde', 'Krume Backstube, Leipzig'], ['Jahr', '2025'], ['Rolle', 'Konzept, Gestaltung'], ['Leistungen', 'Logo, Tüten, Ladenschild'], ['Team', '3 Personen, 7 Wochen'], ['Link', '<span class="link">Website ansehen ↗</span>']],
lead: 'Vorne Tresen, hinten Ofen. Der Bäcker wollte keine Landhausromantik und keine Hipster-Kreide. Gesucht war ein Auftritt, der morgens um halb sechs schon wach aussieht.',
idee: ['Die Wortmarke ist fett, rund und klein geschrieben. Jeder Bogen läuft weich aus wie aufgegangener Teig, keine Kante ist spitz. Darüber sitzt ein Laib mit drei Einschnitten.', 'Zwei Farben tragen alles: Sonnengelb für Tüte, Schild und Schürze, Dunkelbraun wie Kruste für Schrift und Zeichen. Gesperrte Versalien ergänzen die Unterzeilen.'],
plakat: ['Eröffnung', 'Sa 14. März', 'Ofen an ab 5:30 · Bornaische Straße 12'],
zoom: 2.2, ursprung: '30% 48%', detail: 'Die Bögen von r, u und m laufen weich aus. Selbst das k hat keine spitze Ecke.',
k: { mark: [527, 70, 672, 162], zeilen: [[348, 498], [661, 675]], text: [177, 1037] },
farben: [['Sonnengelb', '#EDCA6E'], ['Kruste', '#3A2416'], ['Mehl', '#F6EEDC']],
schrift: { probe: 'krume', stil: "--gr: 26cqi; font-weight: 700; letter-spacing: -.04em;", info: 'Fett, rund, klein geschrieben' },
zahlen: [['Einschnitte im Laib', '3'], ['Anwendungen', '14'], ['Farben für alles', '2']],
zitat: 'Die Tüten nehmen die Leute mit nach Hause und hängen sie an die Wand.', von: 'Ole Hansen, Bäckermeister',
credits: [['Art Direction', 'Lena Brandt'], ['Gestaltung', 'Jonas Weil'], ['Fotografie', 'Mira Okafor'], ['Schildbau', 'Schilderwerk Plagwitz']] },
{ id: 'marke-eselsohr', name: 'Eselsohr', titel: '<em>Eselsohr</em>', art: 'Marke',
farbe: '#1f2024', auf: '#f1e9de', flaeche: '#d8d2c6', umkehr: true,
claim: 'Eine Buchhandlung von 1987 bekommt ein Lesecafé. Die Marke zeigt, was Leser mit Büchern wirklich tun.',
alt: 'Wortmarke Eselsohr in heller, schmaler Kursive auf Anthrazit, links ein Blatt mit umgeknickter rosa Ecke, darunter Buchhandlung und Lesecafé',
meta: [['Kunde', 'Eselsohr Buchhandlung, Freiburg'], ['Jahr', '2023'], ['Rolle', 'Konzept, Gestaltung, Schrift'], ['Leistungen', 'Logo, Tüten, Schaufenster'], ['Team', '2 Personen, 5 Wochen'], ['Link', '<span class="link">Website ansehen ↗</span>']],
lead: 'Der Laden war schon da: Regale bis unter die Decke, ein Lesesessel, seit 1987 dieselbe Inhaberin. Mit dem Lesecafé kam ein neues Publikum, das alte sollte trotzdem bleiben.',
idee: ['Das Zeichen ist ein Blatt mit umgeknickter Ecke, so wie man eine Seite markiert, die man wiederfinden will. Die Ecke ist das einzige Rosa im ganzen System.', 'Die Wortmarke steht in einer ruhigen Kursive, die nach Widmung im Vorsatzblatt aussieht. Für Tüten, Lesezeichen und Schaufenster reicht helles Kreide auf Anthrazit.'],
plakat: ['Lesung', 'Donnerstags um acht', 'Bestellt bis 14 Uhr, morgen da'],
zoom: 2.3, ursprung: '16% 50%', detail: 'Die umgeknickte Ecke ist das einzige Rosa im System. Alles andere bleibt Kreide auf Anthrazit.',
k: { mark: [93, 298, 291, 502], zeilen: [[300, 440]], text: [341, 886] },
farben: [['Anthrazit', '#1F2024'], ['Kreide', '#F1E9DE'], ['Eselsohr', '#E8AA9B']],
schrift: { probe: 'Eselsohr', stil: "--gr: 22cqi; font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-weight: 400;", info: 'Begleitschrift für Tüten und Lesezeichen' },
zahlen: [['Jahre im selben Haus', '38'], ['Anwendungen', '22'], ['Akzentfarbe', '1']],
zitat: 'Unsere Stammkunden haben das Eselsohr sofort verstanden. Die neuen auch.', von: 'Marta Wiegand, Inhaberin',
credits: [['Art Direction', 'Lena Brandt'], ['Gestaltung', 'Paul Seidel'], ['Fotografie', 'Mira Okafor'], ['Druck', 'Offizin am Bach']] },
{ id: 'marke-roestkontor', name: 'Röstkontor', titel: 'Röst<em>kontor</em>', art: 'Wortmarke',
farbe: '#d1402d', auf: '#f3e7d7', flaeche: '#dcd8cf', umkehr: true,
claim: 'Eine Rösterei, die bisher nur Gastronomen belieferte, öffnet einen Ausschank und braucht dafür eine Stimme.',
alt: 'Wortmarke Röstkontor in breiten cremeweißen Versalien auf Ziegelrot, darüber eine dunkle Kaffeebohne mit roter Kerbe im Kreis',
meta: [['Kunde', 'Röstkontor, Hamburg'], ['Jahr', '2024'], ['Rolle', 'Strategie, Gestaltung'], ['Leistungen', 'Wortmarke, Verpackung, Web'], ['Team', '3 Personen, 6 Wochen'], ['Link', '<span class="link">Website ansehen ↗</span>']],
lead: 'Röstkontor röstet seit zwölf Jahren für Cafés im Hafenviertel. Der Name stand fest, ein Zeichen gab es nicht. Die Marke musste auf dem Bohnenbeutel funktionieren, am Ausschank und im Browser-Tab.',
idee: ['Breite Versalien wie auf alten Frachtkisten tragen den Namen. Darüber sitzt eine Bohne im Kreis, deren Kerbe ein einziger Schwung in Ziegelrot ist.', 'Ein sattes Ziegelrot trägt das ganze System. Creme nur für Schrift, keine zweite Farbe, keine Verläufe. Im Regal fällt der Beutel so von weitem auf.'],
plakat: ['Ausschank ab März', 'Hell geröstet, langsam getrunken', 'Filter, Espresso, Bohnen'],
zoom: 2.6, ursprung: '50% 32%', detail: 'Die Kerbe der Bohne ist ein einziger Schwung, ausgespart aus dem dunklen Rund.',
k: { mark: [552, 260, 647, 356], zeilen: [[401, 482]], text: [109, 1094] },
farben: [['Ziegelrot', '#D1402D'], ['Creme', '#F3E7D7'], ['Bohne', '#2F1713']],
schrift: { probe: 'RÖST', stil: "--gr: 22cqi; font-weight: 700; letter-spacing: .04em;", info: 'Breite Versalien wie auf Frachtkisten' },
zahlen: [['Gramm pro Beutel', '250'], ['Farbe', '1'], ['Wochen bis zum Start', '6']],
zitat: 'Endlich sieht man unseren Beuteln an, wie sorgfältig wir rösten.', von: 'Tarek Brünning, Röster',
credits: [['Strategie', 'Lena Brandt'], ['Gestaltung', 'Jonas Weil'], ['Entwicklung', 'Can Aydın'], ['Verpackung', 'Tütenwerk Altona']] }
];
const N = PROJEKTE.length;
const nr = i => String(i + 1).padStart(2, '0');
const hell = hex => { const n = parseInt(hex.slice(1), 16); return (0.299 * (n >> 16) + 0.587 * (n >> 8 & 255) + 0.114 * (n & 255)) > 150; };
// Konstruktionszeichnung im Bildmaßstab 1200 × 800
function konstruktion(k) {
const [x1, y1, x2, y2] = k.mark, cx = (x1 + x2) / 2, cy = (y1 + y2) / 2, r = Math.max(x2 - x1, y2 - y1) * .62;
let s = '';
for (let x = 100; x < 1200; x += 100) s += `<line x1="${x}" y1="0" x2="${x}" y2="800" stroke="rgba(22,21,19,.07)"/>`;
for (let y = 100; y < 800; y += 100) s += `<line x1="0" y1="${y}" x2="1200" y2="${y}" stroke="rgba(22,21,19,.07)"/>`;
s += `<line x1="600" y1="40" x2="600" y2="760" stroke="#d2522a" stroke-dasharray="6 6" stroke-width="1.2"/>`;
s += `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#d2522a" stroke-width="1.2"/>`;
s += `<rect x="${x1}" y="${y1}" width="${x2 - x1}" height="${y2 - y1}" fill="none" stroke="rgba(22,21,19,.55)" stroke-dasharray="3 5"/>`;
s += `<text x="${x1}" y="${y1 - 14}">${x2 - x1} × ${y2 - y1}</text>`;
k.zeilen.forEach(([a, b], j) => {
[a, b].forEach(y => { s += `<line x1="${k.text[0] - 60}" y1="${y}" x2="${k.text[1] + 60}" y2="${y}" stroke="rgba(22,21,19,.45)" stroke-width="1"/>`; });
s += `<text x="${k.text[1] + 72}" y="${b + 5}">${j ? 'Zeile 2' : 'Grundlinie'}</text>`;
s += `<text x="${k.text[1] + 72}" y="${a + 5}">${b - a} px</text>`;
});
[[x1, y1], [x2, y1], [x1, y2], [x2, y2]].forEach(([x, y]) => { s += `<path d="M${x - 8} ${y}h16M${x} ${y - 8}v16" stroke="#d2522a" stroke-width="1.4"/>`; });
s += `<text x="${cx + r * .72 + 10}" y="${cy - r * .72}" fill="#d2522a">r ${Math.round(r)}</text>`;
return `<svg viewBox="0 0 1200 800" preserveAspectRatio="xMidYMid slice" aria-hidden="true">${s}</svg>`;
}
function vorlage(i, titelAn) {
const p = PROJEKTE[i], n = (i + 1) % N, q = PROJEKTE[n];
const an = (j) => `data-an style="--i: ${j}"`;
return `
<article class="case" style="--farbe: ${p.farbe}; --auf: ${p.auf}; --flaeche: ${p.flaeche}" aria-labelledby="titel-${p.id}">
<header class="held">
<div class="held__kopf raster">
<p class="mono kicker" ${an(0)}>Case Study ${nr(i)} · ${p.art}</p>
<h1 class="titel" id="titel-${p.id}" tabindex="-1" data-held-titel ${titelAn ? an(1) : ''}>${p.titel}</h1>
<p class="claim" ${an(2)}>${p.claim}</p>
</div>
<dl class="meta raster mono">${p.meta.map(([a, b], j) => `<div ${an(3 + j)}><dt>${a}</dt><dd>${b}</dd></div>`).join('')}</dl>
<figure>
<div class="held__bild" data-held-bild><img src="${M}${p.id}.jpg" alt="${p.alt}" width="1200" height="800"></div>
<figcaption class="unterzeile mono" ${an(9)}><span>Abb. 01 · Zeichen</span><span>${p.meta[0][1]}</span></figcaption>
</figure>
</header>
<section class="kapitel raster nur-text auf" aria-labelledby="k1-${p.id}">
<h2 class="kapitel__nr mono" id="k1-${p.id}"><b>01</b> Aufgabe</h2>
<p class="lead">${p.lead}</p>
</section>
<section class="modul paar raster" aria-label="Anwendungen">
<figure class="auf">
<div class="plakat"><p class="plakat__kopf mono"><span>${p.plakat[0]}</span><span>${p.name}</span></p><img src="${M}${p.id}.jpg" alt="" width="1200" height="800"><p class="plakat__fuss">${p.plakat[1]}<span class="mono">${p.plakat[2]}</span></p></div>
<figcaption class="mono"><span>Abb. 02 · Plakat</span><span>A1, ein Farbton</span></figcaption>
</figure>
<figure class="auf">
<div class="szene"><span class="becher__schatten"></span><div class="becher"><div class="becher__deckel"></div><div class="becher__koerper"><img src="${M}${p.id}.jpg" alt="" width="1200" height="800"></div></div></div>
<figcaption class="mono"><span>Abb. 03 · Becher</span><span>Zeichen auf 8 cm</span></figcaption>
</figure>
</section>
<section class="kapitel raster nur-text auf" aria-labelledby="k2-${p.id}">
<h2 class="kapitel__nr mono" id="k2-${p.id}"><b>02</b> Idee</h2>
<div class="spalten">${p.idee.map(t => `<p>${t}</p>`).join('')}</div>
</section>
<section class="modul skizze raster" aria-label="Prozess">
<figure class="auf">
<div class="skizze__flaeche" style="${p.umkehr ? '--umkehr: invert(1)' : ''}"><img src="${M}${p.id}.jpg" alt="Konstruktion des Zeichens: Mittelachse, Umkreis und Grundlinien über dem Logo" width="1200" height="800">${konstruktion(p.k)}</div>
<figcaption class="mono"><span>Abb. 04 · Konstruktion</span><span>Raster 100 px · Mittelachse</span></figcaption>
</figure>
</section>
<section class="modul detail raster" aria-label="Detail">
<figure class="auf">
<div class="detail__flaeche" style="--zoom: ${p.zoom}; --ursprung: ${p.ursprung}"><img src="${M}${p.id}.jpg" alt="Ausschnitt: ${p.detail}" width="1200" height="800"></div>
<figcaption class="mono"><span>Abb. 05 · Detail</span><span>${p.detail}</span></figcaption>
</figure>
</section>
<section class="modul system raster auf" aria-label="Farben und Schrift">
<div class="farben">${p.farben.map(([name, hex]) => `<div class="farbfeld" style="--f: ${hex}; --t: ${hell(hex) ? '#161513' : '#f1eee7'}"><span class="mono">${name}</span><b>${hex}</b></div>`).join('')}</div>
<div class="schrift"><p class="mono">Schrift</p><p class="schrift__probe" style="${p.schrift.stil}">${p.schrift.probe}</p><p class="schrift__abc">${p.schrift.info}<br>Aa Bb Cc 0123456789</p></div>
</section>
<section class="kapitel raster nur-text auf" aria-labelledby="k3-${p.id}">
<h2 class="kapitel__nr mono" id="k3-${p.id}"><b>03</b> Ergebnis</h2>
<dl class="zahlen">${p.zahlen.map(([a, b]) => `<div><dt class="mono">${a}</dt><dd>${b}</dd></div>`).join('')}</dl>
<blockquote class="zitat"><p>„${p.zitat}“</p><footer class="mono">${p.von}</footer></blockquote>
</section>
<section class="kapitel raster auf" aria-labelledby="k4-${p.id}">
<h2 class="kapitel__nr mono" id="k4-${p.id}"><b>04</b> Credits</h2>
<dl class="credits">${p.credits.map(([a, b]) => `<div><dt class="mono">${a}</dt><dd>${b}</dd></div>`).join('')}<div><dt class="mono">Konzept</dt><dd>Büro Feldmark</dd></div><div><dt class="mono">Kunde</dt><dd>${p.name}</dd></div></dl>
</section>
<section class="weiter" style="--farbe: ${q.farbe}" aria-label="Nächstes Projekt" data-weiter>
<a class="weiter__link" href="#${q.id}" aria-label="Nächstes Projekt: ${q.name}" data-weiter-link>
<span class="weiter__kopf mono"><span>Nächstes Projekt</span><span>${nr(n)} / ${String(N).padStart(2, '0')}</span></span>
<span class="weiter__titelzeile raster"><span class="titel" data-weiter-titel>${q.titel}</span></span>
<span class="weiter__bild" data-weiter-bild><img src="${M}${q.id}.jpg" alt="" width="1200" height="800"></span>
<span class="weiter__fuss mono">
<span><span class="hinweis-klick">Case Study öffnen <span class="pfeil" aria-hidden="true">→</span></span><span class="hinweis-endlos">Weiterscrollen öffnet die Case Study</span></span>
<span>${q.art} · ${q.meta[1][1]}<span class="hinweis-endlos" data-prozent> · 0 %</span></span>
<span class="balken" aria-hidden="true"><i></i></span>
</span>
</a>
</section>
</article>`;
}
/* ---------- Einblenden beim Scrollen ---------- */
const io = new IntersectionObserver(es => es.forEach(e => { if (e.isIntersecting) { e.target.classList.add('da'); io.unobserve(e.target); } }), { threshold: 0.15 });
let aktuell = 0, form = 'editorial';
function render(i, titelAn) {
aktuell = i;
io.disconnect();
seite.innerHTML = vorlage(i, titelAn);
seite.querySelectorAll('.auf').forEach(el => io.observe(el));
zaehler.innerHTML = `Case Study <b>${nr(i)}</b> / ${String(N).padStart(2, '0')}`;
document.title = `${PROJEKTE[i].name} · Case-Study-Gerüst · Fundus-Demo`;
beimScrollen();
}
/* ---------- Lesefortschritt und Endlos-Auslöser ---------- */
let laeuft = false, ausgeloest = false, gen = 0, vtAktiv = null, wechsel = Promise.resolve();
function beimScrollen() {
const max = document.documentElement.scrollHeight - innerHeight;
html.style.setProperty('--lesen', max > 0 ? (scrollY / max).toFixed(4) : 0);
const w = seite.querySelector('[data-weiter]'); if (!w) return;
if (form !== 'endlos') { w.style.setProperty('--p', 0); return; }
const weg = w.offsetHeight - innerHeight;
const p = Math.min(1, Math.max(0, -w.getBoundingClientRect().top / weg));
w.style.setProperty('--p', p.toFixed(4));
const pz = w.querySelector('[data-prozent]'); if (pz) pz.textContent = ` · ${Math.round(p * 100)} %`;
if (p >= 0.999 && !laeuft && !ausgeloest) {
ausgeloest = true;
setTimeout(() => { if (form === 'endlos' && !laeuft && w.isConnected && -w.getBoundingClientRect().top / weg >= 0.995) geheZu((aktuell + 1) % N, false); else ausgeloest = false; }, 150);
}
}
addEventListener('scroll', beimScrollen, { passive: true });
addEventListener('resize', () => { zeigeWerte(); beimScrollen(); });
/* ---------- Varianten und Werte-Anzeige ---------- */
function zeigeWerte() {
const d = morphDauer();
html.style.setProperty('--morph', d + 'ms');
if (reduziert()) { werteFeld.innerHTML = `<span><b>reduziert</b> kein Morph · Seitenwechsel per Crossfade ${FADE} ms linear</span><span class="lang">keine Einblendungen beim Scrollen</span>`; return; }
const kopf = hatVT ? `<span><b>view-transition-name</b> case-bild · case-titel · ${d} ms · Seite ${BLENDE} ms</span>`
: `<span><b>Browser ohne View Transitions</b> · Crossfade ${BLENDE} ms</span>`;
const fuss = form === 'endlos' ? `<span class="lang">Endlos: ${ENDLOS_WEG}svh Restweg · bei 100 % öffnet das nächste Projekt</span>`
: form === 'galerie' ? '<span class="lang">Galerie: Kapiteltexte, Zahlen und Zitat ausgeblendet · Module rücken auf 24 px</span>'
: '<span class="lang">Meta 6 Felder · 12er-Raster · Ankunft 1000 ms expo.out, +60 ms je Feld</span>';
werteFeld.innerHTML = kopf + `<span><b>${KURVE}</b> power3.inOut</span>` + fuss;
}
function setzeForm(f) {
form = f; document.body.dataset.form = f; ausgeloest = false;
document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === f)));
formName.textContent = f.charAt(0).toUpperCase() + f.slice(1);
zeigeWerte(); beimScrollen();
}
/* ---------- Der Seitenwechsel ---------- */
function geheZu(ziel, mitFokus) {
if (laeuft) return wechsel;
laeuft = true; const g = gen;
wechsel = (async () => {
const neu = () => { render(ziel, false); scrollTo(0, 0); };
if (reduziert() || !hatVT) {
neu();
await seite.animate({ opacity: [0, 1] }, reduziert() ? { duration: FADE, easing: 'linear' } : { duration: BLENDE, easing: KURVE }).finished.catch(() => {});
} else {
// alte Seite: Bild und Titel von „Nächstes Projekt“ tragen die Namen
seite.querySelector('[data-weiter-bild]').style.viewTransitionName = 'case-bild';
seite.querySelector('[data-weiter-titel]').style.viewTransitionName = 'case-titel';
const vt = document.startViewTransition(() => {
neu(); // neue Seite: Hero-Bild und Titel übernehmen die Namen
seite.querySelector('[data-held-bild]').style.viewTransitionName = 'case-bild';
seite.querySelector('[data-held-titel]').style.viewTransitionName = 'case-titel';
});
vtAktiv = vt;
await vt.finished.catch(() => {});
if (vtAktiv === vt) vtAktiv = null;
seite.querySelectorAll('[data-held-bild], [data-held-titel]').forEach(el => { el.style.viewTransitionName = ''; });
}
if (g !== gen) return;
laeuft = false; ausgeloest = false;
if (mitFokus) seite.querySelector('[data-held-titel]').focus({ preventScroll: true });
})();
return wechsel;
}
function zuruecksetzen() {
gen++;
if (vtAktiv) { try { vtAktiv.skipTransition(); } catch (e) {} vtAktiv = null; }
laeuft = false; ausgeloest = false;
render(0, true); scrollTo(0, 0); beimScrollen();
}
/* ---------- Bedienung ---------- */
seite.addEventListener('click', e => {
const a = e.target.closest('[data-weiter-link]'); if (!a) return;
e.preventDefault(); stoppeVorfuehrung();
geheZu((aktuell + 1) % N, true);
});
document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppeVorfuehrung(); setzeForm(b.dataset.variante); }));
// Telefon: Steuerung auf- und zuklappen
const dock = $('.dock'), dockKnopf = $('.dock__knopf'), formName = $('[data-form-name]');
dockKnopf.addEventListener('click', () => {
const offen = dock.classList.toggle('offen');
dockKnopf.setAttribute('aria-expanded', String(offen));
dockKnopf.setAttribute('aria-label', offen ? 'Demo-Steuerung schließen' : 'Demo-Steuerung öffnen');
});
['wheel', 'touchstart', 'keydown'].forEach(t => addEventListener(t, () => { if (lief && !html.classList.contains('vorschau')) stoppeVorfuehrung(); }, { passive: true }));
/* ---------- Vorführung: Seite scrollt durch die Case Study, dann Wechsel ins nächste Projekt ---------- */
const FORMEN = ['editorial', 'galerie', 'endlos'];
let runde = 0, uhr = null, raf = 0, lief = false, schritt = 0, zx = 0, zy = 0;
const warte = (ms, t) => new Promise(r => { uhr = setTimeout(() => r(t === runde), ms); });
function rolle(ziel, dauer, t) {
return new Promise(r => {
const y0 = scrollY, t0 = performance.now();
const f = now => {
if (t !== runde) return r(false);
const q = Math.min(1, (now - t0) / dauer);
scrollTo(0, y0 + (ziel - y0) * (0.5 - 0.5 * Math.cos(Math.PI * q)));
if (q < 1) raf = requestAnimationFrame(f); else r(true);
};
raf = requestAnimationFrame(f);
});
}
function zeigerZu(x, y, t) {
zeiger.classList.add('an');
zeiger.animate({ transform: [`translate(${zx}px, ${zy}px)`, `translate(${x}px, ${y}px)`] }, { duration: 700, easing: 'cubic-bezier(0.16, 1, 0.3, 1)', fill: 'forwards' });
zx = x; zy = y;
return warte(760, t);
}
const klickPuls = () => zeiger.animate({ scale: [1, 0.65, 1] }, { duration: 260, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' });
async function bisGewechselt(t) {
for (let n = 0; n < 40 && !laeuft; n++) if (!(await warte(100, t))) return false;
await wechsel; return t === runde;
}
async function spiel(t) {
while (t === runde) {
setzeForm(FORMEN[schritt % FORMEN.length]);
if (!(await warte(2200, t))) return;
const max = document.documentElement.scrollHeight - innerHeight;
if (form === 'endlos') {
if (!(await rolle(max, 8200, t))) return; // bei 100 % löst der Block selbst aus
if (!(await bisGewechselt(t))) return;
} else {
if (!(await rolle(max, 6800, t))) return;
const r = seite.querySelector('[data-weiter-bild]').getBoundingClientRect();
zx = Math.round(innerWidth * 0.78); zy = Math.round(innerHeight * 0.42);
zeiger.animate({ transform: [`translate(${zx}px, ${zy}px)`] }, { duration: 1, fill: 'forwards' });
if (!(await zeigerZu(Math.round(r.left + r.width * 0.56), Math.round(r.top + r.height * 0.5), t))) return;
klickPuls();
if (!(await warte(180, t))) return;
zeiger.classList.remove('an');
await geheZu((aktuell + 1) % N, false); if (t !== runde) return;
}
if (!(await warte(2300, t))) return;
schritt++;
}
}
function stoppeVorfuehrung() { runde++; clearTimeout(uhr); cancelAnimationFrame(raf); lief = false; zeiger.classList.remove('an'); }
function neustart() { stoppeVorfuehrung(); zuruecksetzen(); }
function vorfuehren() {
stoppeVorfuehrung(); zuruecksetzen();
schritt = 0; lief = true;
spiel(runde);
}
function pause() { const war = lief; stoppeVorfuehrung(); lief = war; }
function weiter() {
if (!lief) return;
const t = ++runde;
wechsel.then(() => { if (t === runde) { scrollTo(0, 0); spiel(t); } });
}
render(0, true);
setzeForm('editorial');
addEventListener('load', zeigeWerte);
})();
</script>
</body>
</html>