Zum Inhalt
← Hero & Section-Layouts

Prozess-/Timeline-Sektion mit wachsender Linie

Eine dünne Linie füllt sich mit dem Scroll und zeigt, in welcher Etappe des Ablaufs man gerade steht: als wachsende Pille, als Unterstrich oder als durchgehende Linie.

Eine Sektion erklärt einen Ablauf oder eine Geschichte in 3 bis 6 Schritten, etwa Erstgespräch, Konzept, Design, Launch oder Jahreszahlen. Eine dünne Linie füllt sich mit dem Scroll, vertikal am Rand oder als Unterstrich je Schritt. Der aktive Schritt hebt sich ab: Der Punkt wird zur Pille, ein Label blendet ein, die Farbe wechselt. Oft steht links eine sticky Schrittliste und rechts scrollen die Inhalte. Ein Klick auf einen Schritt scrollt weich zum Inhalt. Abgeschlossene Schritte bleiben markiert, oder ihre Füllung wischt heraus.

Die Linie macht Fortschritt körperlich. Man sieht, wo man im Ablauf steht und wie viel noch kommt. Ein abstrakter Prozess bekommt Richtung und Rhythmus, und die Füllung belohnt das Weiterscrollen.

Wann einsetzen

„So arbeiten wir“-Seiten, Leistungsabläufe, Onboarding, Firmengeschichte, Kursaufbau mit Modulen, Bewerbungs- oder Bestellschritte.

Wann nicht

Bei nur zwei Schritten. Wenn die Schritte nicht wirklich aufeinander folgen. In sehr kurzen Sektionen, weil sich die Linie dann zu schnell füllt. Und nie so, dass Inhalte nur im aktiven Zustand lesbar sind.

Varianten

  • Unterstrich je Schritt füllt sich per scaleX, nativ mit CSS view-timeline; abgeschlossene Schritte wischen in 0,5 s heraus (Granola)
  • Vertikale Schiene: Punkte von 4px, der aktive wächst zur 80px-Pille und füllt sich nach dem Scroll-Fortschritt (Twenty)
  • Durchgehende Linie wächst in der Höhe neben den Inhalten, Jahreszahlen sticky daneben (Aceternity Timeline)
  • Gepinnte Timeline mit scrub, Punkte und Karten werden nacheinander aktiv (GSAP)
  • SVG-Pfad statt gerader Linie (siehe svg-pfad-zeichnen-scroll)

Umsetzung

Markup: <ol> mit einem <li> je Schritt (Nummer, Titel, Text), damit die Reihenfolge semantisch stimmt. Die Linie ist ein dekoratives Element mit transform: scaleY(0) und transform-origin: top. Nativ: .fuellung { animation: fuellen linear both; animation-timeline: --prozess; animation-range: cover 30% cover 70%; } mit @keyframes fuellen { to { transform: scaleY(1); } } und view-timeline-name: --prozess an der Liste. Für eine Füllung je Schritt bekommt jeder Inhaltsblock eine eigene view-timeline (--s0, --s1 …); timeline-scope am gemeinsamen Elternelement macht sie für die Schrittliste nutzbar (Granola, Range cover 50vh bis cover calc(100% − 50vh)). Abgeschlossene Schritte bekommen per Klasse transform-origin: right und eine 0,5-s-Animation auf scaleX(0). Fallback mit @supports not (animation-timeline: view()): Linie voll zeigen oder ausblenden, alle Schritte gleich stark. Mit JS: Fortschritt je Schritt als (50vh − top) / Höhe berechnen und als Custom Property setzen; mit GSAP ScrollTrigger und scrub auf scaleY, toggleClass für die Punkte. Die Demo rechnet in Vanilla JS, damit alle drei Varianten dieselbe Messung nutzen. Fallstricke: scaleY statt height animieren (kein Reflow), Linie per z-index hinter die Punkte legen, Punkte mittig auf der Linie ausrichten.

Robust umsetzen

Telefon. Vertikale Linie links am Rand, Inhalte einspaltig, die sticky Schrittliste entfällt. Granola zeigt die Variante erst ab md, Twenty nutzt mobil ein Swipe-Deck.

Ohne Bewegung. Linie statisch voll zeigen oder den Fortschritt ohne weiche Übergänge anzeigen. Aktive Schritte über Farbe und Text markieren statt über Bewegung.

