Zum Inhalt
← Seitenübergänge

Spalten-, Streifen- und Jalousie-Wipe

Fünf oder sechs Bahnen in Signalfarbe schließen gestaffelt über der Seite, setzen den Seitennamen zusammen und geben die neue Seite frei: als Spalten, Streifen oder Jalousie.

Statt eines einzelnen Panels decken mehrere Bahnen die Seite: 4 bis 8 senkrechte Spalten oder waagerechte Streifen. Sie fahren gestaffelt ein, mit 40 bis 80 ms Versatz, von links nach rechts oder von der Mitte nach außen. Hinter den geschlossenen Bahnen wird der Inhalt getauscht. Dann fahren die Bahnen gestaffelt weiter und geben die neue Seite frei. Beim Zurück-Navigieren läuft alles in umgekehrter Richtung.

Der Stagger macht aus einer Fläche einen Rhythmus. Das wirkt gebaut und architektonisch. Liegen die Bahnen auf dem Spaltenraster der Seite, wird das Raster beim Seitenwechsel selbst sichtbar.

Wann einsetzen

Bei Seiten mit sichtbarem Spaltenraster: Architektur, Editorial, Mode, Agenturen. Und immer dann, wenn ein einzelnes Panel zu glatt wirkt.

Wann nicht

Bei sehr ruhigen, minimalen Marken: zu viel Unruhe. Bei häufigen, schnellen Wechseln wie Shop-Filtern oder Dashboards, weil jeder Wechsel die Seite kurz verdeckt.

Varianten

  • Senkrechte Spalten (columns): jede Spalte fährt per translateY ein und weiter nach oben weg.
  • Waagerechte Streifen (slices): seitlich per translateX, oft von der Mitte nach außen.
  • Jalousie (shutter): jede Spalte skaliert per scaleY, statt zu fahren. K72 schließt mit Ursprung unten und öffnet mit Ursprung oben.
  • Spalten in Markenfarbe mit Richtungsumkehr bei Zurück (inside-out.tech).

Umsetzung

Ein fixiertes Overlay mit N absolut positionierten Bahnen, Breite calc(100% / N + 1px). Schließen: gsap.fromTo(cols, { yPercent: 100 }, { yPercent: 0, stagger: { each: 0.045, from: 'start' } }). Öffnen: weiter auf yPercent: -100. Jalousie: scaleY 0 → 1 mit transform-origin bottom, zum Öffnen 1 → 0 mit origin top. K72 nutzt dafür CSS-Transitions: 0,3 s, cubic-bezier(.215,.61,.355,1), Delays 75, 120, 165, 210 und 255 ms. Ohne Bibliothek reicht element.animate() mit einem delay je Bahn. Im Router (Barba, Astro ClientRouter, Interlude) schließt der Leave-Hook die Bahnen, der Enter-Hook öffnet sie. Die Richtung bei Zurück über ein Flag umkehren (popstate oder Navigationstyp). Fallstrick: Subpixel-Lücken zwischen den Bahnen, deshalb 1 px Überlappung.

Robust umsetzen

Telefon. Spaltenzahl auf 3 bis 4 senken, sonst werden die Bahnen zu schmal. Die Dauern können bleiben.

Ohne Bewegung. Sofortiger Wechsel (so bei inside-out.tech) oder ein kurzer Fade ohne Bahnen.

Barrierefrei. Overlay mit aria-hidden. Nach dem Wechsel den Fokus auf die neue Hauptüberschrift setzen und den Seitentitel per aria-live ansagen, wie bei jedem JS-Router. Text auf der Bahnfarbe mit genug Kontrast setzen.

Code

<!DOCTYPE html>
<!--
  Spalten-, Streifen- und Jalousie-Wipe
  Fundus · Seitenübergänge · https://cecodesign.de/lab/fundus/seitenuebergaenge/spalten-shutter-wipe/
  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>Spalten-, Streifen- und Jalousie-Wipe · 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: 'TikTok Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/tiktok-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 300 900; font-stretch: 75% 150%; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }

:root {
  --schwarz: #0b0b0b;
  --grau: #f2f2f2;
  --weiss: #ffffff;
  --meta: #8a8a8a;
  --signal: #ff5a1f;      /* einzige Signalfarbe: CTA, Hover-Fläche, Übergang */
  --kurve: cubic-bezier(0.215, 0.61, 0.355, 1);   /* gemessen bei K72 (easeOutCubic) */
  --bg: var(--schwarz); --fg: var(--weiss);
  --linie: color-mix(in srgb, var(--fg) 16%, transparent);
  --leise: color-mix(in srgb, var(--fg) 58%, transparent);
  --rand: clamp(16px, 2.6vw, 40px);
  --kopf: 64px;
  --dock: 120px;          /* wird im JS aus der Höhe der Demo-Steuerung gesetzt */
  --n: 6;                 /* Spaltenzahl der aktuellen Variante, für das Raster */
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--bg); color: var(--fg); font-family: 'TikTok Sans', 'Arial Narrow', system-ui, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; background: var(--bg); color: var(--fg); overflow-x: clip; transition: background-color .3s var(--kurve), color .3s var(--kurve); }
a { color: inherit; text-decoration: none; }
svg { display: block; }
em { font-style: normal; }
.mono { font: 400 12px/1.45 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.gross { font-stretch: 75%; font-weight: 800; letter-spacing: -0.03em; line-height: .9; }
h1:focus { outline: none; }

/* Sichtbares Raster: die Linien liegen genau auf den Kanten der Übergangs-Spalten */
.raster { position: fixed; inset: 0; z-index: 0; pointer-events: none;
  background: repeating-linear-gradient(90deg, transparent 0 calc(100% / var(--n) - 1px), var(--linie) calc(100% / var(--n) - 1px) calc(100% / var(--n))); opacity: .55; transition: opacity .3s var(--kurve); }
[data-modus='streifen'] .raster { background: repeating-linear-gradient(180deg, transparent 0 calc(100% / var(--n) - 1px), var(--linie) calc(100% / var(--n) - 1px) calc(100% / var(--n))); }

/* Rollender Text (text-roll-hover, Signatur des Rezepts) */
.roll { display: inline-grid; overflow: hidden; vertical-align: top; }
.roll > span { grid-area: 1 / 1; transition: transform .3s var(--kurve); }
.roll > span + span { transform: translateY(105%); }
@media (hover: hover) {
  a:hover > .roll > span:first-child, a:focus-visible > .roll > span:first-child { transform: translateY(-105%); }
  a:hover > .roll > span + span, a:focus-visible > .roll > span + span { transform: translateY(0); }
}

/* ---------- Kopfzeile ---------- */
.kopf { position: fixed; z-index: 10; inset: 0 0 auto; height: var(--kopf); display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; padding: 0 var(--rand); color: var(--fg); background: var(--bg); border-bottom: 1px solid var(--linie); transition: background-color .3s var(--kurve), color .3s var(--kurve); }
.marke { justify-self: start; display: inline-flex; align-items: baseline; gap: 12px; }
.marke b { font: 800 24px/1 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.02em; }
.marke b i { font-style: normal; color: var(--signal); }
.marke small { color: var(--leise); }
.nav { display: flex; gap: clamp(16px, 2.6vw, 40px); }
.nav a { position: relative; padding: 8px 0; }
.nav a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 4px; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: right; transition: transform .3s var(--kurve); }
.nav a[aria-current='page']::after { transform: scaleX(1); transform-origin: left; }
.anfrage { justify-self: end; display: inline-flex; align-items: center; gap: 10px; height: 38px; padding: 0 16px; background: var(--signal); color: var(--schwarz); }

