Zum Inhalt
← Hero & Section-Layouts

Kinoreifer Vollbild-Video-Hero

Ein Showreel füllt den ersten Viewport, harte Schnitte geben den Takt, darüber stehen nur ein Filmtitel und ein Play-Reel-Kreis.

  1. Ein Video füllt den ganzen ersten Viewport (100svh, object-fit: cover), ohne Rahmen.
  2. Darüber steht nur wenig Typo: ein großer Claim oder die Wortmarke, dazu ein bis zwei kleine Labels wie Showreel-Hinweis, Ort oder Ton an/aus.
  3. Oft wechseln mehrere kurze Einstellungen von 3 bis 6 s, hart geschnitten wie in einem Trailer.
  4. Ein Klick auf „Play Reel“ öffnet das Video mit Ton in einem Overlay.
  5. Beim Scrollen wird das Video kleiner, dunkler oder von der nächsten Sektion überdeckt.

Bewegtbild vermittelt Atmosphäre und Qualität schneller als jede Behauptung. Die knappe Typo darüber liest sich wie ein Filmtitel. Harte Schnitte geben Takt, eine langsame Kamerafahrt gibt Ruhe.

Wann einsetzen

Film- und Produktionsfirmen, Hotels und Gastronomie, Mode, Architektur mit gutem Material, Marken mit eigenem Showreel.

Wann nicht

Ohne hochwertiges eigenes Footage, denn Stockvideo wirkt sofort beliebig. Wenn Ladezeit oder mobiles Datenvolumen knapp sind. Wenn die Kernbotschaft Text ist.

Varianten

  • Mehrere Einstellungen im Trailer-Schnitt (Demo: Trailer)
  • Ein einzelner ruhiger Loop von 8 bis 15 s (Demo: Ein Loop)
  • Reel-Auswahl: eine Liste wechselt das Video, hier per Clip-Wechsel (Demo: Reel-Auswahl)
  • Übergroße Typo über dem Video (siehe schrift-hero-viewportbreit)
  • Video im kleinen Rahmen, das beim Scrollen zum Vollbild wächst (siehe medien-expansion-scroll)
  • Laufband-Text über dem Video (siehe marquee-laufband)

Umsetzung

<video autoplay muted loop playsinline preload="metadata" poster="…" aria-hidden="true"> mit object-fit: cover und height: 100svh. Formate AV1 oder H.265, H.264 als Fallback; die Auflösung per <source media> oder JS wählen. Kurze Loops von 8 bis 15 s, mobil unter 3 bis 5 MB. Darüber ein Overlay mit 20 bis 40 % Schwarz, unten zusätzlich ein Verlauf für die Typo. Eine sichtbare Pause-Taste ist Pflicht (WCAG 2.2.2, Bewegung länger als 5 s). Das Showreel mit Ton läuft getrennt in einem <dialog>.

Scroll-Abgang: Hero mit position: sticky, die nächste Sektion fährt darüber. Dabei skaliert das Video von 1 auf 0,9 und dunkelt von 30 auf 75 % ab.

Die Demo nutzt Standbilder als Einstellungen, weil der Fundus keine Videodateien führt. Kamerafahrt, harter Schnitt und Filmkorn stehen für das Footage. Schnitt, Fahrt, Fortschrittsbalken und Untertitel hängen an einer gemeinsamen Uhr; beim echten Video übernimmt das video.currentTime (timeupdate oder requestVideoFrameCallback).

Robust umsetzen

Telefon. Eigenes, kleineres Hochformat-Video (9:16) über <source media>, bei Data-Saver nur das Poster. playsinline ist zwingend, sonst öffnet iOS den Vollbild-Player. Der Play-Kreis ruht fest unten rechts.

Ohne Bewegung. Kein Autoplay. Das Poster steht, daneben ein Abspielen-Knopf. Scroll-Skalierung und Filmkorn entfallen, der Titel blendet nur kurz ein.

Barrierefrei. Die Pause-Taste ist sichtbar und per Tastatur erreichbar. Das Video ist dekorativ (aria-hidden), wenn es keine Information trägt. Das Showreel bekommt Untertitel, auch für Geräusche. Textkontrast am hellsten und am dunkelsten Frame prüfen; Overlay und Verlauf sichern ihn. Der Dialog schließt mit Esc und gibt den Fokus an den Play-Knopf zurück.

Code

<!DOCTYPE html>
<!--
  Kinoreifer Vollbild-Video-Hero
  Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/cinematic-video-hero/
  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>Kinoreifer Vollbild-Video-Hero · 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; }