Barrierefrei. <ol> für die Reihenfolge. Linie und Punkte sind dekorativ (aria-hidden). Den aktiven Schritt nicht nur über Farbe zeigen, sondern auch per Text und aria-current="step". Sprungziele der Schrittliste als echte Buttons oder Anker mit sichtbarem Fokus. Alle Inhalte bleiben ohne aktiven Zustand voll lesbar.

Code

<!DOCTYPE html>
<!--
  Prozess-/Timeline-Sektion mit wachsender Linie
  Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/prozess-timeline-wachsende-linie/
  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>Prozess-/Timeline-Sektion mit wachsender Linie · 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 {
  --grund: #0b0b0b; --flaeche: #141414; --text: #ffffff; --meta: #8a8a8a; --stumm: #636363;   /* noch nicht erreichte Phasen: 3,2:1 (große Schrift) */
  --linie: rgba(255, 255, 255, .14); --signal: #ff5a1f;
  --kurve: cubic-bezier(0.16, 1, 0.3, 1);   /* expo.out, Rezept Studio-Bühne */
  --rand: clamp(20px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'TikTok Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { background: var(--grund); overflow-x: clip; }
.mono { font: 500 12px/1.4 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; color: var(--meta); }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--signal); outline-offset: 4px; }

/* Kopf */
.kopf { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; padding: 22px var(--rand); border-bottom: 1px solid var(--linie); }
.marke-wort { font-weight: 800; font-stretch: 75%; font-size: 26px; letter-spacing: -.02em; line-height: 1; }
.marke-wort small { font: 500 12px/1 'DM Mono', monospace; letter-spacing: .04em; color: var(--meta); margin-left: 10px; vertical-align: 4px; }
.modi { display: flex; gap: 4px; align-items: center; }
.modi > span { margin-right: 6px; }
.modi button { color: var(--meta); border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; transition: background-color .2s var(--kurve), color .2s var(--kurve), border-color .2s var(--kurve); }
.modi button:hover { color: var(--text); border-color: var(--meta); }
.modi button[aria-pressed='true'] { background: var(--text); color: var(--grund); border-color: var(--text); }