/* ---------- Seiten ---------- */
#seite { position: relative; z-index: 1; min-height: 100svh; padding-top: var(--kopf); }
.s { min-height: calc(100svh - var(--kopf)); padding: clamp(18px, 3.6vh, 40px) var(--rand) calc(var(--dock) + clamp(16px, 3vh, 32px)); }
.kopfzeile { display: flex; justify-content: space-between; gap: 16px; color: var(--leise); }

/* Start */
.s-start { display: grid; grid-template-rows: auto 1fr auto; gap: clamp(14px, 3vh, 36px); }
.held { font-size: min(11.4vw, 21vh); display: grid; align-content: center; padding: .02em 0 .06em; }
.held span { display: block; white-space: nowrap; }
.held span + span { padding-left: calc(100vw / var(--n) * 2 - var(--rand)); }
.start__unten { display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 5fr) minmax(0, 3fr); gap: 24px clamp(20px, 2.6vw, 40px); align-items: end; padding-top: 18px; border-top: 1px solid var(--linie); }
.lead { font-size: clamp(18px, 1.35vw, 21px); line-height: 1.4; max-width: 30ch; }
.lead + .mono { margin-top: 16px; color: var(--leise); }
.reel { display: grid; gap: 10px; }
.reel__bild { position: relative; aspect-ratio: 16 / 7; overflow: hidden; background: #161616; }
.reel__bild svg { width: 100%; height: 100%; transition: transform .6s var(--kurve); }
.reel__play { position: absolute; left: 16px; bottom: 16px; display: inline-flex; align-items: center; gap: 10px; height: 36px; padding: 0 14px 0 10px; background: var(--weiss); color: var(--schwarz); }
.reel__play::before { content: ''; width: 0; height: 0; border-left: 9px solid currentColor; border-top: 6px solid transparent; border-bottom: 6px solid transparent; }
.reel .zeile { display: flex; justify-content: space-between; gap: 12px; color: var(--leise); }
@media (hover: hover) { .reel:hover .reel__bild svg { transform: scale(1.04); } }
.zuarbeiten { display: grid; gap: 10px; justify-items: start; }
.zuarbeiten b { font: 800 clamp(34px, 3.4vw, 56px)/.9 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.03em; display: flex; justify-content: space-between; gap: .3em; width: 100%; padding-bottom: 10px; border-bottom: 2px solid currentColor; }
.zuarbeiten .mono { color: var(--leise); }

/* Arbeiten (Index) */
.titelzeile { display: flex; justify-content: space-between; align-items: end; gap: 16px; padding-bottom: clamp(12px, 2vh, 22px); border-bottom: 2px solid var(--fg); }
.titel { font-size: clamp(56px, 8vw, 132px); }
.titel sup { font: 400 max(11px, .1em)/1 'DM Mono', monospace; letter-spacing: .04em; vertical-align: top; margin-left: .45em; color: var(--leise); }
.liste { list-style: none; }
.liste a { position: relative; isolation: isolate; display: grid; grid-template-columns: 3.5em minmax(0, 1fr) 15em 4em 120px; align-items: center; gap: 20px; padding: clamp(10px, 1.6vh, 18px) 0; border-bottom: 1px solid var(--linie); }
.liste a::before { content: ''; position: absolute; z-index: -1; inset: 0 calc(var(--rand) * -1); background: var(--signal); transform: scaleY(0); transform-origin: bottom; transition: transform .3s var(--kurve); }
.liste .name { font: 800 clamp(30px, 3.5vw, 58px)/.92 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.03em; }
.liste .mono { color: var(--leise); transition: color .3s var(--kurve); }
.liste .daumen { width: 120px; aspect-ratio: 3 / 2; overflow: hidden; }
.liste .daumen svg { width: 100%; height: 100%; }
@media (hover: hover) {
  .liste a:hover::before { transform: scaleY(1); }
  .liste a:hover { color: var(--schwarz); }
  .liste a:hover .mono { color: var(--schwarz); }
}

/* Projekt */
.s-projekt { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: clamp(24px, 4vw, 64px); align-items: start; }
.projekt__bild { position: sticky; top: calc(var(--kopf) + 20px); height: min(calc(100svh - var(--kopf) - var(--dock) - 56px), 820px); min-height: 320px; overflow: hidden; }
.projekt__bild svg { width: 100%; height: 100%; }
.projekt__text { container-type: inline-size; display: grid; gap: clamp(16px, 2.4vh, 26px); }
.projekt__text .titel { font-size: max(44px, min(clamp(60px, 8vw, 140px), calc(100cqi / (var(--z, 10) * .5)))); }
.unter { font-size: clamp(21px, 1.8vw, 28px); line-height: 1.22; font-weight: 600; max-width: 26ch; margin-top: 14px; }
.fakten div { display: grid; grid-template-columns: 9em 1fr; gap: 12px; padding: 9px 0; border-top: 1px solid var(--linie); }
.fakten dt { color: var(--leise); }
.fliess { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 32px; font-size: 17px; line-height: 1.55; }
.projekt__nav { display: grid; grid-template-columns: auto 1fr; gap: 24px; align-items: end; padding-top: 16px; border-top: 2px solid var(--fg); }
.zurueck { display: inline-flex; align-items: center; height: 38px; padding: 0 14px; border: 1px solid var(--fg); }
.naechstes { display: grid; gap: 8px; justify-items: end; text-align: right; }
.naechstes b { font: 800 clamp(32px, 3.4vw, 56px)/.9 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.03em; }
.projekt .mono { color: var(--leise); }

/* Studio */
.s-studio { display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); grid-template-rows: auto minmax(120px, 1fr) auto; gap: clamp(24px, 4vh, 48px) clamp(24px, 4vw, 64px); }
.studio__band { grid-column: 1 / -1; position: relative; overflow: hidden; background: #e4e4e4; min-height: 120px; }
.studio__band svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.studio__band .mono { position: absolute; left: 14px; bottom: 12px; padding: 6px 10px; background: var(--grau); color: var(--schwarz); }
.aussage { font: 700 clamp(36px, 4.4vw, 76px)/.95 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.025em; text-wrap: balance; }
.aussage em { color: var(--leise); }
.studio__rechts { display: grid; gap: 18px; align-content: end; font-size: 17px; line-height: 1.55; }
.leistungen { list-style: none; }
.leistungen li { display: flex; justify-content: space-between; gap: 12px; padding: 9px 0; border-top: 1px solid var(--linie); }
.leistungen li:last-child { border-bottom: 1px solid var(--linie); }
.leistungen span:last-child { color: var(--leise); }
.zahlen { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; padding-top: 16px; border-top: 2px solid var(--fg); }
.zahlen b { display: block; font: 800 clamp(48px, 5vw, 88px)/.9 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.03em; margin-bottom: 10px; }
.zahlen .mono { color: var(--leise); }

/* Kontakt */
.s-kontakt { display: grid; grid-template-rows: 1fr auto auto; gap: clamp(20px, 4vh, 40px); }
.frage { font-size: clamp(46px, 6.4vw, 112px); align-self: center; text-wrap: balance; max-width: 15ch; }
.mail { justify-self: start; display: inline-flex; align-items: center; gap: 14px; height: 54px; padding: 0 22px; background: var(--signal); color: var(--schwarz); font: 700 clamp(18px, 1.6vw, 24px)/1 'TikTok Sans', sans-serif; letter-spacing: -0.01em; }
.adresse { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; padding-top: 16px; border-top: 1px solid var(--linie); font-size: 16px; line-height: 1.5; }
.adresse .mono { display: block; color: var(--leise); margin-bottom: 6px; }
.kontakt__fuss { min-width: 0; overflow: clip; }
.wortmarke { width: max-content; font-weight: 800; font-stretch: 75%; letter-spacing: -0.04em; line-height: .8; white-space: nowrap; margin: clamp(20px, 4vh, 40px) 0 -.04em -.03em; }
.wortmarke i { font-style: normal; color: var(--signal); }

/* ---------- Der Übergang: Spalten / Streifen / Jalousie ---------- */
.shutter { position: fixed; inset: 0; z-index: 30; pointer-events: none; visibility: hidden; }
.shutter.an { visibility: visible; }
.shutter__scrim { position: absolute; inset: 0; background: #1d1d1d; opacity: 0; }
.bahn { position: absolute; overflow: hidden; background: var(--signal); color: var(--schwarz); will-change: transform; }
.bahn__voll { position: absolute; display: grid; grid-template-rows: auto 1fr auto; padding: 22px var(--rand); }
.bahn__nr { position: absolute; left: 12px; top: calc(var(--kopf) + 10px); font-size: 11px; }
.bahn__voll .mono { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid currentColor; }
.bahn__voll .mono:last-child { padding: 12px 0 0; border: 0; border-top: 1px solid currentColor; }
.bahn__name { align-self: center; justify-self: center; text-align: center; font-size: clamp(76px, 16vw, 280px); white-space: nowrap; }
.shutter__name { inset: 0; color: var(--schwarz); opacity: 0; }

/* ---------- Demo-Steuerung ---------- */
.dock { position: fixed; z-index: 40; left: 50%; bottom: 14px; translate: -50% 0; width: max-content; max-width: calc(100% - 24px);
  display: grid; gap: 8px; padding: 10px 12px; color: var(--grau);
  background: rgba(11, 11, 11, .9); border: 1px solid rgba(242, 242, 242, .16); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
.dock__zeile { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dock .mono { color: rgba(242, 242, 242, .6); }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: rgba(242, 242, 242, .8); background: none; border: 1px solid rgba(242, 242, 242, .22); height: 30px; padding: 0 12px; cursor: pointer; transition: background-color .15s linear, color .15s linear; }
.modi button:hover { color: var(--weiss); }
.modi button[aria-pressed='true'] { background: var(--signal); color: var(--schwarz); border-color: var(--signal); }
.modi button:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
.werte { font-size: 10px; line-height: 1.6; letter-spacing: .02em; text-transform: none; min-height: 4.8em; }
.werte > span { display: block; white-space: nowrap; }
.werte b { color: var(--signal); font-weight: 500; }
:root.reduziert .werte { min-height: 0; }

/* 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: keine Spalten, nur kurzer Fade (im JS) */
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }

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

@media (hover: none) {
  .modi button { height: 36px; }
  .liste a:active::before { transform: scaleY(1); }
}
@media (max-width: 1100px) {
  .liste a { grid-template-columns: 3em minmax(0, 1fr) 12em 88px; }
  .liste .jahr { display: none; }
  .liste .daumen { width: 88px; }
}
@media (max-width: 900px) {
  .start__unten { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .start__unten .zuarbeiten { grid-column: 1 / -1; }
  .s-projekt, .s-studio { grid-template-columns: minmax(0, 1fr); }
  .projekt__bild { position: static; height: auto; min-height: 0; aspect-ratio: 4 / 3; }
  .zahlen, .adresse { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
  .werte > span { white-space: normal; }
}
@media (max-width: 600px) {
  :root { --kopf: 56px; }
  .kopf { grid-template-columns: auto 1fr; }
  .nav { justify-self: end; gap: 14px; font-size: 11px; }
  .anfrage, .marke small, .nav .nav__start { display: none; }
  .marke b { font-size: 21px; }
  .kopfzeile span:last-child { display: none; }
  .held { font-size: 19vw; }
  .held span + span { padding-left: 0; }
  .start__unten { grid-template-columns: minmax(0, 1fr); }
  .s-start { grid-template-rows: auto auto auto; align-content: start; }
  .titelzeile > .mono { display: none; }
  .liste a { grid-template-columns: 2.2em minmax(0, 1fr) 72px; gap: 12px; }
  .liste .wer { display: none; }
  .liste .daumen { width: 72px; }
  .liste .name { font-size: 32px; }
  .fliess { grid-template-columns: minmax(0, 1fr); }
  .fakten div { grid-template-columns: 7.5em 1fr; }
  .projekt__nav { grid-template-columns: minmax(0, 1fr); }
  .naechstes { justify-items: start; text-align: left; }
  .adresse, .zahlen { gap: 20px 16px; }
  .bahn__voll .mono span:last-child { display: none; }
  .bahn__nr { display: none; }
  .dock__zeile > span.mono { display: none; }
  .dock { padding: 8px 10px; bottom: 12px; width: calc(100% - 24px); }
  .werte .lang { display: none; }
}
</style>
</head>
<body data-modus="spalten">
<div class="raster" aria-hidden="true"></div>
<header class="kopf">
  <a class="marke" href="#start" data-ziel="start" aria-label="Kette und Schuss, Startseite">
    <b>Kette<i>&amp;</i>Schuss</b><small class="mono">Bielefeld</small>
  </a>
  <nav class="nav mono" aria-label="Hauptnavigation">
    <a class="nav__start" href="#start" data-ziel="start"><span class="roll"><span>Start</span><span aria-hidden="true">Start</span></span></a>
    <a href="#arbeiten" data-ziel="arbeiten"><span class="roll"><span>Arbeiten</span><span aria-hidden="true">Arbeiten</span></span></a>
    <a href="#studio" data-ziel="studio"><span class="roll"><span>Studio</span><span aria-hidden="true">Studio</span></span></a>
    <a href="#kontakt" data-ziel="kontakt"><span class="roll"><span>Kontakt</span><span aria-hidden="true">Kontakt</span></span></a>
  </nav>
  <a class="anfrage mono" href="#kontakt" data-ziel="kontakt"><span class="roll"><span>Projekt anfragen →</span><span aria-hidden="true">Projekt anfragen →</span></span></a>
</header>

<main id="seite"></main>
<p class="sr" aria-live="polite" data-ansage></p>

<!-- Übergang: Abdunklung (wie bei K72) und N Bahnen, im JS je Variante erzeugt -->
<div class="shutter" aria-hidden="true">
  <div class="shutter__scrim"></div>
  <div class="shutter__bahnen"></div>
  <div class="shutter__name bahn__voll"><div class="mono"><span>Kette&amp;Schuss</span><span>Digitalstudio · Bielefeld</span></div><b class="bahn__name gross" data-s-name>Arbeiten</b><div class="mono"><span data-s-nr>02 / 04</span><span>Seite wird geladen</span></div></div>
</div>

<aside class="dock" aria-label="Demo-Steuerung">
  <div class="dock__zeile">
    <span class="mono">Übergang</span>
    <div class="modi mono" role="group" aria-label="Variante des Übergangs">
      <button type="button" data-variante="spalten" aria-pressed="true">Spalten</button>
      <button type="button" data-variante="streifen" aria-pressed="false">Streifen</button>
      <button type="button" data-variante="jalousie" aria-pressed="false">Jalousie</button>
    </div>
  </div>
  <p class="werte mono" data-werte></p>
</aside>
<div class="zeiger" aria-hidden="true"></div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const seite = document.getElementById('seite');
  const shutter = document.querySelector('.shutter');
  const scrim = shutter.querySelector('.shutter__scrim');
  const bahnenBox = shutter.querySelector('.shutter__bahnen');
  const sName = shutter.querySelector('.shutter__name');
  const sNameText = shutter.querySelector('[data-s-name]');
  const sNr = shutter.querySelector('[data-s-nr]');
  const ansage = document.querySelector('[data-ansage]');
  const werteFeld = document.querySelector('[data-werte]');
  const dock = document.querySelector('.dock');
  const zeiger = document.querySelector('.zeiger');

  /* ---------- Werte ----------
     Gemessen bei K72 (main.css, .c-loader_col): 5 Spalten, scale3d(1,0,1) ↔ scale3d(1,1,1),
     transform-origin bottom (schließen) / top (öffnen), transition .3s cubic-bezier(.215,.61,.355,1),
     transition-delay .075/.12/.165/.21/.255s, Overlay #1D1D1D opacity .8.
     Diese Kurve und diese Zeiten gelten in der Demo für alle drei Varianten. */
  const KURVE = 'cubic-bezier(0.215, 0.61, 0.355, 1)';
  const DAUER = 300;        // je Bahn
  const ERSTE = 75;         // Verzögerung der ersten Bahn
  const STAGGER = 45;       // Abstand zwischen den Bahnen
  const HALTEN = 260;       // Pause bei geschlossenen Bahnen (Inhaltstausch)
  const SCRIM = 0.8;        // Abdunklung der alten Seite
  const NEU = { y: 40, dauer: 600 };   // neue Seite setzt sich (K72-Standard .6s transform)
  const FADE = 200;         // reduzierte Bewegung

  const VARIANTEN = {
    spalten:  { achse: 'x', n: 6, nTel: 4, art: 'fahren',    folge: 'start', titel: 'Spalten' },
    streifen: { achse: 'y', n: 5, nTel: 5, art: 'fahren',    folge: 'mitte', titel: 'Streifen' },
    jalousie: { achse: 'x', n: 5, nTel: 4, art: 'skalieren', folge: 'start', titel: 'Jalousie' }
  };
  let variante = 'spalten';
  const telefon = () => matchMedia('(max-width: 600px)').matches;
  const anzahl = v => telefon() ? v.nTel : v.n;
  const zuMs = v => Math.round(ERSTE + stufen(v) * STAGGER + DAUER);
  const stufen = v => v.folge === 'mitte' ? Math.ceil(anzahl(v) / 2) - 1 : anzahl(v) - 1;

  const dockMessen = () => html.style.setProperty('--dock', Math.ceil(dock.getBoundingClientRect().height + 14) + 'px');
  function zeigeWerte() {
    const v = VARIANTEN[variante], n = anzahl(v), zu = zuMs(v);
    let t;
    if (reduziert()) t = `<span><b>reduziert</b> keine Bahnen · Seite opacity 1 → 0 → 1 · je ${FADE} ms linear · Tausch dazwischen</span>`;
    else {
      const bahn = v.achse === 'x' ? `${n} Spalten · width calc(100% / ${n} + 1px)` : `${n} Streifen · height calc(100% / ${n} + 1px)`;
      const bewegung = v.art === 'skalieren'
        ? `scaleY 0 → 1, origin bottom · öffnen 1 → 0, origin top`
        : (v.achse === 'x' ? `translateY(100% → 0) · öffnen 0 → −100%` : `translateX(−100% → 0) · öffnen 0 → 100%`);
      const folge = v.folge === 'mitte' ? 'von der Mitte nach außen' : 'links nach rechts';
      const zurueck = v.art === 'skalieren' ? 'origin getauscht, rechts nach links' : (v.achse === 'x' ? 'von oben, rechts nach links' : 'von rechts');
      t = `<span><b>${v.titel}</b> ${bahn} · ${bewegung}</span>` +
          `<span><b>je bahn</b> ${DAUER} ms · <b>stagger</b> ${STAGGER} ms ab ${ERSTE} ms, ${folge} · ${KURVE}</span>` +
          `<span class="lang"><b>zu</b> ${zu} ms · <b>halten</b> ${HALTEN} ms · <b>scrim</b> #1d1d1d 0 → ${SCRIM} · <b>zurück</b> ${zurueck} · <b>neu</b> y ${NEU.y}px → 0, ${NEU.dauer} ms</span>`;
    }
    werteFeld.innerHTML = t;
    dockMessen();
  }

  /* ---------- Bahnen erzeugen ---------- */
  let bahnen = [];
  function baueBahnen(name, nr) {
    const v = VARIANTEN[variante], n = anzahl(v);
    bahnenBox.innerHTML = '';
    bahnen = [];
    for (let i = 0; i < n; i++) {
      const b = document.createElement('div');
      b.className = 'bahn';
      const p = (i * 100 / n).toFixed(4) + '%';
      // 1 px Überlappung gegen Subpixel-Lücken
      if (v.achse === 'x') Object.assign(b.style, { left: p, top: 0, bottom: 0, width: `calc(${100 / n}% + 1px)` });
      else Object.assign(b.style, { top: p, left: 0, right: 0, height: `calc(${100 / n}% + 1px)` });
      if (v.art === 'fahren') {
        // Jede Bahn trägt den ganzen Bildschirm, um ihre eigene Breite verschoben: Der Seitenname setzt sich aus den Bahnen zusammen
        const voll = document.createElement('div');
        voll.className = 'bahn__voll';
        if (v.achse === 'x') Object.assign(voll.style, { top: 0, bottom: 0, width: `${n * 100}%`, left: `${-i * 100}%` });
        else Object.assign(voll.style, { left: 0, right: 0, height: `${n * 100}%`, top: `${-i * 100}%` });
        voll.innerHTML = `<div class="mono"><span>Kette&amp;Schuss</span><span>Digitalstudio · Bielefeld</span></div><b class="bahn__name gross">${name}</b><div class="mono"><span>${nr}</span><span>Seite wird geladen</span></div>`;
        b.appendChild(voll);
      }
      if (v.achse === 'x' && v.art === 'fahren') {
        const z = document.createElement('span');
        z.className = 'bahn__nr mono';
        z.textContent = String(i + 1).padStart(2, '0');
        b.appendChild(z);
      }
      bahnenBox.appendChild(b);
      bahnen.push(b);
    }
  }
  // Reihenfolge der Bahnen: links → rechts, rechts → links oder Mitte → außen
  function stufe(i, n, folge, zurueck) {
    if (folge === 'mitte') return Math.round(Math.abs(i - (n - 1) / 2) - (n % 2 ? 0 : .5));
    return zurueck ? n - 1 - i : i;
  }
  function frames(v, phase, zurueck) {
    if (v.art === 'skalieren') {
      // K72: schließen mit Ursprung unten, öffnen mit Ursprung oben; zurück getauscht
      const unten = (phase === 'zu') !== zurueck;
      return { kf: { transform: phase === 'zu' ? ['scaleY(0)', 'scaleY(1)'] : ['scaleY(1)', 'scaleY(0)'] }, origin: unten ? '50% 100%' : '50% 0%' };
    }
    const T = v.achse === 'x' ? 'translateY' : 'translateX';
    const rein = v.achse === 'x' ? (zurueck ? '-100%' : '100%') : (zurueck ? '100%' : '-100%');
    const raus = rein.startsWith('-') ? '100%' : '-100%';
    return { kf: { transform: phase === 'zu' ? [`${T}(${rein})`, `${T}(0)`] : [`${T}(0)`, `${T}(${raus})`] }, origin: '50% 50%' };
  }

  function setzeVariante(name) {
    variante = name;
    document.body.dataset.modus = name;
    html.style.setProperty('--n', anzahl(VARIANTEN[name]));
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === name)));
    zeigeWerte();
  }

  /* ---------- Bilder: eigene SVG-Kompositionen je Projekt ---------- */
  function bild(art, w, h, gross) {
    let g = '';
    const u = Math.min(w, h);
    if (art === 'brennwerk') {          // Heiztechnik: Flammenbalken
      g += `<rect width="${w}" height="${h}" fill="#121212"/>`;
      const n = 18, bw = w / n;
      for (let i = 0; i < n; i++) {
        const hh = h * (.18 + .62 * Math.pow(Math.sin((i + .5) / n * Math.PI), 1.6));
        g += `<rect x="${(i * bw + bw * .18).toFixed(1)}" y="${(h - hh).toFixed(1)}" width="${(bw * .64).toFixed(1)}" height="${hh.toFixed(1)}" fill="${i % 4 === 1 ? '#f2f2f2' : '#2f4bff'}"/>`;
      }
      if (gross) g += `<text x="${w * .06}" y="${h * .16}" font-family="TikTok Sans, sans-serif" font-stretch="75%" font-weight="800" font-size="${u * .14}" fill="#f2f2f2" letter-spacing="-2">BRENNWERK</text>`;
    } else if (art === 'muehlenkamp') { // Optiker: zwei Gläser
      g += `<rect width="${w}" height="${h}" fill="#e6e1d8"/>`;
      const r = u * .24, cy = h * .52;
      g += `<circle cx="${w * .5 - r * 1.15}" cy="${cy}" r="${r}" fill="none" stroke="#0b0b0b" stroke-width="${u * .045}"/><circle cx="${w * .5 + r * 1.15}" cy="${cy}" r="${r}" fill="#0b0b0b"/>`
        + `<path d="M${w * .5 - r * .2} ${cy - r * .2} q${r * .2} ${-r * .25} ${r * .4} 0" fill="none" stroke="#0b0b0b" stroke-width="${u * .03}"/>`;
      if (gross) g += `<text x="${w * .06}" y="${h * .92}" font-family="DM Mono, monospace" font-size="${u * .05}" fill="#0b0b0b" letter-spacing="2">MÜHLENKAMP · OPTIK SEIT 1958</text>`;
    } else if (art === 'tannhof') {     // Saatgut: Samenraster
      g += `<rect width="${w}" height="${h}" fill="#2c5a3a"/>`;
      const sp = 9, zl = Math.round(sp * h / w), dx = w / sp, dy = h / zl;
      for (let z = 0; z < zl; z++) for (let s = 0; s < sp; s++) {
        const rot = (s * 37 + z * 61) % 180;
        g += `<ellipse cx="${(s + .5) * dx}" cy="${(z + .5) * dy}" rx="${dx * .14}" ry="${dy * .3}" fill="${(s + z) % 5 === 0 ? '#f2c94c' : '#e9e4d4'}" transform="rotate(${rot} ${(s + .5) * dx} ${(z + .5) * dy})"/>`;
      }
    } else if (art === 'ostwind') {     // Kurier-Genossenschaft: Wind und Räder
      g += `<rect width="${w}" height="${h}" fill="#17637d"/>`;
      for (let i = -8; i < 20; i++) g += `<path d="M${i * w * .07} ${h} l${h * .7} ${-h}" stroke="#7fc6db" stroke-width="${u * .012}" opacity=".55"/>`;
      g += `<circle cx="${w * .32}" cy="${h * .64}" r="${u * .2}" fill="none" stroke="#f2f2f2" stroke-width="${u * .03}"/><circle cx="${w * .72}" cy="${h * .64}" r="${u * .2}" fill="none" stroke="#f2f2f2" stroke-width="${u * .03}"/>`
        + `<path d="M${w * .32} ${h * .64} L${w * .46} ${h * .38} L${w * .64} ${h * .38} L${w * .72} ${h * .64} M${w * .46} ${h * .38} L${w * .52} ${h * .64} L${w * .64} ${h * .38}" fill="none" stroke="#f2f2f2" stroke-width="${u * .03}" stroke-linejoin="round"/>`;
    } else {                            // Theaterfestival: Ziffern und Vorhangfalten
      g += `<rect width="${w}" height="${h}" fill="#0b0b0b"/>`;
      for (let i = 0; i < 14; i++) g += `<rect x="${i * w / 14}" y="0" width="${w / 28}" height="${h}" fill="#2a2440"/>`;
      g += `<text x="${w * .5}" y="${h * .78}" text-anchor="middle" font-family="TikTok Sans, sans-serif" font-stretch="75%" font-weight="900" font-size="${h * .82}" fill="#b9a4ff" letter-spacing="-8">27</text>`;
    }
    return `<svg data-setzen viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid slice" aria-hidden="true">${g}</svg>`;
  }
  function reel() {
    // Kette (senkrecht) und Schuss (waagerecht) im Wechsel: das Studio-Motiv als Showreel-Standbild
    const w = 800, h = 350, n = 32, m = 13, bw = w / n, bh = h / m;
    let g = `<rect width="${w}" height="${h}" fill="#161616"/>`;
    for (let s = 0; s < n; s++) g += `<rect x="${s * bw + bw * .2}" y="0" width="${bw * .6}" height="${h}" fill="#333333"/>`;
    for (let z = 0; z < m; z++) for (let s = 0; s < n; s++) {
      if ((s + z) % 2) g += `<rect x="${s * bw - 1}" y="${z * bh + bh * .22}" width="${bw + 2}" height="${bh * .56}" fill="${z === 6 && s > 9 && s < 23 ? '#ff5a1f' : '#d9d9d9'}"/>`;
    }
    return `<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid slice" aria-hidden="true">${g}</svg>`;
  }

  function band() {
    // Gewebe in der Draufsicht: helle Kette, dunkler Schuss, ein Faden in Signalfarbe
    const w = 1600, h = 260, n = 64, m = 13, bw = w / n, bh = h / m;
    let g = `<rect width="${w}" height="${h}" fill="#e4e4e4"/>`;
    for (let s = 0; s < n; s++) g += `<rect x="${s * bw + bw * .12}" y="0" width="${bw * .76}" height="${h}" fill="#cfcfcf"/>`;
    for (let z = 0; z < m; z++) for (let s = 0; s < n; s++) {
      if ((s + z * 3) % 4 < 2) g += `<rect x="${s * bw - .5}" y="${z * bh + bh * .14}" width="${bw + 1}" height="${bh * .72}" fill="${z === 7 ? '#ff5a1f' : '#0b0b0b'}"/>`;
    }
    return `<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid slice" aria-hidden="true">${g}</svg>`;
  }

  /* ---------- Inhalte: erfundenes Digitalstudio Kette & Schuss, Bielefeld ---------- */
  const SEITEN = {
    start:    { nr: '01 / 04', name: 'Start',    titel: 'Kette & Schuss', hell: false, ordnung: 0 },
    arbeiten: { nr: '02 / 04', name: 'Arbeiten', titel: 'Arbeiten', hell: true, ordnung: 1 },
    studio:   { nr: '03 / 04', name: 'Studio',   titel: 'Studio', hell: true, ordnung: 3 },
    kontakt:  { nr: '04 / 04', name: 'Kontakt',  titel: 'Kontakt', hell: false, ordnung: 4 }
  };
  const PROJEKTE = [
    { id: 'brennwerk', name: 'Brennwerk', kunde: 'Brennwerk Heiztechnik, Gütersloh', leistung: 'Marke, Website', jahr: '2026',
      unter: 'Ein Heizungsbauer mit 60 Leuten, der endlich so klingt, wie er arbeitet.',
      fakten: [['Kunde', 'Brennwerk Heiztechnik GmbH'], ['Leistung', 'Markenauftritt, Website, Recruiting'], ['Zeitraum', 'Januar bis Juni 2026'], ['Ergebnis', '41 Bewerbungen im ersten Quartal']],
      text: ['Brennwerk hatte ein gutes Handwerk und ein Logo aus den Neunzigern. Wir haben die Marke um ein einziges Bild gebaut: Wärme als Balken, die steigen und fallen.', 'Die Website erklärt Wärmepumpen ohne Fachchinesisch und führt in drei Schritten zum Vor-Ort-Termin. Monteure finden ihre Stellen in zwei Klicks.'] },
    { id: 'muehlenkamp', name: 'Mühlenkamp Optik', kunde: 'Mühlenkamp, Bielefeld', leistung: 'Shop, Terminbuchung', jahr: '2026',
      unter: 'Drei Generationen Augenoptik und ein Shop, der Beratung nicht ersetzt.',
      fakten: [['Kunde', 'Mühlenkamp Optik, Altstadt Bielefeld'], ['Leistung', 'Onlineshop, Sehtest-Buchung'], ['Zeitraum', 'März bis August 2026'], ['Ergebnis', '62 % der Termine online gebucht']],
      text: ['Fassungen kann man online ansehen, aber nicht anprobieren. Der Shop zeigt deshalb jede Brille an drei Gesichtern und endet nicht im Warenkorb, sondern im Termin.', 'Die Buchung zeigt freie Zeiten für Sehtest und Anpassung getrennt. Das Team pflegt beides selbst, ohne Agentur.'] },
    { id: 'tannhof', name: 'Tannhof Saatgut', kunde: 'Tannhof, Detmold', leistung: 'Verpackung, Shop', jahr: '2025',
      unter: 'Samenfeste Sorten aus Lippe, verpackt wie kleine Bücher.',
      fakten: [['Kunde', 'Tannhof Saatgut eG'], ['Leistung', 'Verpackungssystem, Onlineshop'], ['Zeitraum', 'Herbst 2025'], ['Sortiment', '140 Sorten, 12 Familien']],
      text: ['Jede Tüte folgt demselben Raster: Sorte oben, Aussaat-Kalender in der Mitte, Herkunft unten. Die Farbe zeigt die Pflanzenfamilie.', 'Der Shop übernimmt das Raster eins zu eins. Wer eine Sorte kennt, findet sie auf dem Regal und im Netz an derselben Stelle.'] },
    { id: 'ostwind', name: 'Ostwind Kollektiv', kunde: 'Ostwind, Bielefeld', leistung: 'App, Marke', jahr: '2025',
      unter: 'Eine Fahrradkurier-Genossenschaft bucht ihre Touren jetzt selbst.',
      fakten: [['Kunde', 'Ostwind Kurierkollektiv eG'], ['Leistung', 'Buchungs-App, Markenauftritt'], ['Zeitraum', 'Mai bis Oktober 2025'], ['Ergebnis', '2.300 Touren im ersten Monat']],
      text: ['Vorher liefen Aufträge über ein Telefon und eine Tabelle. Jetzt buchen Kunden in der App, und die Fahrer verteilen die Touren unter sich.', 'Die Marke kommt mit zwei Farben und einem Rad aus. Sie muss auf einer Warnweste genauso funktionieren wie auf dem Bildschirm.'] },
    { id: 'spielzeit', name: 'Spielzeit Ost', kunde: 'Theaterfestival, Bielefeld', leistung: 'Kampagne, Website', jahr: '2024',
      unter: 'Ein Festival für freies Theater, 27 Stücke in neun Tagen.',
      fakten: [['Kunde', 'Spielzeit Ost e. V.'], ['Leistung', 'Kampagne, Programm-Website'], ['Zeitraum', 'Frühjahr 2024'], ['Ergebnis', '91 % Auslastung']],
      text: ['Das Festival hat kein Haus, nur Orte. Die Kampagne setzt deshalb auf eine Zahl statt auf ein Bild: 27 Stücke, groß auf jedem Plakat.', 'Die Website sortiert das Programm nach Abend, nicht nach Ort. Wer einen freien Abend hat, sieht sofort, was läuft.'] }
  ];
  const proj = id => PROJEKTE.find(p => p.id === id);

  function htmlStart() {
    return `<section class="s s-start">
      <p class="kopfzeile mono" data-rein><span>Digitalstudio für Marken und Produkte</span><span>Seit 2014 · 14 Leute · Ravensberger Spinnerei</span></p>
      <h1 class="held gross" tabindex="-1" data-rein><span>Marken mit</span><span>Struktur.</span></h1>
      <div class="start__unten" data-rein>
        <div>
          <p class="lead">Wir entwerfen Marken, Websites und digitale Produkte für den Mittelstand in Ostwestfalen und darüber hinaus. Ein Raster, keine Vorlagen.</p>
          <p class="mono">Aktuell: zwei Plätze ab Februar 2027</p>
        </div>
        <a class="reel" href="#arbeiten" data-ziel="arbeiten">
          <span class="reel__bild">${reel()}<span class="reel__play mono">Showreel</span></span>
          <span class="zeile mono"><span>Showreel 2026</span><span>01:12</span></span>
        </a>
        <a class="zuarbeiten" href="#arbeiten" data-ziel="arbeiten"><span class="mono">38 Projekte seit 2014</span><b><span>Arbeiten</span><span aria-hidden="true">→</span></b></a>
      </div>
    </section>`;
  }
  function htmlArbeiten() {
    const zeilen = PROJEKTE.map((p, i) => `
      <li><a href="#${p.id}" data-ziel="${p.id}">
        <span class="mono">${String(i + 1).padStart(2, '0')}</span>
        <span class="name">${p.name}</span>
        <span class="mono wer">${p.kunde}<br>${p.leistung}</span>
        <span class="mono jahr">${p.jahr}</span>
        <span class="daumen">${bild(p.id, 300, 200, false)}</span>
      </a></li>`).join('');
    return `<section class="s s-arbeiten">
      <div class="titelzeile" data-rein><h1 class="titel gross" tabindex="-1">Arbeiten<sup>05 / 38</sup></h1><span class="mono" style="color:var(--leise)">Auswahl 2024 bis 2026</span></div>
      <ul class="liste" data-rein>${zeilen}</ul>
    </section>`;
  }
  function htmlProjekt(p) {
    const i = PROJEKTE.indexOf(p), n = PROJEKTE[(i + 1) % PROJEKTE.length];
    return `<section class="s s-projekt projekt">
      <figure class="projekt__bild" data-rein>${bild(p.id, 500, 640, true)}</figure>
      <div class="projekt__text" data-rein>
        <p class="mono">Projekt ${String(i + 1).padStart(2, '0')} · ${p.leistung} · ${p.jahr}</p>
        <div><h1 class="titel gross" tabindex="-1" style="--z:${p.name.length}">${p.name}</h1><p class="unter">${p.unter}</p></div>
        <dl class="fakten mono">${p.fakten.map(([a, b]) => `<div><dt>${a}</dt><dd>${b}</dd></div>`).join('')}</dl>
        <div class="fliess">${p.text.map(t => `<p>${t}</p>`).join('')}</div>
        <div class="projekt__nav">
          <a class="zurueck mono" href="#arbeiten" data-ziel="arbeiten" data-richtung="zurueck"><span class="roll"><span>← Alle Arbeiten</span><span aria-hidden="true">← Alle Arbeiten</span></span></a>
          <a class="naechstes" href="#${n.id}" data-ziel="${n.id}"><span class="mono">Nächstes Projekt</span><b>${n.name} →</b></a>
        </div>
      </div>
    </section>`;
  }
  function htmlStudio() {
    return `<section class="s s-studio">
      <h1 class="sr" tabindex="-1">Studio</h1>
      <p class="aussage" data-rein>Vierzehn Leute in einer alten Leinenweberei. <em>Wir verweben Marke und Technik, bis man die Naht nicht mehr sieht.</em></p>
      <div class="studio__rechts" data-rein>
        <p>Kette sind die Fäden, die stehen bleiben: Raster, Typo, Haltung. Schuss ist das, was sich jedes Mal ändert. Gute digitale Marken brauchen beides.</p>
        <ul class="leistungen mono"><li><span>Marke</span><span>Strategie, Name, Gestaltung</span></li><li><span>Web</span><span>Websites, Shops, Systeme</span></li><li><span>Produkt</span><span>Apps, Werkzeuge, Prototypen</span></li></ul>
      </div>
      <figure class="studio__band" data-rein>${band()}<figcaption class="mono">Webstuhl im Studio, Baujahr 1912</figcaption></figure>
      <div class="zahlen" data-rein>
        <div><b>14</b><span class="mono">Leute im Studio</span></div>
        <div><b>2014</b><span class="mono">gegründet in Bielefeld</span></div>
        <div><b>38</b><span class="mono">Marken begleitet</span></div>
        <div><b>9 J.</b><span class="mono">längste Zusammenarbeit</span></div>
      </div>
    </section>`;
  }
  function htmlKontakt() {
    return `<section class="s s-kontakt">
      <h1 class="frage gross" tabindex="-1" data-rein>Haben Sie etwas, das Struktur braucht?</h1>
      <a class="mail" href="#kontakt" data-rein><span class="roll"><span>hallo@kette-und-schuss.de</span><span aria-hidden="true">hallo@kette-und-schuss.de</span></span><span aria-hidden="true">→</span></a>
      <div class="kontakt__fuss" data-rein>
        <div class="adresse">
          <p><span class="mono">Studio</span>Ravensberger Spinnerei, Haus C<br>Am Ravensberger Park 6, 33607 Bielefeld</p>
          <p><span class="mono">Telefon</span>0521 329 17 40<br>Mo bis Do, 9 bis 17 Uhr</p>
          <p><span class="mono">Neue Projekte</span>Lena Strothmann<br>Geschäftsführung</p>
          <p><span class="mono">Jobs</span>Wir suchen eine Frontend-Entwicklerin oder einen Frontend-Entwickler.</p>
        </div>
        <p class="wortmarke" data-fit aria-hidden="true">Kette<i>&amp;</i>Schuss</p>
      </div>
    </section>`;
  }

  function info(ziel) {
    const p = proj(ziel);
    if (p) return { nr: `Projekt 0${PROJEKTE.indexOf(p) + 1} / 05`, name: p.name.split(' ')[0], titel: p.name, hell: false, nav: 'arbeiten', ordnung: 2 };
    const s = SEITEN[ziel];
    return { nr: s.nr, name: s.name, titel: s.titel, hell: s.hell, nav: ziel, ordnung: s.ordnung };
  }
  function passeAn() {
    seite.querySelectorAll('[data-fit]').forEach(el => {
      el.style.fontSize = '100px';
      const breite = el.parentElement.clientWidth;
      el.style.fontSize = Math.floor(100 * breite / el.getBoundingClientRect().width * 10) / 10 + 'px';
    });
  }
  let aktuell = 'start';
  function zeige(ziel) {
    const p = proj(ziel), i = info(ziel);
    seite.innerHTML = p ? htmlProjekt(p) : ({ start: htmlStart, arbeiten: htmlArbeiten, studio: htmlStudio, kontakt: htmlKontakt })[ziel]();
    html.style.setProperty('--bg', i.hell ? '#f2f2f2' : '#0b0b0b');
    html.style.setProperty('--fg', i.hell ? '#0b0b0b' : '#ffffff');
    document.querySelectorAll('.nav a').forEach(a => {
      if (a.dataset.ziel === i.nav) a.setAttribute('aria-current', 'page'); else a.removeAttribute('aria-current');
    });
    document.title = (ziel === 'start' ? 'Kette & Schuss' : i.titel + ' · Kette & Schuss') + ' · Fundus-Demo';
    aktuell = ziel;
    passeAn();
  }

  /* ---------- Der Übergang ---------- */
  let laeuft = false, lauf = 0, laufende = [];
  const warte = (n, t) => new Promise(r => setTimeout(() => r(t === lauf), n));
  const anim = (el, kf, o) => { const a = el.animate(kf, { fill: 'both', ...o }); laufende.push(a); return a; };
  const fertig = list => Promise.all(list.map(a => a.finished.catch(() => {})));

  function aufraeumen() {
    laufende.forEach(a => a.cancel()); laufende = [];
    shutter.classList.remove('an');
    laeuft = false;
  }
  function abbrechen() { lauf++; aufraeumen(); }

  function bahnenFahren(phase, zurueck) {
    const v = VARIANTEN[variante], n = bahnen.length, { kf, origin } = frames(v, phase, zurueck);
    return bahnen.map((b, i) => {
      b.style.transformOrigin = origin;
      return anim(b, kf, { duration: DAUER, delay: ERSTE + stufe(i, n, v.folge, zurueck) * STAGGER, easing: KURVE });
    });
  }

  async function uebergang(ziel, quelle, mitFokus) {
    if (laeuft) return;
    laeuft = true;
    const t = ++lauf, i = info(ziel), v = VARIANTEN[variante];
    const zurueck = (quelle && quelle.dataset.richtung === 'zurueck') || i.ordnung < info(aktuell).ordnung;
    zeigeWerte();

    const tauschen = () => {
      zeige(ziel);
      scrollTo(0, 0);
      ansage.textContent = 'Seite geladen: ' + i.titel;
    };
    const neuSetzen = () => {
      seite.querySelectorAll('[data-rein]').forEach((el, k) => anim(el, { transform: [`translateY(${NEU.y}px)`, 'translateY(0)'], opacity: [0, 1] }, { duration: NEU.dauer, delay: k * STAGGER, easing: KURVE }));
    };

    if (reduziert()) {
      await fertig([anim(seite, { opacity: [1, 0] }, { duration: FADE, easing: 'linear' })]);
      if (t !== lauf) return;
      laufende.forEach(a => a.cancel()); laufende = [];
      tauschen();
      await fertig([anim(seite, { opacity: [0, 1] }, { duration: FADE, easing: 'linear' })]);
    } else {
      baueBahnen(i.name, i.nr);
      sNameText.textContent = i.name; sNr.textContent = i.nr;
      shutter.classList.add('an');
      anim(scrim, { opacity: [0, SCRIM] }, { duration: zuMs(v), easing: KURVE });
      const zu = bahnenFahren('zu', zurueck);
      await fertig(zu);
      if (t !== lauf) return;
      // Inhalt hinter den geschlossenen Bahnen tauschen
      laufende.filter(a => a.effect.target === scrim).forEach(a => a.cancel());
      tauschen();
      if (v.art === 'skalieren') anim(sName, { opacity: [0, 1] }, { duration: 150, easing: 'linear' });
      if (!(await warte(HALTEN, t))) return;
      if (v.art === 'skalieren') anim(sName, { opacity: [1, 0] }, { duration: 150, easing: 'linear' });
      const auf = bahnenFahren('auf', zurueck);
      neuSetzen();
      await fertig(auf);
    }
    if (t !== lauf) return;
    await fertig(laufende.filter(a => a.playState !== 'idle'));
    if (t !== lauf) return;
    aufraeumen();
    if (mitFokus) { const h = seite.querySelector('h1'); if (h) h.focus({ preventScroll: true }); }
  }

  document.addEventListener('click', e => {
    const a = e.target.closest('[data-ziel]');
    if (!a) return;
    e.preventDefault();
    if (a.dataset.ziel === aktuell || laeuft) return;
    stoppeVorfuehrung();
    uebergang(a.dataset.ziel, a, true);
  });
  document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppeVorfuehrung(); setzeVariante(b.dataset.variante); }));
  addEventListener('resize', () => { setzeVariante(variante); passeAn(); });

  /* ---------- Vorführung: gedachter Zeiger klickt sich durch ---------- */
  const PFAD = [
    ['.nav [data-ziel="arbeiten"]', 'spalten'],
    ['.liste [data-ziel="brennwerk"]', 'streifen'],
    ['[data-richtung="zurueck"]', 'jalousie'],
    ['.nav [data-ziel="studio"]', 'jalousie'],
    ['.nav [data-ziel="kontakt"]', 'spalten'],
    ['.marke', 'streifen']
  ];
  let runde = 0, uhr = null, lief = false, schritt = 0, zx = 0, zy = 0;
  const pause_ = (n, r) => new Promise(res => { uhr = setTimeout(() => res(r === runde), n); });

  function zeigerZu(x, y, r) {
    const von = `translate(${zx}px, ${zy}px)`, nach = `translate(${x}px, ${y}px)`;
    zx = x; zy = y;
    zeiger.animate({ transform: [von, nach] }, { duration: reduziert() ? 1 : 600, easing: KURVE, fill: 'forwards' });
    return pause_(reduziert() ? 200 : 660, r);
  }
  async function spiel(r, erst) {
    if (!(await pause_(erst, r))) return;
    while (r === runde) {
      const [sel, v] = PFAD[schritt % PFAD.length];
      let el = document.querySelector(sel);
      if (!el || el.getClientRects().length === 0) el = document.querySelector('.marke');
      const b = el.getBoundingClientRect();
      const x = Math.round(b.left + Math.min(b.width * 0.5, 160)), y = Math.round(b.top + b.height * 0.5);
      if (!(await zeigerZu(x, y, r))) return;
      zeiger.animate({ scale: [1, 0.7, 1] }, { duration: 260, easing: KURVE });
      setzeVariante(v);
      if (!(await pause_(120, r))) return;
      const ziel = el.dataset.ziel;
      schritt++;
      if (ziel !== aktuell) {
        uebergang(ziel, el, false);
        const vv = VARIANTEN[v];
        if (!(await pause_(zuMs(vv) * 2 + HALTEN + 1500, r))) return;
      }
    }
  }
  let nachStart = null;
  function stoppeVorfuehrung() { runde++; clearTimeout(uhr); clearTimeout(nachStart); lief = false; zeiger.classList.remove('an'); }

  function neustart() {
    stoppeVorfuehrung();
    abbrechen();
    setzeVariante(variante);
    zeige('start'); scrollTo(0, 0);
    nachStart = setTimeout(() => uebergang('arbeiten', document.querySelector('.nav [data-ziel="arbeiten"]'), false), 500);
  }
  function vorfuehren() {
    stoppeVorfuehrung();
    abbrechen();
    zeige('start'); scrollTo(0, 0);
    schritt = 0; lief = true;
    zx = Math.round(innerWidth * 0.5); zy = Math.round(innerHeight * 0.42);
    zeiger.animate({ transform: [`translate(${zx}px, ${zy}px)`] }, { duration: 1, fill: 'forwards' });
    zeiger.classList.add('an');
    spiel(runde, 1300);
  }
  function pause() { const war = lief; stoppeVorfuehrung(); lief = war; laufende.forEach(a => a.pause()); }
  function weiter() {
    laufende.forEach(a => { if (a.playState === 'paused') a.play(); });
    if (lief) { const r = ++runde; zeiger.classList.add('an'); spiel(r, 400); }
  }

  setzeVariante('spalten');
  zeige('start');
  addEventListener('load', () => { zeigeWerte(); passeAn(); });
  if (document.fonts) document.fonts.ready.then(() => { dockMessen(); passeAn(); });

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