:root {
  --grund: #0b0b0c; --text: #f4f1ea; --leise: rgba(244, 241, 234, .62); --linie: rgba(244, 241, 234, .2);
  --akzent: #e3c47a;
  --kurve: cubic-bezier(0.16, 1, 0.3, 1);   /* expo.out */
  --overlay: .3;                             /* 30 % Schwarz über dem Bewegtbild */
  --rand: clamp(18px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { overflow-x: clip; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
:focus-visible { outline: 1px solid var(--text); outline-offset: 4px; }

/* ---------- Hero: klebt oben, die nächste Sektion schiebt sich darüber ---------- */
.hero { position: sticky; top: 0; height: 100svh; min-height: 520px; overflow: hidden; isolation: isolate; }
.film { position: absolute; inset: 0; overflow: hidden; background: #000; transform-origin: 50% 0; will-change: transform; }
.shot { position: absolute; inset: 0; visibility: hidden; }
.shot.an, .shot.vorher { visibility: visible; }
.shot.an { z-index: 2; }
.shot.vorher { z-index: 1; }
.shot img { display: block; width: 100%; height: 100%; object-fit: cover; will-change: transform; filter: contrast(1.08) saturate(.86) brightness(.94); }
/* Abdunkeln: 30 % flach + Verlauf unten für die Typo + Vignette */
.film__ton { position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 45%, transparent 55%, rgba(0,0,0,.5)),
    linear-gradient(to top, rgba(0,0,0,.6), transparent 50%),
    linear-gradient(to right, rgba(0,0,0,.45), transparent 58%),
    rgba(0,0,0,var(--overlay)); }
/* Filmkorn: SVG-Rauschen, springt in 8 Schritten */
.film__korn { position: absolute; inset: -50%; z-index: 4; pointer-events: none; opacity: .16; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  animation: korn .8s steps(8) infinite; }
@keyframes korn { 0% { transform: translate(0,0) } 25% { transform: translate(-6%,4%) } 50% { transform: translate(5%,-7%) } 75% { transform: translate(-3%,-2%) } 100% { transform: translate(4%,6%) } }

.hero__ui { position: absolute; inset: 0; z-index: 5; display: grid; grid-template-rows: auto 1fr auto; padding: var(--rand); pointer-events: none; will-change: opacity, transform; }
.hero__ui > * { pointer-events: auto; }
.oben { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; }
.marke { display: flex; align-items: baseline; gap: 14px; }
.marke b { font-size: 19px; font-weight: 600; letter-spacing: -.03em; color: var(--text); }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; backdrop-filter: blur(6px); transition: background-color .15s, color .15s, border-color .15s; }
.modi button:hover { color: var(--text); border-color: rgba(244,241,234,.45); }
.modi button[aria-pressed='true'] { background: var(--text); color: var(--grund); border-color: var(--text); }

.mitte { display: grid; align-items: end; grid-template-columns: 1fr auto; gap: 32px; padding-bottom: clamp(18px, 3vh, 32px); pointer-events: none; }
.mitte > * { pointer-events: auto; }
.titelblock { display: grid; gap: 18px; }
.titel { font-size: clamp(48px, 7.6vw, 120px); font-weight: 600; letter-spacing: -.05em; line-height: .9; text-wrap: balance; text-shadow: 0 2px 40px rgba(0,0,0,.45); }
.titel em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: -.025em; color: var(--akzent); padding-right: .05em; }
.zeile { display: block; overflow: hidden; padding-bottom: .06em; margin-bottom: -.06em; }
.zeile > span { display: block; transform: translateY(105%); }
.geladen .zeile > span { transform: none; transition: transform 1.2s var(--kurve); }
.geladen .zeile:nth-child(2) > span { transition-delay: .12s; }
.untertitel { display: flex; gap: 18px; flex-wrap: wrap; align-items: baseline; opacity: 0; }
.geladen .untertitel { opacity: 1; transition: opacity .9s .5s var(--kurve); }
.untertitel b { color: var(--text); font-weight: 500; }

/* Reel-Auswahl (Variante 3) */
.auswahl { list-style: none; display: none; gap: 2px; justify-items: end; align-self: end; }
.auswahl-modus .auswahl { display: grid; }
.auswahl-modus .mitte { padding-right: 144px; }
.auswahl button { display: flex; gap: 14px; align-items: baseline; padding: 6px 0; color: var(--leise); font-size: clamp(16px, 1.5vw, 21px); font-weight: 500; letter-spacing: -.02em; transition: color .15s; text-align: right; }
.auswahl button .mono { font-size: 10px; transition: color .15s; }
.auswahl button:hover, .auswahl button[aria-current='true'] { color: var(--text); }
.auswahl button[aria-current='true'] .mono { color: var(--akzent); }

.leiste { display: grid; gap: 14px; }
.spur { display: flex; gap: 6px; height: 2px; }
.spur span { flex: 1; background: rgba(244,241,234,.22); position: relative; overflow: hidden; }
.spur i { position: absolute; inset: 0; background: var(--text); transform-origin: 0 50%; transform: scaleX(0); }
.steuerung { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.knoepfe { display: flex; gap: 22px; align-items: center; }
.knopf { display: inline-flex; gap: 9px; align-items: center; height: 28px; color: var(--leise); transition: color .15s; }
.knopf:hover { color: var(--text); }
.knopf svg { width: 12px; height: 12px; fill: currentColor; }
.pegel { display: inline-flex; gap: 2px; align-items: end; height: 11px; }
.pegel s { width: 2px; height: 4px; background: currentColor; text-decoration: none; }
.pegel s:nth-child(2) { height: 8px; } .pegel s:nth-child(3) { height: 6px; }
.knopf[aria-pressed='false'] .pegel { opacity: .5; }
.knopf[aria-pressed='true'] { color: var(--text); }
.knopf[aria-pressed='true'] .pegel s { animation: pegel .9s ease-in-out infinite alternate; }
.knopf[aria-pressed='true'] .pegel s:nth-child(2) { animation-delay: -.3s; }
.knopf[aria-pressed='true'] .pegel s:nth-child(3) { animation-delay: -.6s; }
@keyframes pegel { from { height: 3px } to { height: 11px } }
.werte { text-align: right; }
.werte b { color: var(--text); font-weight: 500; }

/* Play-Reel-Kreis: ruht unten rechts, folgt auf dem Desktop dem Zeiger */
.play { position: absolute; left: 0; top: 0; z-index: 6; width: 112px; height: 112px; border-radius: 50%; display: grid; place-content: center; gap: 4px; text-align: center;
  background: rgba(244,241,234,.1); border: 1px solid rgba(244,241,234,.35); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); will-change: transform; transition: background-color .15s, border-color .15s; }
.play:hover { background: rgba(244,241,234,.18); }
.play .mono { color: var(--text); font-size: 10px; }
.play .mono + .mono { color: var(--leise); }
.play__pfeil { width: 0; height: 0; margin: 0 auto 4px; border-left: 11px solid var(--text); border-top: 7px solid transparent; border-bottom: 7px solid transparent; transform: translateX(2px); }
.hero.zeiger, .hero.zeiger .film { cursor: none; }

/* ---------- Nächste Sektion schiebt sich über das Video ---------- */
.studio { position: relative; z-index: 2; background: var(--grund); border-radius: 22px 22px 0 0; margin-top: -22px; padding: clamp(56px, 10vh, 120px) var(--rand) clamp(80px, 14vh, 160px); display: grid; grid-template-columns: 1fr 2fr; gap: clamp(40px, 8vh, 88px) 32px; align-content: start; min-height: 100svh; box-shadow: 0 -30px 60px rgba(0,0,0,.35); }
.studio > .mono:first-child { padding-top: .9em; }
.spalten, .tabelle { grid-column: 1 / -1; }
.studio__satz { font-size: clamp(26px, 3.5vw, 52px); font-weight: 500; letter-spacing: -.035em; line-height: 1.08; max-width: 24ch; text-wrap: pretty; }
.studio__satz em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: -.015em; color: var(--akzent); }
.spalten { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; border-top: 1px solid var(--linie); padding-top: 22px; }
.spalten p:last-child { margin-top: 10px; font-size: 16px; line-height: 1.5; color: var(--leise); max-width: 34ch; }
.tabelle { display: grid; grid-template-columns: max-content 1fr; gap: 10px 32px; border-top: 1px solid var(--linie); padding-top: 22px; }
.tabelle dt { color: var(--leise); }
.tabelle dd { color: var(--text); }