/* Einstieg */
.intro { display: grid; grid-template-columns: 1fr minmax(240px, 30%); align-items: end; gap: 24px 48px; padding: clamp(48px, 9vh, 104px) var(--rand) clamp(40px, 7vh, 80px); border-bottom: 1px solid var(--linie); }
.intro h1 { font-weight: 800; font-stretch: 75%; font-size: clamp(3.5rem, 10vw, 11rem); letter-spacing: -.03em; line-height: .9; }
.intro h1 em { font-style: normal; color: var(--meta); }
.intro p:not(.mono) { font-size: 1.125rem; line-height: 1.4; color: #cfcfcf; max-width: 34ch; margin-top: 14px; }

/* Prozess: links sticky Index, rechts die Etappen */
.prozess { display: grid; grid-template-columns: minmax(280px, 36%) 1fr; position: relative; }
.index { position: sticky; top: 0; height: 100svh; display: flex; flex-direction: column; justify-content: space-between; padding: 36px var(--rand) 28px; border-right: 1px solid var(--linie); }
.index__kopf { display: flex; justify-content: space-between; gap: 12px; }
.index__liste { list-style: none; display: grid; gap: 6px; position: relative; }
.index__liste button { display: grid; grid-template-columns: 22px 34px 1fr; align-items: start; width: 100%; text-align: left; padding: 12px 0; min-height: 96px; }
.nr { padding-top: 9px; }
.name { display: block; font-weight: 600; font-size: clamp(26px, 2.7vw, 40px); letter-spacing: -.02em; line-height: 1; color: var(--stumm); transition: color .3s var(--kurve); }
.index__liste button:hover .name { color: var(--meta); }
.zeit { display: block; margin-top: 8px; }
.jetzt { display: inline-block; margin-left: 8px; color: var(--signal); opacity: 0; transition: opacity .3s var(--kurve); }
.ist-aktiv .name, .index__liste .ist-aktiv button:hover .name { color: var(--text); }
.ist-fertig .name { color: var(--meta); }
.ist-aktiv .jetzt { opacity: 1; }

/* Variante A · Schiene: Punkt 4px wächst zur Pille 80px, darin Füllung (Twenty) */
.schiene-spur { position: absolute; left: 1.5px; top: 22px; bottom: 22px; width: 1px; background: var(--linie); }
.punkt { position: relative; justify-self: start; margin-top: 15px; width: 4px; height: 4px; border-radius: 4px; background: var(--stumm); overflow: hidden; transition: height .4s var(--kurve), background-color .3s var(--kurve); }
.punkt i { position: absolute; inset: 0; background: var(--signal); transform: scaleY(var(--p, 0)); transform-origin: top; transition: transform .15s linear; }
.ist-aktiv .punkt { height: 80px; margin-top: 4px; background: #2a2a2a; }
.ist-fertig .punkt { background: var(--signal); }

/* Variante B · Unterstrich: 1px-Strich 174px, Füllung 3px per scaleX (Granola) */
.strich { display: none; position: relative; width: 174px; max-width: 100%; height: 3px; margin-top: 14px; }
.strich::before { content: ''; position: absolute; left: 0; right: 0; top: 1px; height: 1px; background: var(--linie); }
.strich i { position: absolute; inset: 0; background: var(--signal); transform: scaleX(var(--p, 0)); transform-origin: left; }
.ist-fertig .strich i { transform: scaleX(0); transform-origin: right; transition: transform .5s var(--kurve); }

/* Variante C · Linie: durchgehende Linie wächst neben den Inhalten (Aceternity) */
.bahn { position: absolute; top: 0; bottom: 0; left: 0; width: 1px; background: var(--linie); display: none; pointer-events: none; }
.bahn i { position: absolute; top: 0; bottom: 0; left: -.5px; width: 2px; background: linear-gradient(to bottom, rgba(255, 90, 31, 0), var(--signal) 120px); transform: scaleY(var(--g, 0)); transform-origin: top; opacity: var(--go, 0); }
.bahn em { position: absolute; left: -3.5px; top: calc(var(--g, 0) * 100%); width: 8px; height: 8px; margin-top: -4px; border-radius: 50%; background: var(--signal); box-shadow: 0 0 0 5px rgba(255, 90, 31, .18); opacity: var(--go, 0); z-index: 2; }
.bahn b { position: absolute; left: -4px; width: 9px; height: 9px; border-radius: 50%; background: var(--grund); border: 1px solid var(--stumm); transition: background-color .3s var(--kurve), border-color .3s var(--kurve); }
.bahn b.an { background: var(--signal); border-color: var(--signal); }

[data-variante='schiene'] .punkt, [data-variante='schiene'] .schiene-spur { display: block; }
[data-variante='unterstrich'] .punkt, [data-variante='unterstrich'] .schiene-spur,
[data-variante='linie'] .punkt, [data-variante='linie'] .schiene-spur { display: none; }
[data-variante='unterstrich'] .index .strich { display: block; }
[data-variante='linie'] .bahn { display: block; }
[data-variante='unterstrich'] .index__liste button,
[data-variante='linie'] .index__liste button { grid-template-columns: 34px 1fr; }
[data-variante='unterstrich'] .index__liste button { min-height: 0; padding: 14px 0 16px; }

.werte { display: grid; gap: 4px; border-top: 1px solid var(--linie); padding-top: 14px; }
.werte b { color: var(--text); font-weight: 500; }
.werte [data-wert] { display: none; }
[data-variante='schiene'] [data-wert='schiene'], [data-variante='unterstrich'] [data-wert='unterstrich'], [data-variante='linie'] [data-wert='linie'] { display: block; }

/* Etappen */
.schritte { list-style: none; padding: 0 var(--rand) 0 calc(var(--rand) + 32px); }
.schritt { min-height: 96svh; padding: 15svh 0 10svh; display: grid; align-content: start; gap: 22px; max-width: 760px; position: relative; }
.schritt__kopf { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; border-bottom: 1px solid var(--linie); padding-bottom: 14px; }
.schritt h2 { font-weight: 800; font-stretch: 75%; font-size: clamp(2.6rem, 5.4vw, 5.6rem); letter-spacing: -.03em; line-height: .9; }
.schritt h2 span { color: var(--meta); font-weight: 600; margin-right: .18em; }
.schritt__text { display: grid; grid-template-columns: 1.4fr 1fr; gap: 28px; }
.schritt__text p { font-size: 1.125rem; line-height: 1.4; color: #d8d8d8; max-width: 40ch; }
.liefer { list-style: none; display: grid; gap: 7px; align-content: start; }
.liefer li::before { content: '+ '; color: var(--stumm); }
.liefer li { color: #bdbdbd; }
.abb { position: relative; background: var(--flaeche); border: 1px solid var(--linie); aspect-ratio: 16 / 9; overflow: hidden; }
.abb svg { display: block; width: 100%; height: 100%; }
.abb figcaption { position: absolute; left: 14px; bottom: 12px; right: 14px; display: flex; justify-content: space-between; gap: 12px; }
.schritt .strich, .schritt .punkt, .schritt .knoten { display: none; }

.outro { border-top: 1px solid var(--linie); padding: clamp(56px, 12vh, 140px) var(--rand) clamp(40px, 8vh, 80px); display: grid; gap: 28px; }
.outro p:first-child { font-weight: 800; font-stretch: 75%; font-size: clamp(2.6rem, 7vw, 7.5rem); letter-spacing: -.03em; line-height: .9; max-width: 13ch; }
.cta { justify-self: start; display: inline-flex; align-items: center; gap: 14px; height: 56px; padding: 0 26px; border-radius: 999px; background: var(--signal); color: #0b0b0b; font-weight: 600; font-size: 1.05rem; text-decoration: none; transition: transform .4s var(--kurve); }
.cta:hover { transform: translateX(4px); }
.fuss { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 18px var(--rand) 28px; border-top: 1px solid var(--linie); }

/* Reduzierte Bewegung: Fortschritt folgt dem Scroll ohne weiche Übergänge, kein Wisch, kein Pillen-Wachstum */
:root.reduziert *, :root.reduziert *::before { transition: none !important; animation: none !important; }
:root.reduziert .ist-fertig .strich i { transform: scaleX(1); }

/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .intro, .vorschau .outro, .vorschau .fuss { display: none; }

/* Telefon und schmale Fenster: einspaltig, Linie links am Rand */
@media (max-width: 760px) {
  .intro { grid-template-columns: 1fr; }
  .prozess { display: block; }
  .index { display: none; }
  .schritte { padding: 0 var(--rand) 0 calc(var(--rand) + 26px); position: relative; }
  .schritte::before { content: ''; position: absolute; left: calc(var(--rand) + 1px); top: 0; bottom: 0; width: 1px; background: var(--linie); }
  .schritt { min-height: 0; padding: 56px 0 48px; }
  .schritt h2 { font-size: clamp(2.6rem, 13vw, 4rem); }
  .schritt__kopf { flex-direction: column; gap: 8px; }
  .schritt__text { grid-template-columns: 1fr; gap: 18px; }
  .abb { aspect-ratio: 4 / 3; }
  .abb figcaption span:last-child { display: none; }
  .schritt .punkt { position: absolute; left: -27px; top: 64px; margin: 0; }
  [data-variante='schiene'] .schritt .punkt { display: block; }
  [data-variante='unterstrich'] .schritt .strich { display: block; margin-top: 0; }
  [data-variante='linie'] .schritt .knoten { display: block; position: absolute; left: -30px; top: 70px; width: 9px; height: 9px; border-radius: 50%; background: var(--grund); border: 1px solid var(--stumm); }
  [data-variante='linie'] .schritt.erreicht .knoten { background: var(--signal); border-color: var(--signal); }
  [data-variante='linie'] .bahn b { display: none; }
  .modi > span { display: none; }
}
@media (hover: none) { .modi button:hover { color: var(--meta); border-color: var(--linie); } .modi button[aria-pressed='true'] { color: var(--grund); } .cta:hover { transform: none; } }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<header class="kopf">
  <p class="marke-wort">BRACHE<small>Digitalstudio · Zürich</small></p>
  <div class="modi mono" role="group" aria-label="Variante der Linie">
    <span>Variante</span>
    <button type="button" data-v="schiene" aria-pressed="true">Schiene</button>
    <button type="button" data-v="unterstrich" aria-pressed="false">Unterstrich</button>
    <button type="button" data-v="linie" aria-pressed="false">Linie</button>
  </div>
</header>

<section class="intro">
  <h1>Vier Etappen.<br><em>Ein Livegang.</em></h1>
  <div>
    <p class="mono">So arbeiten wir · 12 Wochen</p>
    <p>Wir bauen Websites für Museen, Verlage und Marken, die etwas zu sagen haben. Jedes Projekt läuft durch dieselben vier Etappen, in eurem Tempo, mit festen Übergaben.</p>
  </div>
</section>

<section class="prozess" id="prozess" data-variante="schiene" aria-labelledby="prozess-titel">
  <aside class="index">
    <div class="index__kopf">
      <p class="mono" id="prozess-titel">Ablauf</p>
      <p class="mono" aria-hidden="true"><span data-zaehler>00</span> / 04</p>
    </div>
    <ol class="index__liste">
      <li aria-hidden="true" class="schiene-spur"></li>
    </ol>
    <div class="werte mono" aria-label="Verwendete Werte">
      <p data-wert="schiene">Punkt <b>4px</b> → Pille <b>80px</b> in <b>400ms</b></p>
      <p data-wert="schiene">Füllung <b>scaleY linear</b> · Nachlauf <b>150ms</b></p>
      <p data-wert="unterstrich">Strich <b>174 × 1px</b> · Füllung <b>3px scaleX</b></p>
      <p data-wert="unterstrich">Wisch nach Abschluss <b>500ms</b> · Ursprung rechts</p>
      <p data-wert="linie">Linie <b>scaleY linear</b> · Füllung blendet bei <b>0–10 %</b> ein</p>
      <p><span data-bereich>cover 50vh → cover 100% − 50vh</span></p>
      <p>Fortschritt <b data-fortschritt>0 %</b></p>
    </div>
  </aside>

  <ol class="schritte">
    <li class="schritt" id="etappe-1">
      <span class="punkt" aria-hidden="true"><i></i></span><span class="knoten" aria-hidden="true"></span>
      <div class="schritt__kopf"><h2><span>01</span>Grabung</h2><p class="mono">Woche 1–2</p></div>
      <span class="strich" aria-hidden="true"><i></i></span>
      <figure class="abb"><svg viewBox="0 0 640 360" aria-hidden="true" data-abb="topo"></svg><figcaption class="mono"><span>Abb. 01 · Schichten</span><span>Workshop vor Ort</span></figcaption></figure>
      <div class="schritt__text">
        <p>Wir graben, bevor wir bauen. Zwei Workshops in eurem Haus, Gespräche mit fünf bis acht Leuten aus Publikum und Team, ein ehrlicher Blick auf Zahlen und Altlasten.</p>
        <ul class="liefer mono"><li>Zielbild auf einer Seite</li><li>Interview-Protokolle</li><li>Inhaltsinventar</li></ul>
      </div>
    </li>
    <li class="schritt" id="etappe-2">
      <span class="punkt" aria-hidden="true"><i></i></span><span class="knoten" aria-hidden="true"></span>
      <div class="schritt__kopf"><h2><span>02</span>Rohbau</h2><p class="mono">Woche 3–5</p></div>
      <span class="strich" aria-hidden="true"><i></i></span>
      <figure class="abb"><svg viewBox="0 0 640 360" aria-hidden="true">
        <g fill="none" stroke="rgba(255,255,255,.1)"><path d="M80 0V360M240 0V360M400 0V360M560 0V360M0 60H640M0 300H640" stroke-dasharray="2 6"/></g>
        <g fill="none" stroke="rgba(255,255,255,.55)">
          <rect x="80" y="60" width="480" height="240"/>
          <path d="M80 92H560"/><rect x="96" y="72" width="44" height="9"/><path d="M420 77H448M460 77H488M500 77H544"/>
          <rect x="96" y="112" width="270" height="118"/><path d="M96 112L366 230M366 112L96 230" stroke="rgba(255,255,255,.18)"/>
          <rect x="384" y="112" width="160" height="10"/><rect x="384" y="132" width="120" height="10"/>
          <rect x="96" y="246" width="140" height="38"/><rect x="250" y="246" width="140" height="38"/><rect x="404" y="246" width="140" height="38"/>
        </g>
        <g fill="rgba(255,255,255,.22)"><rect x="384" y="160" width="160" height="4"/><rect x="384" y="172" width="150" height="4"/><rect x="384" y="184" width="132" height="4"/><rect x="384" y="206" width="70" height="20" rx="10"/></g>
      </svg><figcaption class="mono"><span>Abb. 02 · Startseite, Graustufen</span><span>Prototyp v3</span></figcaption></figure>
      <div class="schritt__text">
        <p>Struktur vor Stil: Seitenbaum, Wireframes in Graustufen und ein klickbarer Prototyp. Hier fallen die großen Entscheidungen, solange sie noch günstig sind.</p>
        <ul class="liefer mono"><li>Seitenbaum</li><li>Wireframes</li><li>Klickbarer Prototyp</li></ul>
      </div>
    </li>
    <li class="schritt" id="etappe-3">
      <span class="punkt" aria-hidden="true"><i></i></span><span class="knoten" aria-hidden="true"></span>
      <div class="schritt__kopf"><h2><span>03</span>Ausbau</h2><p class="mono">Woche 6–11</p></div>
      <span class="strich" aria-hidden="true"><i></i></span>
      <figure class="abb"><svg viewBox="0 0 640 360" aria-hidden="true">
        <g stroke="rgba(255,255,255,.16)"><path d="M0 92H640M0 132H640M0 262H640"/></g>
        <text x="56" y="262" fill="#fff" font-family="TikTok Sans, sans-serif" font-weight="800" font-stretch="75%" font-size="236" letter-spacing="-7">Ag</text>
        <g font-family="DM Mono, monospace" font-size="11" fill="#8a8a8a" letter-spacing=".5"><text x="604" y="88" text-anchor="end">VERSAL</text><text x="604" y="128" text-anchor="end">X-HÖHE</text><text x="604" y="258" text-anchor="end">GRUNDLINIE</text></g>
        <g transform="translate(372 186)"><rect width="56" height="56" fill="#fff"/><rect x="64" width="56" height="56" fill="#0b0b0b" stroke="rgba(255,255,255,.4)"/><rect x="128" width="56" height="56" fill="#8a8a8a"/></g>
      </svg><figcaption class="mono"><span>Abb. 03 · Schnitt 75 / 800</span><span>Stand Woche 8</span></figcaption></figure>
      <div class="schritt__text">
        <p>Typo, Bewegung und Code wachsen gemeinsam. Jede Woche ein Stand im Browser statt Präsentationen; ihr testet auf euren eigenen Geräten.</p>
        <ul class="liefer mono"><li>Designsystem</li><li>Frontend und CMS</li><li>Redaktionsschulung</li></ul>
      </div>
    </li>
    <li class="schritt" id="etappe-4">
      <span class="punkt" aria-hidden="true"><i></i></span><span class="knoten" aria-hidden="true"></span>
      <div class="schritt__kopf"><h2><span>04</span>Eröffnung</h2><p class="mono">Woche 12</p></div>
      <span class="strich" aria-hidden="true"><i></i></span>
      <figure class="abb"><svg viewBox="0 0 640 360" aria-hidden="true">
        <g fill="none" stroke="rgba(255,255,255,.4)"><rect x="70" y="44" width="500" height="272" rx="6"/><path d="M70 74H570"/></g>
        <g fill="rgba(255,255,255,.4)"><circle cx="88" cy="59" r="4"/><circle cx="102" cy="59" r="4"/><circle cx="116" cy="59" r="4"/></g>
        <rect x="200" y="52" width="240" height="14" rx="7" fill="rgba(255,255,255,.08)"/>
        <text x="96" y="236" fill="#fff" font-family="TikTok Sans, sans-serif" font-weight="800" font-stretch="75%" font-size="150" letter-spacing="-5">Offen.</text>
        <g font-family="DM Mono, monospace" font-size="11" fill="#8a8a8a" letter-spacing=".5"><text x="98" y="288">200 OK · LCP 0,9 S · DIENSTAG 09:00</text></g>
      </svg><figcaption class="mono"><span>Abb. 04 · Livegang</span><span>Nie an einem Freitag</span></figcaption></figure>
      <div class="schritt__text">
        <p>Livegang an einem Dienstag, nie an einem Freitag. Danach sechs Wochen Feinschliff und ein Bericht, was die Zahlen nach dem Start sagen.</p>
        <ul class="liefer mono"><li>Livegang und Monitoring</li><li>Sechs Wochen Feinschliff</li><li>Bericht mit Zahlen</li></ul>
      </div>
    </li>
  </ol>
  <div class="bahn" aria-hidden="true"><i></i><em></em></div>
</section>

<section class="outro">
  <p>Nächstes freies Fenster: März 2027.</p>
  <a class="cta" href="#prozess">Projekt anfragen <span aria-hidden="true">→</span></a>
</section>
<footer class="fuss mono"><span>Brache GmbH · Konradstrasse 27 · 8005 Zürich</span><span>Fundus-Demo, erfundenes Studio</span></footer>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const prozess = document.getElementById('prozess');
  const liste = prozess.querySelector('.schritte');
  const schritte = [...liste.children];
  const indexListe = prozess.querySelector('.index__liste');
  const bahn = prozess.querySelector('.bahn');
  const zaehler = prozess.querySelector('[data-zaehler]');
  const anzeige = prozess.querySelector('[data-fortschritt]');
  const bereich = prozess.querySelector('[data-bereich]');
  const VARIANTEN = ['schiene', 'unterstrich', 'linie'];
  const BEREICH = { schiene: 'cover 50vh → cover 100% − 50vh', unterstrich: 'cover 50vh → cover 100% − 50vh', linie: 'offset start 10% → end 50%' };
  const clamp = v => Math.min(1, Math.max(0, v));

  // Abb. 01: Höhenlinien einer Grabung, einmal deterministisch erzeugt
  (() => {
    const svg = document.querySelector('[data-abb="topo"]');
    let d = '';
    for (let k = 0; k < 11; k++) {
      const r = 18 + k * 15, pts = [];
      for (let a = 0; a <= 64; a++) {
        const w = a / 64 * Math.PI * 2;
        const s = 1 + .15 * Math.sin(w * 3 + k * .16) + .07 * Math.sin(w * 5 - k * .28) + .05 * Math.cos(w * 2 + k * .2);
        pts.push((330 + Math.cos(w) * r * 1.45 * s).toFixed(1) + ' ' + (170 + Math.sin(w) * r * .82 * s).toFixed(1));
      }
      d += `<path d="M${pts.join('L')}Z" stroke="rgba(255,255,255,${(.6 - k * .045).toFixed(2)})"/>`;
    }
    svg.innerHTML = `<g fill="none" stroke-width="1">${d}</g><path d="M330 160v20M320 170h20M340 160L372 142" stroke="#fff"/><g font-family="DM Mono, monospace" font-size="11" fill="#8a8a8a" stroke="#141414" stroke-width="5" paint-order="stroke"><text x="378" y="146">FUNDSTELLE</text><text x="40" y="44">TIEFE 0–4</text></g>`;
  })();

  // Index-Liste aus den Etappen bauen (echte Buttons, Klick scrollt zur Etappe)
  const knoepfe = schritte.map((s, i) => {
    const li = document.createElement('li');
    const name = s.querySelector('h2').lastChild.textContent;
    const zeit = s.querySelector('.schritt__kopf .mono').textContent;
    li.innerHTML = `<button type="button" aria-controls="${s.id}"><span class="punkt" aria-hidden="true"><i></i></span><span class="mono nr">0${i + 1}</span><span><span class="name">${name}</span><span class="mono zeit">${zeit}<span class="jetzt">· Etappe läuft</span></span><span class="strich" aria-hidden="true"><i></i></span></span></button>`;
    li.querySelector('button').addEventListener('click', () => {
      pause();
      s.scrollIntoView({ behavior: reduziert() ? 'auto' : 'smooth', block: 'center' });
    });
    indexListe.appendChild(li);
    return li;
  });
  const knoten = schritte.map(() => bahn.appendChild(document.createElement('b')));

  let variante = 'schiene';
  function setzen(v) {
    variante = v;
    prozess.dataset.variante = v;
    bereich.textContent = BEREICH[v];
    document.querySelectorAll('[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
    messen();
  }
  document.querySelectorAll('[data-v]').forEach(b => b.addEventListener('click', () => setzen(b.dataset.v)));

  // Fortschritt messen: je Etappe wie Granola (cover 50vh → cover 100% − 50vh),
  // die durchgehende Linie wie Aceternity (start 10% → end 50%)
  function messen() {
    const vh = innerHeight, mitte = vh / 2;
    let aktiv = -1, summe = 0;
    const p = schritte.map(s => { const r = s.getBoundingClientRect(); return clamp((mitte - r.top) / r.height); });
    p.forEach((v, i) => {
      if (v > 0 && v < 1) aktiv = i;
      summe += v;
      const fertig = v >= 1;
      [schritte[i], knoepfe[i]].forEach(el => {
        el.style.setProperty('--p', v.toFixed(4));
        el.classList.toggle('ist-aktiv', i === aktiv);
        el.classList.toggle('ist-fertig', fertig);
      });
      knoepfe[i].querySelector('button').toggleAttribute('aria-current', i === aktiv);
      if (i === aktiv) knoepfe[i].querySelector('button').setAttribute('aria-current', 'step');
    });
    const lr = liste.getBoundingClientRect();
    const g = clamp((vh * .1 - lr.top) / (lr.height - vh * .4));
    const pr = prozess.getBoundingClientRect();
    const schmal = matchMedia('(max-width: 760px)').matches;
    bahn.style.left = (liste.offsetLeft + parseFloat(getComputedStyle(liste).paddingLeft) - (schmal ? 25 : 20)) + 'px';
    bahn.style.setProperty('--g', g.toFixed(4));
    bahn.style.setProperty('--go', clamp(g / .1).toFixed(3));   // Opazität der Füllung [0, 0.1] → [0, 1]
    const spitze = (lr.top - pr.top) + g * lr.height;
    schritte.forEach((s, i) => {
      const y = s.offsetTop + 70;
      knoten[i].style.top = (y - 4) + 'px';
      const an = spitze >= y;
      knoten[i].classList.toggle('an', an);
      s.classList.toggle('erreicht', an);
    });
    const gesamt = variante === 'linie' ? g : summe / schritte.length;
    anzeige.textContent = Math.round(gesamt * 100) + ' %';
    const nr = variante === 'linie' ? knoten.filter(k => k.classList.contains('an')).length : (aktiv >= 0 ? aktiv + 1 : p.filter(v => v >= 1).length);
    zaehler.textContent = String(nr).padStart(2, '0');
  }
  let geplant = false;
  const anfordern = () => { if (!geplant) { geplant = true; requestAnimationFrame(() => { geplant = false; messen(); }); } };
  addEventListener('scroll', anfordern, { passive: true });
  addEventListener('resize', anfordern);
  setzen(variante);

  // Vorführung: die Seite scrollt selbst durch den Ablauf, in der Galerie-Vorschau mit wechselnder Variante
  let uhr = null, t0 = 0, runde = 0;
  const DAUER = 11000;
  function vorfuehren() {
    pause();
    html.classList.add('vorfuehrung');
    const start = prozess.offsetTop;
    scrollTo(0, start);
    t0 = performance.now();
    const schritt = t => {
      const zeit = t - t0;
      if (zeit >= DAUER) {
        t0 = t; runde++;
        if (html.classList.contains('vorschau')) setzen(VARIANTEN[runde % VARIANTEN.length]);
        scrollTo(0, start);
      } else {
        const ende = prozess.offsetTop + prozess.offsetHeight - innerHeight;
        const q = clamp((zeit / DAUER - .05) / .82);
        scrollTo(0, start + (ende - start) * (.5 - .5 * Math.cos(q * Math.PI)));
      }
      messen();
      uhr = requestAnimationFrame(schritt);
    };
    uhr = requestAnimationFrame(schritt);
  }
  function pause() { cancelAnimationFrame(uhr); uhr = null; html.classList.remove('vorfuehrung'); }
  function neustart() { pause(); scrollTo(0, 0); messen(); }
  function weiter() { if (!uhr) vorfuehren(); }
  addEventListener('wheel', () => { if (uhr && !html.classList.contains('vorschau')) pause(); }, { passive: true });
  addEventListener('touchstart', () => { if (uhr && !html.classList.contains('vorschau')) pause(); }, { passive: true });

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