/* ---------- Showreel mit Ton im Dialog ---------- */
.reel { position: fixed; inset: 0; width: 100%; height: 100%; max-width: none; max-height: none; background: rgba(6,6,7,0); border: 0; color: var(--text); padding: 0; transition: background-color .5s var(--kurve); }
.reel::backdrop { background: transparent; }
.reel.offen { background: rgba(6,6,7,.97); }
.reel__innen { height: 100%; display: grid; grid-template-rows: auto 1fr auto; gap: 18px; padding: var(--rand); }
.reel__kopf, .reel__leiste { display: flex; justify-content: space-between; align-items: center; gap: 16px; opacity: 0; transition: opacity .5s var(--kurve); }
.reel.offen .reel__kopf, .reel.offen .reel__leiste { opacity: 1; transition-delay: .2s; }
.reel__buehne { display: grid; place-items: center; min-height: 0; }
.reel__bild { position: relative; width: min(100%, calc((100svh - 190px) * 16 / 9)); aspect-ratio: 16 / 9; overflow: hidden; background: #000;
  clip-path: inset(12% 18% round 14px); transform: scale(.94); transition: clip-path .9s var(--kurve), transform .9s var(--kurve); }
.reel.offen .reel__bild { clip-path: inset(0 round 4px); transform: none; }
.reel__bild .shot img { filter: contrast(1.06) saturate(.92); }
.reel__ut { position: absolute; left: 50%; bottom: 7%; z-index: 5; transform: translateX(-50%); padding: 6px 12px; background: rgba(0,0,0,.62); color: #fff; font-size: clamp(13px, 1.4vw, 17px); line-height: 1.35; white-space: nowrap; border-radius: 3px; }
.reel__zeit { font-variant-numeric: tabular-nums; }
.reel__spur { flex: 1; height: 2px; background: rgba(244,241,234,.22); position: relative; }
.reel__spur i { position: absolute; inset: 0; background: var(--text); transform-origin: 0 50%; transform: scaleX(0); }

/* ---------- Reduzierte Bewegung: Poster statt Autoplay ---------- */
.reduziert .film__korn { animation: none; }
.reduziert .zeile > span { transform: none; }
.reduziert .titelblock { opacity: 0; }
.reduziert.geladen .titelblock { opacity: 1; transition: opacity .25s linear; }
.reduziert .untertitel { opacity: 1; }
.reduziert .reel__bild { clip-path: none; transform: none; transition: none; }
.reduziert .knopf[aria-pressed='true'] .pegel s { animation: none; height: 8px; }

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

@media (hover: none) {
  .esc { display: none; }
  .play { width: 84px; height: 84px; }
  .play .mono + .mono { display: none; }
}
@media (max-width: 760px) {
  .oben { display: grid; gap: 14px; }
  .marke .mono { display: none; }
  .modi button { height: 28px; padding: 0 10px; font-size: 10px; }
  .mitte { grid-template-columns: 1fr; gap: 20px; padding-bottom: 104px; }
  .titel { font-size: clamp(46px, 13.5vw, 72px); }
  .auswahl-modus .mitte { padding-right: 0; }
  .auswahl { grid-auto-flow: column; justify-content: start; gap: 6px; }
  .auswahl button { padding: 0; width: 38px; height: 30px; justify-content: center; align-items: center; border: 1px solid var(--linie); border-radius: 999px; }
  .auswahl button .name { display: none; }
  .auswahl button .mono { font-size: 10px; }
  .auswahl button[aria-current='true'] { border-color: var(--akzent); }
  .reel__ut { font-size: 12px; bottom: 5%; }
  .werte { display: none; }
  .studio { grid-template-columns: 1fr; }
  .spalten { grid-template-columns: 1fr; gap: 26px; }
  .tabelle { grid-template-columns: 1fr; gap: 4px; }
  .tabelle dd { margin-bottom: 10px; }
  .reel__ut { white-space: normal; width: 86%; text-align: center; }
}
</style>
</head>
<body>
<header class="hero" data-hero>
  <div class="film" data-film aria-hidden="true">
    <div class="shot"><picture><source media="(max-width: 760px)" srcset="bilder/flakon-hoch.jpg"><img src="bilder/flakon.jpg" alt="" width="1200" height="800"></picture></div>
    <div class="shot"><picture><source media="(max-width: 760px)" srcset="bilder/zitronen-tisch-hoch.jpg"><img src="bilder/zitronen-tisch.jpg" alt="" width="1200" height="800"></picture></div>
    <div class="shot"><picture><source media="(max-width: 760px)" srcset="bilder/keramik-stillleben-hoch.jpg"><img src="bilder/keramik-stillleben.jpg" alt="" width="1200" height="800"></picture></div>
    <div class="shot"><picture><source media="(max-width: 760px)" srcset="bilder/beton-treppe-hoch.jpg"><img src="bilder/beton-treppe.jpg" alt="" width="1200" height="800"></picture></div>
    <div class="film__ton"></div>
    <div class="film__korn"></div>
  </div>

  <div class="hero__ui" data-ui>
    <div class="oben">
      <p class="marke"><b>Halbton</b><span class="mono">Marke &amp; Bewegtbild · Köln</span></p>
      <div class="modi mono" role="group" aria-label="Variante">
        <button type="button" data-modus="trailer" aria-pressed="true">Trailer</button>
        <button type="button" data-modus="loop" aria-pressed="false">Ein Loop</button>
        <button type="button" data-modus="auswahl" aria-pressed="false">Reel-Auswahl</button>
      </div>
    </div>

    <div class="mitte">
      <div class="titelblock">
        <h1 class="titel"><span class="zeile"><span>Marken in</span></span><span class="zeile"><span><em>Bewegung.</em></span></span></h1>
        <p class="untertitel mono"><span>Showreel 2026</span><span data-zaehler>01 / 04</span><span><b data-shot-titel>Ocker 04</b> · <span data-shot-art>Duftfilm</span></span></p>
      </div>
      <ol class="auswahl" aria-label="Reel wählen" data-auswahl></ol>
    </div>

    <div class="leiste">
      <div class="spur" data-spur aria-hidden="true"></div>
      <div class="steuerung mono">
        <div class="knoepfe">
          <button type="button" class="knopf" data-pause aria-label="Video anhalten"><svg viewBox="0 0 12 12" aria-hidden="true"><rect x="2" y="1" width="3" height="10"/><rect x="7" y="1" width="3" height="10"/></svg><span>Pause</span></button>
          <button type="button" class="knopf" data-ton aria-pressed="false"><span class="pegel" aria-hidden="true"><s></s><s></s><s></s></span><span data-ton-text>Ton aus</span></button>
        </div>
        <p class="werte" data-werte></p>
      </div>
    </div>
  </div>

  <button type="button" class="play" data-play aria-haspopup="dialog"><span class="play__pfeil" aria-hidden="true"></span><span class="mono">Play Reel</span><span class="mono">01:12</span></button>
</header>

<main class="studio">
  <p class="mono">Studio · seit 2014</p>
  <p class="studio__satz">Halbton entwickelt Erscheinungsbilder und dreht die Filme dazu. Vom Signet bis zum <em>Dreißigsekünder,</em> aus einer Hand in Köln-Ehrenfeld.</p>
  <div class="spalten">
    <div><p class="mono">01 · Marke</p><p>Name, Zeichen, Schrift und Farbe. Ein System, das auch in Bewegung funktioniert.</p></div>
    <div><p class="mono">02 · Bewegtbild</p><p>Imagefilme, Produktclips und Loops für Website und Social, gedreht im eigenen Studio.</p></div>
    <div><p class="mono">03 · Räume</p><p>Ladenbau, Leitsystem und Schaufenster für Gastronomie und Handwerk.</p></div>
  </div>
  <dl class="tabelle mono" aria-label="Werte dieser Demo">
    <dt>Schnitt</dt><dd>alle 3500 ms, harter Cut · Reel-Auswahl: clip-path 900 ms expo.out</dd>
    <dt>Kamerafahrt</dt><dd>scale 1,12 → 1,2 linear je Einstellung, Versatz bis 8 % · Loop 12 000 ms hin und zurück</dd>
    <dt>Abdunkeln</dt><dd>30 % Schwarz flach + Verlauf unten 60 % und links 45 % + Vignette 50 %</dd>
    <dt>Scroll</dt><dd>Video 1 → 0,9 skaliert, Overlay 30 → 75 % über 100svh</dd>
    <dt>Titel</dt><dd>Masken-Reveal 1200 ms cubic-bezier(0.16, 1, 0.3, 1), versetzt 120 ms</dd>
  </dl>
</main>

<dialog class="reel" data-reel aria-label="Showreel 2026 mit Ton">
  <div class="reel__innen">
    <div class="reel__kopf mono"><span>Halbton · Showreel 2026</span><button type="button" class="knopf" data-schliessen>Schließen<span class="esc"> · Esc</span></button></div>
    <div class="reel__buehne"><div class="reel__bild" data-reel-film><p class="reel__ut" data-ut></p></div></div>
    <div class="reel__leiste mono">
      <button type="button" class="knopf" data-reel-pause>Pause</button>
      <div class="reel__spur" aria-hidden="true"><i data-reel-spur></i></div>
      <span class="reel__zeit" data-reel-zeit>00:00 / 00:14</span>
      <button type="button" class="knopf" data-reel-ton aria-pressed="false"><span class="pegel" aria-hidden="true"><s></s><s></s><s></s></span><span>Ton</span></button>
    </div>
  </div>
</dialog>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);

  // expo.out als echte cubic-bezier(0.16, 1, 0.3, 1)
  const bezier = (x1, y1, x2, y2) => {
    const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx, cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
    const sx = u => ((ax * u + bx) * u + cx) * u, dx = u => (3 * ax * u + 2 * bx) * u + cx, sy = u => ((ay * u + by) * u + cy) * u;
    return t => { if (t <= 0) return 0; if (t >= 1) return 1; let u = t; for (let i = 0; i < 8; i++) { const d = dx(u); if (Math.abs(d) < 1e-6) break; u -= (sx(u) - t) / d; } return sy(Math.min(1, Math.max(0, u))); };
  };
  const expoOut = bezier(0.16, 1, 0.3, 1);

  // Werte
  const SCHNITT = 3500;          // ms je Einstellung (Messwert 3000–6000)
  const CLIP = 900;              // ms Clip-Wechsel in der Reel-Auswahl
  const LOOP = 12000;            // ms ein Loop (hin und zurück)
  const S0 = 1.12, S1 = 1.2;     // Kamerafahrt: scale von → bis

  // Einstellungen: Titel, Art, Drift in % (dx, dy) und Grundversatz nach oben, damit das Motiv über der Typo liegt
  const SHOTS = [
    { titel: 'Ocker 04', art: 'Duftfilm', ut: '[Glas setzt leise auf Stein auf]', dx: [3, 5], dy: [-5, -8] },
    { titel: 'Salz & Schale', art: 'Kochbuch-Trailer', ut: '[Ein Messer teilt eine Zitrone]', dx: [6, 3], dy: [-4, -7] },
    { titel: 'Scheibenwerk', art: 'Keramikwerkstatt', ut: '[Die Töpferscheibe läuft aus]', dx: [2, 6], dy: [-7, -4] },
    { titel: 'Haus am Hang', art: 'Architekturfilm', ut: '[Schritte auf Beton, ein langes Echo]', dx: [5, 1], dy: [-3, -7] },
  ];
  const n = SHOTS.length, TRAILER = n * SCHNITT;

  const hero = $('[data-hero]'), film = $('[data-film]'), ui = $('[data-ui]'), play = $('[data-play]');
  const heroShots = [...film.querySelectorAll('.shot')];
  const spur = $('[data-spur]'), auswahl = $('[data-auswahl]');
  const zaehler = $('[data-zaehler]'), shotTitel = $('[data-shot-titel]'), shotArt = $('[data-shot-art]');
  const werte = $('[data-werte]'), pauseKnopf = $('[data-pause]');

  // Dialog-Bühne bekommt eigene Kopien (immer Querformat)
  const reel = $('[data-reel]'), reelFilm = $('[data-reel-film]'), ut = $('[data-ut]');
  const reelShots = heroShots.map(s => { const c = document.createElement('div'); c.className = 'shot'; c.append(s.querySelector('img').cloneNode()); reelFilm.insertBefore(c, ut); return c; });

  // Reel-Auswahl-Liste
  SHOTS.forEach((s, i) => {
    const li = document.createElement('li');
    li.innerHTML = `<button type="button" data-wahl="${i}" aria-label="${s.titel}"><span class="name">${s.titel}</span><span class="mono">${String(i + 1).padStart(2, '0')}</span></button>`;
    auswahl.append(li);
  });
  const wahlKnoepfe = [...auswahl.querySelectorAll('button')];

  let modus = 'trailer', t = 0, laeuft = !reduziert(), tReel = 0, reelLaeuft = true, reelOffen = false;

  function spurBauen() {
    const anzahl = modus === 'loop' ? 1 : n;
    spur.innerHTML = '<span><i></i></span>'.repeat(anzahl);
  }
  const fmt = ms => { const s = Math.floor(ms / 1000); return '00:' + String(s).padStart(2, '0'); };

  // Eine Einstellung zeichnen. p = Fortschritt 0..1 der Kamerafahrt
  function zeichne(el, shot, p, still) {
    const img = el.firstElementChild.tagName === 'IMG' ? el.firstElementChild : el.querySelector('img');
    if (still) p = 0;
    const s = S0 + (S1 - S0) * p, rand = (s - 1) * 50 - 0.2;   // Versatz nie größer als der Überstand
    const kl = v => Math.max(-rand, Math.min(rand, v));
    const x = kl(shot.dx[0] + (shot.dx[1] - shot.dx[0]) * p), y = kl(shot.dy[0] + (shot.dy[1] - shot.dy[0]) * p);
    img.style.transform = `translate3d(${x.toFixed(3)}%, ${y.toFixed(3)}%, 0) scale(${s.toFixed(4)})`;
  }

  // Bühne in Zustand bringen: welche Einstellung, Clip, Fahrt
  function buehne(shots, zeit, art) {
    const still = reduziert();
    if (art === 'loop') {
      shots.zuletzt = 0;
      shots.forEach((el, i) => { el.classList.toggle('an', i === 0); el.classList.remove('vorher'); el.style.clipPath = ''; });
      const p = 0.5 - 0.5 * Math.cos((zeit % LOOP) / LOOP * Math.PI * 2);
      zeichne(shots[0], SHOTS[0], p, still);
      return { idx: 0, lokal: (zeit % LOOP) / LOOP };
    }
    const idx = Math.floor(zeit / SCHNITT) % n, ms = zeit % SCHNITT;
    if (idx !== shots.zuletzt) { shots.herkunft = shots.zuletzt ?? (idx + n - 1) % n; shots.herkunftP = shots.letztesP ?? 1; shots.zuletzt = idx; }
    const vor = shots.herkunft;
    shots.letztesP = ms / SCHNITT;
    const clip = art === 'auswahl' && !still && ms < CLIP && zeit >= SCHNITT && vor !== idx;
    shots.forEach((el, i) => {
      el.classList.toggle('an', i === idx);
      el.classList.toggle('vorher', clip && i === vor);
    });
    zeichne(shots[idx], SHOTS[idx], ms / SCHNITT, still);
    if (clip) {
      zeichne(shots[vor], SHOTS[vor], shots.herkunftP + ms / SCHNITT, still);
      shots[idx].style.clipPath = `inset(0 0 0 ${(100 * (1 - expoOut(ms / CLIP))).toFixed(2)}%)`;
    } else shots[idx].style.clipPath = '';
    return { idx, lokal: ms / SCHNITT };
  }

  let letzterIdx = -1;
  function heroZeichnen() {
    const { idx, lokal } = buehne(heroShots, t, modus);
    const balken = spur.querySelectorAll('i');
    balken.forEach((b, i) => { b.style.transform = `scaleX(${i < idx ? 1 : i === idx ? lokal.toFixed(4) : 0})`; });
    if (idx !== letzterIdx) {
      letzterIdx = idx;
      zaehler.textContent = modus === 'loop' ? 'Loop · 00:12' : `${String(idx + 1).padStart(2, '0')} / ${String(n).padStart(2, '0')}`;
      shotTitel.textContent = SHOTS[idx].titel; shotArt.textContent = SHOTS[idx].art;
      wahlKnoepfe.forEach((b, i) => b.setAttribute('aria-current', String(i === idx)));
    }
  }
  function reelZeichnen() {
    const { idx } = buehne(reelShots, tReel, 'trailer');
    ut.textContent = SHOTS[idx].ut;
    $('[data-reel-spur]').style.transform = `scaleX(${((tReel % TRAILER) / TRAILER).toFixed(4)})`;
    $('[data-reel-zeit]').textContent = `${fmt(tReel % TRAILER)} / ${fmt(TRAILER)}`;
  }

  const WERTE = {
    trailer: 'Schnitt <b>3500 ms</b> · harter Cut · scale <b>1,12 → 1,2</b> linear · Overlay <b>30 %</b>',
    loop: 'Ein Loop <b>12 000 ms</b> · scale <b>1,12 ↔ 1,2</b> sinus · Overlay <b>30 %</b>',
    auswahl: 'Wechsel <b>clip-path 900 ms</b> expo.out · Schnitt <b>3500 ms</b> · Overlay <b>30 %</b>',
  };
  function setzeModus(neu) {
    modus = neu; t = 0; letzterIdx = -1;
    html.classList.toggle('auswahl-modus', modus === 'auswahl');
    document.querySelectorAll('[data-modus]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === modus)));
    werte.innerHTML = WERTE[modus];
    spurBauen(); heroZeichnen();
  }
  document.querySelectorAll('[data-modus]').forEach(b => b.addEventListener('click', () => { stopVorfuehrung(); setzeModus(b.dataset.modus); }));

  // Auswahl: Hover (Desktop) oder Tippen springt zur Einstellung, der Clip-Wechsel läuft ab
  wahlKnoepfe.forEach((b, i) => {
    const springe = () => { if (Math.floor(t / SCHNITT) % n === i) return; stopVorfuehrung(); t = (Math.floor(t / TRAILER) + 1) * TRAILER + i * SCHNITT; };
    b.addEventListener('click', springe);
    b.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') springe(); });
  });

  // Pause (WCAG 2.2.2) – im reduzierten Modus startet das Video erst hier
  function setzeLauf(an) {
    laeuft = an;
    pauseKnopf.querySelector('span').textContent = an ? 'Pause' : 'Abspielen';
    pauseKnopf.setAttribute('aria-label', an ? 'Video anhalten' : 'Video abspielen');
    pauseKnopf.querySelector('svg').innerHTML = an ? '<rect x="2" y="1" width="3" height="10"/><rect x="7" y="1" width="3" height="10"/>' : '<path d="M2 1l9 5-9 5z"/>';
  }
  pauseKnopf.addEventListener('click', () => { stopVorfuehrung(); setzeLauf(!laeuft); });

  // Ton: leiser Klangteppich per Web Audio (statt Videoton), nur nach Klick
  let audio = null;
  function setzeTon(an) {
    document.querySelectorAll('[data-ton], [data-reel-ton]').forEach(b => b.setAttribute('aria-pressed', String(an)));
    $('[data-ton-text]').textContent = an ? 'Ton an' : 'Ton aus';
    try {
      if (an && !audio) {
        const C = window.AudioContext || window.webkitAudioContext; const ctx = new C();
        const g = ctx.createGain(); g.gain.value = 0; const f = ctx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 900;
        [110, 164.81, 220.5].forEach((hz, i) => { const o = ctx.createOscillator(); o.type = i === 2 ? 'triangle' : 'sine'; o.frequency.value = hz; const og = ctx.createGain(); og.gain.value = i === 2 ? .25 : .5; o.connect(og).connect(f); o.start(); });
        const lfo = ctx.createOscillator(), lg = ctx.createGain(); lfo.frequency.value = .12; lg.gain.value = 380; lfo.connect(lg).connect(f.frequency); lfo.start();
        f.connect(g).connect(ctx.destination); audio = { ctx, g };
      }
      if (audio) { const now = audio.ctx.currentTime; audio.g.gain.cancelScheduledValues(now); audio.g.gain.setTargetAtTime(an ? .05 : 0, now, an ? .6 : .2); if (an) audio.ctx.resume(); }
    } catch (e) { /* ohne Web Audio bleibt es still */ }
  }
  const tonAn = () => $('[data-ton]').getAttribute('aria-pressed') === 'true';
  $('[data-ton]').addEventListener('click', () => setzeTon(!tonAn()));
  $('[data-reel-ton]').addEventListener('click', () => setzeTon(!tonAn()));

  // Showreel im Dialog
  function oeffnen() {
    if (reelOffen) return;
    stopVorfuehrung();
    reelOffen = true; tReel = 0; reelLaeuft = true; $('[data-reel-pause]').textContent = 'Pause';
    reelZeichnen(); reel.showModal();
    requestAnimationFrame(() => requestAnimationFrame(() => reel.classList.add('offen')));
  }
  function schliessen() {
    if (!reelOffen) return;
    reel.classList.remove('offen');
    setTimeout(() => { reel.close(); reelOffen = false; }, reduziert() ? 0 : 450);
  }
  play.addEventListener('click', oeffnen);
  film.addEventListener('click', oeffnen);
  $('[data-schliessen]').addEventListener('click', schliessen);
  reel.addEventListener('cancel', e => { e.preventDefault(); schliessen(); });
  reel.addEventListener('click', e => { if (e.target === reel || e.target.classList.contains('reel__buehne')) schliessen(); });
  $('[data-reel-pause]').addEventListener('click', e => { reelLaeuft = !reelLaeuft; e.currentTarget.textContent = reelLaeuft ? 'Pause' : 'Abspielen'; });

  // Play-Kreis folgt dem Zeiger (Lerp 0,14 bei 60 Hz), sonst Ruheplatz unten rechts
  const fein = matchMedia('(hover: hover) and (pointer: fine)');
  let pos = null, ziel = { x: 0, y: 0 }, zeigerDrin = false;
  function ruheplatz() {
    const r = hero.getBoundingClientRect(), w = play.offsetWidth, leiste = ui.querySelector('.leiste').offsetHeight;
    const rand = parseFloat(getComputedStyle(ui).paddingRight);
    const mobil = innerWidth <= 760;
    return { x: r.width - rand - w, y: r.height - rand - leiste - w - (mobil ? 12 : 30) };
  }
  hero.addEventListener('pointermove', e => {
    if (!fein.matches || reduziert() || e.pointerType !== 'mouse') return;
    const ueberUi = e.target.closest('.oben, .leiste, .auswahl, .titel');
    zeigerDrin = !ueberUi;
    hero.classList.toggle('zeiger', zeigerDrin);
    if (zeigerDrin) { const r = hero.getBoundingClientRect(), w = play.offsetWidth; ziel = { x: e.clientX - r.left - w / 2, y: e.clientY - r.top - w / 2 }; }
  });
  hero.addEventListener('pointerleave', () => { zeigerDrin = false; hero.classList.remove('zeiger'); });

  // Scroll: Video schrumpft leicht und dunkelt ab, während die nächste Sektion darüber fährt
  let scrollP = -1;
  function scrollen() {
    const p = Math.min(1, Math.max(0, scrollY / innerHeight));
    if (p === scrollP) return; scrollP = p;
    if (reduziert()) { film.style.transform = ''; ui.style.opacity = ''; ui.style.transform = ''; hero.style.setProperty('--overlay', .3); return; }
    film.style.transform = `scale(${(1 - 0.1 * p).toFixed(4)})`;
    film.style.borderRadius = `${(p * 18).toFixed(1)}px`;
    hero.style.setProperty('--overlay', (0.3 + 0.45 * p).toFixed(3));
    ui.style.opacity = Math.max(0, 1 - p * 1.6).toFixed(3);
    ui.style.transform = `translateY(${(-40 * p).toFixed(1)}px)`;
    play.style.opacity = Math.max(0, 1 - p * 1.6).toFixed(3);
  }

  // Eine Uhr für alles
  let vorher = performance.now();
  function tick(jetzt) {
    const dt = Math.max(0, Math.min(100, jetzt - vorher)); vorher = jetzt;
    if (reelOffen) { if (reelLaeuft) tReel += dt; reelZeichnen(); }
    else if (laeuft && !document.hidden) t += dt;
    if (!reelOffen) heroZeichnen();
    if (vorfuehrung) vorfuehrSchritt(jetzt);
    scrollen();
    const ruhe = ruheplatz();
    const z = zeigerDrin ? ziel : ruhe;
    if (!pos || reduziert()) pos = { ...z };
    const k = 1 - Math.pow(1 - 0.14, dt / 16.67);
    pos.x += (z.x - pos.x) * k; pos.y += (z.y - pos.y) * k;
    play.style.transform = `translate3d(${pos.x.toFixed(1)}px, ${pos.y.toFixed(1)}px, 0)`;
    requestAnimationFrame(tick);
  }

  // Vorführung: Trailer läuft, in der letzten Einstellung fährt die Seite kurz hoch und zurück
  let vorfuehrung = false, v0 = 0;
  function vorfuehrSchritt(jetzt) {
    const runde = modus === 'loop' ? LOOP : TRAILER;
    const z = (jetzt - v0) % runde, start = runde - 3500;
    let p = 0;
    if (z > start) { const q = (z - start) / 3500; p = Math.sin(q * Math.PI); p = p * p * (3 - 2 * p); }
    scrollTo(0, p * innerHeight * 0.55);
  }
  function stopVorfuehrung() { vorfuehrung = false; }

  function textZeigen() {
    html.classList.remove('geladen'); void ui.offsetWidth;
    requestAnimationFrame(() => html.classList.add('geladen'));
  }
  function neustart() {
    stopVorfuehrung(); if (reelOffen) { reel.classList.remove('offen'); reel.close(); reelOffen = false; }
    scrollTo(0, 0); setzeModus(modus); setzeLauf(!reduziert()); textZeigen();
  }
  function vorfuehren() {
    neustart(); setzeLauf(true);
    vorfuehrung = true; v0 = performance.now();
  }
  function pause() { stopVorfuehrung(); setzeLauf(false); reelLaeuft = false; }
  function weiter() { if (html.classList.contains('vorschau')) vorfuehren(); else { setzeLauf(true); reelLaeuft = true; } }

  // Start erst nach dem Rahmen-Skript, damit ?reduziert schon gesetzt ist
  function start() { setzeModus('trailer'); setzeLauf(!reduziert()); requestAnimationFrame(tick); }
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
  addEventListener('load', textZeigen);
  // Schalter der Detailseite: reduziert an/aus zur Laufzeit
  let warReduziert = reduziert();
  new MutationObserver(() => { const r = reduziert(); if (r !== warReduziert) { warReduziert = r; scrollP = -1; letzterIdx = -1; setzeLauf(!r); } }).observe(html, { attributes: true, attributeFilter: ['class'] });

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