Zum Inhalt
← Glas & Material

Flüssiger Marmor-Hintergrund

Ein langsam fließender Marmor aus einem einzigen Shader trägt den Schrift-Hero, in Hausfarben statt Pastell-Aurora.

Grundvariante für Kreativstudio, Mode und Kultur: heller Grund, ein Band in der Signalfarbe, dünne dunkle Adern; wirkt über Struktur, nicht über Farbe.

Ein flächiger Hintergrund, der langsam fließt wie Marmor, Tinte in Wasser oder Öl auf einer Pfütze. Er wirkt wie Material, nicht wie Licht: Schlieren, Falten und dünne Adern statt weicher Farbwolken. Technisch ist es ein einziger Fragment-Shader mit Domain Warping nach Inigo Quilez: Ein Rauschmuster wird an Koordinaten ausgewertet, die selbst schon durch Rauschen verschoben sind, zweimal hintereinander. Die Farben kommen aus CSS-Variablen der Marke. Über dem Hintergrund liegt eine Kontrastschicht, damit fette Schrift darauf immer lesbar bleibt.

Das Auge liest gefaltete Schlieren als echtes Material mit Zähigkeit und Gewicht, nicht als Bildschirmeffekt. Weil sich die Fläche sehr langsam bewegt, wirkt die Seite lebendig, ohne vom Inhalt abzulenken. Eine harte Groteske davor gibt den Gegenpol: ruhige Schrift, bewegter Grund.

Wann einsetzen

Als Bühne für einen Schrift-Hero, eine Kampagne, einen Launch oder ein Festival-Key-Visual. Bei Kreativstudios und Kulturhäusern, wenn das Studio Bewegung als Haltung zeigen will. Für Mode und Beauty in Produktfarben (Launch-Seite, nicht im Shop-Raster). Für Eigenprojekte und Labore.

Wann nicht

Nicht in Pastell-Lila, Pink und Cyan als Standard: Das ist dasselbe Klischee wie der Aurora-Hintergrund (Leitfaden C2, Anti-Muster). Diese Farbigkeit gibt es hier nur als Experiment (Variante seifenhaut, Priorität F). Nicht hinter Fließtext, nicht zusammen mit einem Foto-Hero und nicht neben einem zweiten Material-Effekt (Glas, Chrom, Verlauf) in derselben Ansicht. Nicht für SaaS, Kanzlei, Praxis, Pflege, Handwerk und Vereine. Nicht ohne Standbild für schwache Geräte. Abgrenzung: mesh-gradient-shader macht weiche Farbwolken aus wandernden Zentren (Licht), webgl-fluid-cursor ist eine echte Strömungssimulation, die der Zeiger antreibt. Dieses Muster ist Material, läuft ohne Zeiger und kostet einen einzigen Shader-Durchlauf.

Varianten

  • marmor: heller Grund, ein Band in der Signalfarbe, dünne dunkle Adern (Grundvariante)
  • tinte: dunkler Grund, Signalfarbe als Tinte in Wasser, weiche helle Schleier
  • seifenhaut: irisierende Dünnschicht-Farben auf Creme (Experiment, Priorität F)
  • lava: warme Tropfen, die langsam verschmelzen und sich trennen (Smooth-Min-SDF)
  • bild: ein eigenes Foto wird mit demselben Warp-Feld verflüssigt

Umsetzung

Rohes WebGL 1, kein three.js: ein Dreieck über den ganzen Bildschirm, ein Fragment-Shader. Rauschen als Value Noise mit sinusfreiem Hash, fbm mit 4 Oktaven und Drehmatrix je Oktave. Warp nach Quilez: q = (fbm(p + 0.05t), fbm(p + (5.2, 1.3))), r = (fbm(p + k·q + (1.7, 9.2)), fbm(p + k·q + (8.3, 2.8))), f = fbm(p + k·r) mit k = 3 bis 3,5. Einfärben je Variante: marmor über Isolinien von f (Adern) und ein Band in r.x (Signalfarbe); tinte über f und |q|; seifenhaut über eine zyklische Rampe aus drei Farben entlang f + |r|; lava über Smooth-Min-SDF aus sechs Kreisen, deren Ränder q leicht verbiegt; bild verschiebt die Textur-Koordinaten um (r − 0,5). Farben liest das Skript aus CSS-Variablen (--fl-0 bis --fl-3). Koordinaten auf die Höhe normiert und zentriert, so passt das Standbild (object-fit: cover) zum ersten Bild des Shaders. Leistung: Gerätepixel auf 1 begrenzt, Renderfaktor 0,75, auf schwachen Geräten (≤ 4 Kerne, ≤ 4 GB, Touch oder Datensparen) und bei langsamen Bildern automatisch 0,5; höchstens 30 Bilder pro Sekunde. Pause per IntersectionObserver, visibilitychange und Pause-Knopf. Fallback: Standbild (WebP, aus demselben Shader-Code in Python gerendert) liegt immer unter dem Canvas; ohne WebGL oder bei Kontextverlust bleibt es stehen und driftet langsam per CSS; bei reduzierter Bewegung zeigt der Shader ein einziges Bild. Kontrastschicht: Verlauf in der Grundfarbe (Deckkraft 0,62) unter Kopfzeile und Text.

Robust umsetzen

Telefon. Gleiches Bild, auf Touch-Geräten mit halber Auflösung und 30 fps. Keine Zeiger-Reaktion. Der Pause-Knopf bleibt sichtbar und hat 44 px Trefferfläche. Das Wort im Hero passt sich der Breite an.

Ohne Bewegung. Kein Fließen: Der Shader zeichnet ein einziges Standbild (ohne WebGL das WebP-Standbild), keine Drift, keine Zeiger-Reaktion. Der Pause-Knopf entfällt, weil sich nichts bewegt.

Barrierefrei. Der Hintergrund ist Dekoration (aria-hidden, Canvas ohne Inhalt). Er bewegt sich länger als 5 Sekunden, darum gibt es einen sichtbaren Pause-Knopf „Bewegung anhalten“ mit aria-pressed (WCAG 2.2.2). Die Kontrastschicht hält Text auch über der hellsten und der dunkelsten Stelle bei mindestens 4,5 : 1 (WCAG 1.4.3); in der Demo am Standbild gemessen. Außerhalb des Bildschirms und im versteckten Tab steht der Shader. prefers-reduced-motion zeigt ein Standbild.

Code

<!DOCTYPE html>
<!--
  Flüssiger Marmor-Hintergrund
  Fundus · Glas & Material · https://cecodesign.de/lab/fundus/glas-material/fluessig-marmor-hintergrund/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="marmor">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flüssiger Marmor-Hintergrund · 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; }

/* Hausfarben Strudel (Rezept kreativstudio-buehne): Schwarz, Hellgrau, eine Signalfarbe */
:root {
  --schwarz: #0b0b0b; --hell: #f2f2f2; --weiss: #ffffff; --signal: #ff5a1f;
  --kurve: cubic-bezier(0.16, 1, 0.3, 1);   /* eine Kurve für alles Sichtbare */
  --ui: 300ms;
  --schleier-deckkraft: .62;                /* Kontrastschicht, siehe Werte */
  --rand: clamp(18px, 3vw, 40px);
}
/* Shader-Farben je Variante: Das Skript liest --fl-0 bis --fl-3 (Grund, dann drei Farben) */
html[data-v="marmor"], html[data-v="bild"] {
  --fl-0: #f2f2f2; --fl-1: #8a8a8a; --fl-2: #ff5a1f; --fl-3: #0b0b0b;
  --grund: var(--hell); --text: var(--schwarz); --schleier: 242, 242, 242;
}
html[data-v="tinte"] {
  --fl-0: #0b0b0b; --fl-1: #8a8a8a; --fl-2: #ff5a1f; --fl-3: #f2f2f2;
  --grund: var(--schwarz); --text: var(--weiss); --schleier: 11, 11, 11;
}
html[data-v="seifenhaut"] { /* Experiment (Priorität F): Pastell nur hier, nie als Standard */
  --fl-0: #f4f1ea; --fl-1: #7fe3b0; --fl-2: #c79bf2; --fl-3: #f4a6cf;
  --grund: #f4f1ea; --text: var(--schwarz); --schleier: 244, 241, 234;
}
html[data-v="lava"] {
  --fl-0: #0b0b0b; --fl-1: #ff5a1f; --fl-2: #ffc2a3; --fl-3: #7a1d05;
  --grund: var(--schwarz); --text: var(--weiss); --schleier: 11, 11, 11;
}

* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'TikTok Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { overflow-x: hidden; }
:root:has(:focus-visible) { scroll-behavior: auto; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }
.label { font: 500 .75rem/1.35 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ---------- Bühne ---------- */
.buehne { position: relative; isolation: isolate; overflow: hidden; min-height: 100svh; min-height: max(100svh, 600px);
  display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr auto auto; padding: var(--rand); color: var(--text); }
.grund { position: absolute; inset: 0; z-index: -1; overflow: hidden; background: var(--fl-0); }
.grund img, .grund canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.standbild { object-fit: cover; transform-origin: 50% 50%; }
html[data-v="bild"] .standbild { transform: scale(2.35); object-position: 50% 47%; }
.grund canvas { opacity: 0; transition: opacity .6s var(--kurve); }
.grund.gl-an canvas { opacity: 1; }
.grund.wechsel canvas { opacity: 0; transition-duration: .2s; }
/* Ohne WebGL: Standbild driftet sehr langsam (CSS), Pause hält es an */
.grund.ohne-gl .standbild { animation: drift 48s ease-in-out infinite alternate; }
html[data-v="bild"] .grund.ohne-gl .standbild { animation-name: drift-bild; }
.grund.ohne-gl canvas { display: none; }
@keyframes drift { from { transform: scale(1.04) translate(0, 0); } to { transform: scale(1.12) translate(-2.5%, 1.5%); } }
@keyframes drift-bild { from { transform: scale(2.35); } to { transform: scale(2.6) translate(-2%, 1%); } }
.buehne.steht .standbild { animation-play-state: paused; }
.reduziert .grund .standbild { animation: none; }

/* Kontrastschicht: Grundfarbe mit fester Deckkraft unter Kopfzeile und Textzone */
.schleier { position: absolute; inset: 0; pointer-events: none;
  --a: rgba(var(--schleier), var(--schleier-deckkraft)); --null: rgba(var(--schleier), 0);
  background:
    linear-gradient(to bottom, var(--a) 0, var(--a) var(--oben, 80px), var(--null) calc(var(--oben, 80px) + 72px)),
    linear-gradient(to top, var(--a) 0, var(--a) var(--unten, 45%), var(--null) calc(var(--unten, 45%) + 96px)); }

/* ---------- Kopfzeile ---------- */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 16px 24px; flex-wrap: wrap; }
.marke { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; font-weight: 800; font-stretch: 75%; font-size: 1.5rem; letter-spacing: -.02em; line-height: 1; min-height: 44px; }
.marke svg { width: 28px; height: 28px; flex: none; }
.nav ul { display: flex; gap: clamp(18px, 2.6vw, 36px); list-style: none; padding: 0; }
.nav a { text-decoration: none; font-size: 1rem; font-weight: 500; display: inline-block; padding: 10px 0; background: linear-gradient(currentColor, currentColor) 0 calc(100% - 6px) / 0 1.5px no-repeat; transition: background-size var(--ui) var(--kurve); }
.nav a:hover { background-size: 100% 1.5px; }
.cta { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 20px; border-radius: 999px; background: var(--signal); color: var(--schwarz); text-decoration: none; font-weight: 600; font-size: .95rem; transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
.cta:hover { background: var(--text); color: var(--grund); }
.cta svg { width: 14px; height: 14px; }

/* ---------- Text über dem Material ---------- */
.held { grid-row: 3; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 20px clamp(24px, 5vw, 80px); align-items: end; padding-bottom: clamp(14px, 2.4vh, 28px); border-bottom: 1.5px solid currentColor; }
.held__links { display: flex; flex-direction: column; align-items: flex-start; gap: 16px; }
.held__satz { font-size: clamp(1.125rem, 1.6vw, 1.5rem); line-height: 1.3; font-weight: 500; letter-spacing: -.01em; max-width: 34ch; }
.held__fuss { display: flex; align-items: center; gap: 10px 22px; flex-wrap: wrap; margin-top: 16px; }
.textlink { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; font-weight: 600; text-decoration: none; background: linear-gradient(currentColor, currentColor) 0 calc(100% - 8px) / 100% 1.5px no-repeat; transition: background-size var(--ui) var(--kurve); }
.textlink:hover { background-size: 0 1.5px; background-position: 100% calc(100% - 8px); }
.textlink svg { width: 14px; height: 14px; }

/* Pause-Knopf (WCAG 2.2.2): fester Name, Zustand über aria-pressed */
.pause { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 16px 0 14px; border: 1.5px solid currentColor; border-radius: 999px; background: rgba(var(--schleier), .5); color: inherit; cursor: pointer;
  font: 500 .75rem/1 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
.pause:hover { background: var(--text); color: var(--grund); }
.pause svg { width: 12px; height: 12px; flex: none; }
.pause .spiel { display: none; }
.pause[aria-pressed="true"] .halt { display: none; }
.pause[aria-pressed="true"] .spiel { display: block; }
.pause[hidden] { display: none; }

/* Wortmarke über die volle Breite (Fit-to-Width per Skript) */
.wort { grid-row: 4; margin-top: clamp(6px, 1.2vh, 14px); font-weight: 800; font-stretch: 75%; letter-spacing: -.03em; line-height: .8; white-space: nowrap; font-size: clamp(5rem, 26vw, 24rem); }
.wort [data-fit] { display: inline-block; padding-bottom: .04em; }

/* ---------- Folgesektion (zeigt: Shader pausiert außer Sicht) ---------- */
.arbeiten { background: var(--text); color: var(--grund); padding: clamp(56px, 10vh, 120px) var(--rand) clamp(72px, 12vh, 140px); }
.arbeiten__kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; margin-bottom: clamp(28px, 5vh, 56px); }
.arbeiten h2 { font-weight: 600; font-size: clamp(2rem, 4.5vw, 4.5rem); line-height: .95; letter-spacing: -.02em; }
.liste { list-style: none; padding: 0; border-top: 1.5px solid currentColor; }
.liste a { display: grid; grid-template-columns: 3.5rem minmax(0, 1.4fr) minmax(0, 1fr) auto; gap: 8px 20px; align-items: baseline; padding: 22px 0; border-bottom: 1px solid color-mix(in srgb, currentColor 30%, transparent); text-decoration: none; transition: padding var(--ui) var(--kurve); }
.liste a:hover { padding-left: 12px; }
.liste a:hover .liste__titel { color: var(--signal); }
.liste__titel { font-size: clamp(1.4rem, 2.6vw, 2.4rem); font-weight: 700; font-stretch: 85%; letter-spacing: -.02em; line-height: 1; transition: color var(--ui) var(--kurve); }
.liste .label { opacity: 1; }

/* ---------- Pult (Demo-Bedienung) ---------- */
.pult { position: fixed; left: 14px; bottom: 14px; z-index: 20; max-width: min(calc(100% - 28px), 720px); padding: 12px 14px; border-radius: 14px; background: rgba(11, 11, 11, .88); color: #f2f2f2; box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
  font: 400 11px/1.4 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; display: grid; gap: 10px; }
.pult__reihe { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pult button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: #f2f2f2; background: transparent; border: 1px solid rgba(242, 242, 242, .3); border-radius: 999px; min-height: 30px; padding: 0 11px; cursor: pointer; transition: background-color 150ms var(--kurve), color 150ms var(--kurve); }
.pult button:hover { background: rgba(242, 242, 242, .12); }
.pult button[aria-pressed="true"] { background: #f2f2f2; color: #0b0b0b; border-color: #f2f2f2; }
.pult button:disabled { opacity: .45; cursor: not-allowed; }
.pult__trenner { width: 1px; height: 18px; background: rgba(242, 242, 242, .3); margin: 0 4px; }
.werte { display: flex; flex-wrap: wrap; gap: 2px 14px; color: rgba(242, 242, 242, .78); }
.werte b { color: #fff; font-weight: 500; }
.werte .an { color: #ffb08f; }

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

@media (max-width: 860px) {
  .nav { display: none; }
  .held { grid-template-columns: minmax(0, 1fr); }
  .held__links { flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: space-between; width: 100%; }
}
@media (max-width: 640px) {
  .liste a { grid-template-columns: 2.4rem minmax(0, 1fr); }
  .liste a > :nth-child(3), .liste a > :nth-child(4) { grid-column: 2; }
  .cta { padding: 0 16px; }
}
@media (max-width: 360px) {
  .cta span { display: none; }
  .cta { width: 44px; justify-content: center; padding: 0; }
  .pause { padding: 0 12px; }
}
@media (hover: none) {
  .nav a, .cta, .textlink, .pause, .liste a, .liste__titel { transition: none; }
  .liste a:hover { padding-left: 0; }
}
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
/* Die Wortmarke wird per Skript vermessen: nie über eine Transition, sonst misst fit() die alte Größe */
:root .buehne .wort, :root .buehne .wort * { transition: none !important; }
.reduziert .grund canvas { transition: opacity .2s linear !important; }
/* Windows-Kontrastmodus: Bedienelemente brauchen eine Kante (!important, sonst gewinnen Regeln wie .x button { border: 0 }) */
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
  .grund { display: none; }
  .cta { border: 1px solid ButtonText; }
}
</style>
</head>
<body>
<section class="buehne" data-buehne aria-labelledby="titel">
  <div class="grund" data-grund aria-hidden="true">
    <img class="standbild" data-standbild src="bilder/fluessig-marmor-hintergrund-marmor.webp" alt="" width="1280" height="800" fetchpriority="high" decoding="async">
    <canvas data-canvas></canvas>
    <div class="schleier"></div>
  </div>

  <header class="kopf" data-kopf>
    <a class="marke" href="#" aria-label="Strudel, Startseite">
      <svg viewBox="0 0 32 32" fill="none" aria-hidden="true"><path d="M16 16.2c0-1.3 1.6-1.9 2.6-1 1.6 1.4.4 4.3-1.8 4.6-3.1.4-5.2-2.8-4.2-5.6 1.3-3.8 6.2-4.9 9.2-2.5 3.8 3 3 9.2-1.2 11.6-5 2.8-11.5.2-12.9-5.4C6.1 10.6 10.8 4.4 17 4.1c4.6-.2 8.7 2.4 10.4 6.3" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/></svg>
      Strudel
    </a>
    <nav class="nav" aria-label="Hauptmenü">
      <ul>
        <li><a href="#arbeiten">Arbeiten</a></li>
        <li><a href="#">Studio</a></li>
        <li><a href="#">Journal</a></li>
      </ul>
    </nav>
    <a class="cta" href="#"><span>Projekt anfragen</span><svg viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M2 12 12 2M4.5 2H12v7.5" stroke="currentColor" stroke-width="1.8"/></svg></a>
  </header>

  <h1 class="wort" id="titel"><span data-fit>Strudel</span><span class="sr">: Studio für Marke, Bewegtbild und Web in Dessau</span></h1>

  <div class="held" data-held>
    <div class="held__links">
      <p class="label">Studio für Marke, Bewegtbild und Web · Dessau-Roßlau</p>
      <button class="pause" type="button" data-pause aria-pressed="false">
        <svg class="halt" viewBox="0 0 12 12" aria-hidden="true"><path d="M2 1h3v10H2zM7 1h3v10H7z" fill="currentColor"/></svg>
        <svg class="spiel" viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 1 11 6l-8.5 5z" fill="currentColor"/></svg>
        Bewegung anhalten
      </button>
    </div>
    <div>
      <p class="held__satz">Wir gestalten Marken, die sich bewegen dürfen, und bauen Websites, die trotzdem in einer Sekunde stehen.</p>
      <div class="held__fuss">
        <a class="textlink" href="#arbeiten">Ausgewählte Arbeiten <svg viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M7 1v11M2 7.5 7 12.5l5-5" stroke="currentColor" stroke-width="1.8"/></svg></a>
        <span class="label">Seit 2014 · 9 Leute · Anhalt und überall</span>
      </div>
    </div>
  </div>
</section>

<section class="arbeiten" id="arbeiten" aria-labelledby="arbeiten-titel">
  <div class="arbeiten__kopf">
    <h2 id="arbeiten-titel">Ausgewählte Arbeiten</h2>
    <p class="label">2024 bis 2026</p>
  </div>
  <ul class="liste">
    <li><a href="#"><span class="label">01</span><span class="liste__titel">Muldeufer Festival</span><span class="label">Key Visual in Bewegung</span><span class="label">2026</span></a></li>
    <li><a href="#"><span class="label">02</span><span class="liste__titel">Kanalbad Roßlau</span><span class="label">Identität und Leitsystem</span><span class="label">2025</span></a></li>
    <li><a href="#"><span class="label">03</span><span class="liste__titel">Verlag Kiefernhaus</span><span class="label">Website und Programmvorschau</span><span class="label">2025</span></a></li>
    <li><a href="#"><span class="label">04</span><span class="liste__titel">Elbwiesen Lichtspiele</span><span class="label">Trailer-Paket und Plakate</span><span class="label">2024</span></a></li>
  </ul>
</section>

<div class="pult">
  <div class="pult__reihe" role="group" aria-label="Variante">
    <button type="button" data-v="marmor" aria-pressed="true">Marmor</button>
    <button type="button" data-v="tinte" aria-pressed="false">Tinte</button>
    <button type="button" data-v="seifenhaut" aria-pressed="false">Seifenhaut · Experiment</button>
    <button type="button" data-v="lava" aria-pressed="false">Lava</button>
    <button type="button" data-v="bild" aria-pressed="false">Bild</button>
    <span class="pult__trenner" aria-hidden="true"></span>
    <button type="button" data-fallback aria-pressed="false">Standbild ohne WebGL</button>
  </div>
  <p class="werte" data-werte aria-live="polite"></p>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const buehne = document.querySelector('[data-buehne]');
  const grund = document.querySelector('[data-grund]');
  const canvas = document.querySelector('[data-canvas]');
  const standbild = document.querySelector('[data-standbild]');
  const pauseKnopf = document.querySelector('[data-pause]');
  const werteEl = document.querySelector('[data-werte]');
  const MEDIEN = 'bilder/';

  /* Varianten: Parameter wie Paper Warp (Skala, Warp-Stärke, Weichheit), Zeit in Einheiten pro Sekunde */
  const V = {
    marmor:     { modus: 0, skala: 0.9, warp: 3.0, weich: 0.006, tempo: 0.12, poster: 'fluessig-marmor-hintergrund-marmor.webp', kontrast: '6,9' },
    tinte:      { modus: 1, skala: 1.0, warp: 3.0, weich: 0.04,  tempo: 0.12, poster: 'fluessig-marmor-hintergrund-tinte.webp', kontrast: '11,5' },
    seifenhaut: { modus: 2, skala: 1.0, warp: 3.5, weich: 0.1,   tempo: 0.12, poster: 'fluessig-marmor-hintergrund-seifenhaut.webp', kontrast: '13,7' },
    lava:       { modus: 3, skala: 1.6, warp: 2.0, weich: 0.1,   tempo: 0.5,  poster: 'fluessig-marmor-hintergrund-lava.webp', kontrast: '9,2' },
    bild:       { modus: 4, skala: 0.9, warp: 3.0, weich: 0.18,  tempo: 0.12, poster: 'stoff-stapel.jpg', kontrast: '6,6', bild: 'stoff-stapel.jpg' },
  };
  const T0 = 7.0;              // Startzeit: dieses Bild zeigt auch das Standbild
  const FPS_DECKEL = 30;
  const schwach = (navigator.hardwareConcurrency || 8) <= 4 || (navigator.deviceMemory || 8) <= 4
    || matchMedia('(hover: none)').matches || !!(navigator.connection && navigator.connection.saveData);
  let faktor = schwach ? 0.5 : 0.75;   // Renderauflösung relativ zu CSS-Pixeln (Gerätepixel auf 1 begrenzt)

  /* ---------- Shader ---------- */
  const VS = 'attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }';
  const FS = `
precision highp float;
uniform vec2 u_res;
uniform float u_t, u_modus, u_skala, u_warp, u_weich;
uniform vec3 u_c0, u_c1, u_c2, u_c3;
uniform vec3 u_zeiger;          // xy wie c (auf die Höhe normiert), z = Stärke 0..1
uniform sampler2D u_bild;
uniform vec4 u_bildfeld;        // Mitte xy, Zoom, Seitenverhältnis des Bildes

// Hash ohne Sinus (stabil auf allen GPUs), Value Noise, fbm mit 4 Oktaven
float hash(vec2 p){ vec3 p3 = fract(vec3(p.xyx) * .1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
float noise(vec2 p){
  vec2 i = floor(p), f = fract(p), u = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
}
const mat2 R = mat2(1.6, 1.2, -1.2, 1.6);
float fbm(vec2 p){ float f = 0.0, a = 0.5; for (int i = 0; i < 4; i++){ f += a * noise(p); p = R * p; a *= 0.5; } return f / 0.9375; }
// smoothstep, das auch fallende Kanten erlaubt (a > b)
float stufe(float a, float b, float x){ float t = clamp((x - a) / (b - a), 0.0, 1.0); return t * t * (3.0 - 2.0 * t); }

void main(){
  vec2 c = (gl_FragCoord.xy - 0.5 * u_res) / u_res.y;
  // Zeiger: sanfter Wirbel um die Zeigerposition
  vec2 d = c - u_zeiger.xy;
  float w = u_zeiger.z * exp(-dot(d, d) * 9.0) * 1.1;
  c = u_zeiger.xy + mat2(cos(w), sin(w), -sin(w), cos(w)) * d;

  // Domain Warping nach Quilez: f = fbm(p + k*r), r = fbm(p + k*q), q = fbm(p)
  float t = u_t, k = u_warp, s = u_weich;
  vec2 p = c * u_skala;
  vec2 q = vec2(fbm(p + vec2(0.05, 0.015) * t), fbm(p + vec2(5.2 - 0.04 * t, 1.3)));
  vec2 r = vec2(fbm(p + k * q + vec2(1.7 + 0.075 * t, 9.2)), fbm(p + k * q + vec2(8.3, 2.8 + 0.063 * t)));
  float f = fbm(p + k * r);
  float ql = length(q);
  vec3 col = u_c0;

  if (u_modus < 0.5) {            // marmor: Band in r.x (Signalfarbe), Adern als Isolinien von f
    col = mix(col, u_c1, stufe(0.45, 0.85, f) * stufe(0.5, 1.1, ql) * 0.35);
    float band = stufe(0.05 + s * 1.5, 0.05 - s * 1.5, abs(r.x - 0.56));
    col = mix(col, u_c2, band);
    float ader = 1.0 - stufe(0.0, 0.004 + s * 0.4 + 0.006 * stufe(0.6, 1.0, ql), abs(f - 0.50));
    col = mix(col, u_c3, ader * 0.92);
    float ader2 = 1.0 - stufe(0.0, 0.003 + s * 0.3, abs(f - 0.64));
    col = mix(col, u_c1, ader2 * 0.7);
  } else if (u_modus < 1.5) {     // tinte: Signalfarbe als Tinte, helle Schleier an den Kanten
    float dichte = stufe(0.52, 0.80, f) * stufe(0.62, 1.05, ql);
    col = mix(col, u_c2, dichte);
    col = mix(col, u_c1, stufe(0.58, 0.80, r.y) * 0.22 * (1.0 - dichte));
    float kante = (1.0 - stufe(0.0, 0.012 + s, abs(f - 0.62))) * 0.30 * stufe(0.5, 0.9, ql);
    col = mix(col, u_c3, kante);
  } else if (u_modus < 2.5) {     // seifenhaut: zyklische Dünnschicht-Rampe aus drei Farben
    float x = f * 1.6 + length(r) * 0.9;
    vec3 wv = pow(0.5 + 0.5 * cos(6.2832 * (x - vec3(0.0, 1.0 / 3.0, 2.0 / 3.0))), vec3(3.0));
    vec3 film = (u_c1 * wv.x + u_c2 * wv.y + u_c3 * wv.z) / (wv.x + wv.y + wv.z);
    col = mix(col, film, stufe(0.44, 0.60, f) * 0.92);
    col = mix(col, vec3(1.0), (1.0 - stufe(0.0, 0.02, abs(f - 0.70))) * 0.5);
  } else if (u_modus < 3.5) {     // lava: Smooth-Min aus sechs Kreisen, Ränder leicht verbogen
    vec2 pw = c + (q - 0.5) * 0.22;
    float asp = u_res.x / u_res.y, sdf = 1e3, kk = 0.38;
    for (int i = 0; i < 6; i++) {
      float fi = float(i);
      vec2 m = vec2((fi / 5.0 - 0.5) * 0.62 * asp + 0.12 * sin(t * 0.21 + fi * 2.1), 0.40 * sin(t * (0.11 + 0.027 * fi) + fi * 1.9));
      float dd = length(pw - m) - (0.11 + 0.05 * sin(fi * 3.1 + 1.0));
      float h = clamp(0.5 + 0.5 * (dd - sdf) / kk, 0.0, 1.0);
      sdf = mix(dd, sdf, h) - kk * h * (1.0 - h);
    }
    float dz = length(pw - u_zeiger.xy) - 0.07 * u_zeiger.z;      // Zeiger als kleiner Tropfen
    float hz = clamp(0.5 + 0.5 * (dz - sdf) / kk, 0.0, 1.0);
    sdf = mix(sdf, mix(dz, sdf, hz) - kk * hz * (1.0 - hz), step(0.01, u_zeiger.z));
    col = mix(col, u_c3, exp(-max(sdf, 0.0) * 6.0) * 0.38 * (0.6 - c.y * 0.6));
    vec3 koerper = mix(u_c3, u_c1, stufe(0.0, -0.06, sdf));
    koerper = mix(koerper, u_c2, stufe(-0.05, -0.24, sdf) * (0.35 + 0.65 * stufe(0.40, 0.75, f)));
    col = mix(col, koerper, stufe(0.003, -0.003, sdf));
  } else {                        // bild: dasselbe Warp-Feld verschiebt die Textur-Koordinaten
    vec2 uv = gl_FragCoord.xy / u_res;
    float asp = u_res.x / u_res.y, ia = u_bildfeld.w;
    vec2 sc = asp > ia ? vec2(1.0, ia / asp) : vec2(asp / ia, 1.0);
    vec2 b = (uv - 0.5) * sc * u_bildfeld.z + u_bildfeld.xy + (r - 0.5) * s;
    col = texture2D(u_bild, clamp(b, 0.002, 0.998)).rgb;
  }
  col += (hash(gl_FragCoord.xy + 17.0) - 0.5) / 255.0;   // Dither gegen Treppen im Verlauf
  gl_FragColor = vec4(col, 1.0);
}`;

  let gl = null, U = {}, textur = null, bildBereit = false;
  function aufsetzen() {
    try {
      gl = canvas.getContext('webgl', { antialias: false, alpha: false, depth: false, stencil: false, preserveDrawingBuffer: false, powerPreference: 'low-power' });
      if (!gl) return false;
      const sh = (typ, src) => { const o = gl.createShader(typ); gl.shaderSource(o, src); gl.compileShader(o);
        if (!gl.getShaderParameter(o, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(o)); return o; };
      const prog = gl.createProgram();
      gl.attachShader(prog, sh(gl.VERTEX_SHADER, VS)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, FS));
      gl.linkProgram(prog);
      if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));
      gl.useProgram(prog);
      gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);   // ein Dreieck deckt alles
      const loc = gl.getAttribLocation(prog, 'a'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
      ['u_res', 'u_t', 'u_modus', 'u_skala', 'u_warp', 'u_weich', 'u_c0', 'u_c1', 'u_c2', 'u_c3', 'u_zeiger', 'u_bild', 'u_bildfeld']
        .forEach(n => U[n] = gl.getUniformLocation(prog, n));
      textur = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, textur);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([200, 190, 180]));
      ['TEXTURE_WRAP_S', 'TEXTURE_WRAP_T'].forEach(n => gl.texParameteri(gl.TEXTURE_2D, gl[n], gl.CLAMP_TO_EDGE));
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.uniform1i(U.u_bild, 0);
      return true;
    } catch (e) { gl = null; return false; }
  }
  function bildLaden() {
    const im = new Image();
    im.onload = () => {
      if (!gl) return;
      try {
        gl.bindTexture(gl.TEXTURE_2D, textur);
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, im);
        bildBereit = true; bildfeld = [0.5, 0.53, 0.42, im.naturalWidth / im.naturalHeight];
        if (variante === 'bild') { zustandPruefen(); zeichnen(); }
      } catch (e) { bildBereit = false; }   // z. B. file:// ohne Server: Standbild bleibt
    };
    im.src = MEDIEN + V.bild.bild;
  }
  let bildfeld = [0.5, 0.53, 0.42, 1.5];

  function farbe(name) {
    const h = getComputedStyle(html).getPropertyValue(name).trim().replace('#', '');
    return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16) / 255);
  }

  /* ---------- Zustand ---------- */
  const url = new URLSearchParams(location.search);
  let variante = V[url.get('v')] ? url.get('v') : 'marmor';
  let zeit = T0, rafId = 0, letzte = 0, letzteZeichnung = 0;
  let angehalten = false, imBild = true, tabDa = !document.hidden, vorfuehrung = false, erzwungenOhneGl = false;
  const zeiger = { x: 0, y: 0, z: 0, zx: 0, zy: 0, zz: 0, ruhe: 0 };
  const dauer = [];
  let glDa = aufsetzen();
  if (glDa) bildLaden();

  function glAktiv() { return glDa && !erzwungenOhneGl && (variante !== 'bild' || bildBereit); }
  function groesse() {
    const r = canvas.getBoundingClientRect();
    const f = Math.min(devicePixelRatio || 1, 1) * faktor;
    const w = Math.max(1, Math.round(r.width * f)), h = Math.max(1, Math.round(r.height * f));
    if (canvas.width !== w || canvas.height !== h) { canvas.width = w; canvas.height = h; gl.viewport(0, 0, w, h); }
  }
  function zeichnen() {
    if (!glAktiv()) return;
    const v = V[variante];
    groesse();
    gl.uniform2f(U.u_res, canvas.width, canvas.height);
    gl.uniform1f(U.u_t, zeit);
    gl.uniform1f(U.u_modus, v.modus); gl.uniform1f(U.u_skala, v.skala); gl.uniform1f(U.u_warp, v.warp); gl.uniform1f(U.u_weich, v.weich);
    gl.uniform3fv(U.u_c0, farbe('--fl-0')); gl.uniform3fv(U.u_c1, farbe('--fl-1')); gl.uniform3fv(U.u_c2, farbe('--fl-2')); gl.uniform3fv(U.u_c3, farbe('--fl-3'));
    gl.uniform3f(U.u_zeiger, zeiger.x, zeiger.y, zeiger.z);
    gl.uniform4f(U.u_bildfeld, bildfeld[0], bildfeld[1], bildfeld[2], bildfeld[3]);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
    grund.classList.add('gl-an');
  }

  function schritt(t) {
    rafId = 0;
    if (!darfLaufen()) return;
    rafId = requestAnimationFrame(schritt);
    if (letzteZeichnung && t - letzteZeichnung < 1000 / FPS_DECKEL - 2) return;   // 30-fps-Deckel
    const dt = letzte ? Math.min(0.1, (t - letzte) / 1000) : 0;
    letzte = t;
    if (letzteZeichnung) {                                    // langsame Geräte: auf halbe Auflösung
      dauer.push(t - letzteZeichnung); if (dauer.length > 30) dauer.shift();
      if (faktor > 0.5 && dauer.length === 30 && dauer.reduce((a, b) => a + b) / 30 > 55) { faktor = 0.5; werteZeigen(); }
    }
    letzteZeichnung = t;
    zeit += dt * V[variante].tempo * (vorfuehrung ? 3 : 1);
    zeigerSchritt(t);
    zeichnen();
  }
  function darfLaufen() { return glAktiv() && !angehalten && imBild && tabDa && !reduziert(); }
  function zustandPruefen() {
    const gl_ = glAktiv();
    grund.classList.toggle('ohne-gl', !gl_);
    if (!gl_) grund.classList.remove('gl-an');
    buehne.classList.toggle('steht', angehalten || !imBild || !tabDa);
    pauseKnopf.hidden = reduziert();
    if (darfLaufen()) { if (!rafId) { letzte = 0; letzteZeichnung = 0; rafId = requestAnimationFrame(schritt); } }
    else { cancelAnimationFrame(rafId); rafId = 0; if (gl_) zeichnen(); }
    werteZeigen();
  }

  /* Außerhalb des Viewports und im versteckten Tab steht der Shader */
  new IntersectionObserver(e => { imBild = e[0].isIntersecting; zustandPruefen(); }, { threshold: 0 }).observe(buehne);
  document.addEventListener('visibilitychange', () => { tabDa = !document.hidden; zustandPruefen(); });
  new ResizeObserver(() => { schleierMessen(); fit(); if (!rafId) zeichnen(); }).observe(buehne);
  canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); glDa = false; zustandPruefen(); });
  canvas.addEventListener('webglcontextrestored', () => { glDa = aufsetzen(); if (glDa) bildLaden(); zustandPruefen(); });
  new MutationObserver(zustandPruefen).observe(html, { attributes: true, attributeFilter: ['class'] });

  /* Zeiger: sanfter Wirbel, nur mit Maus und ohne reduzierte Bewegung */
  const feinZeiger = matchMedia('(hover: hover) and (pointer: fine)').matches;
  function zuC(clientX, clientY) {
    const r = buehne.getBoundingClientRect();
    return [(clientX - r.left - r.width / 2) / r.height, (r.height / 2 - (clientY - r.top)) / r.height];
  }
  if (feinZeiger) {
    buehne.addEventListener('pointermove', e => {
      if (vorfuehrung || e.pointerType !== 'mouse') return;
      [zeiger.zx, zeiger.zy] = zuC(e.clientX, e.clientY); zeiger.zz = 0.9; zeiger.ruhe = performance.now();
    });
    buehne.addEventListener('pointerleave', () => { if (!vorfuehrung) zeiger.zz = 0; });
  }
  function zeigerSchritt(t) {
    if (vorfuehrung) {           // gedachter Zeiger kreist langsam
      const a = t / 1000 * 0.45;
      zeiger.zx = 0.18 + Math.cos(a) * 0.32; zeiger.zy = 0.08 + Math.sin(a * 1.3) * 0.18; zeiger.zz = 0.9;
    } else if (t - zeiger.ruhe > 1200) zeiger.zz = 0;            // Wirbel klingt ab, wenn die Maus ruht
    zeiger.x += (zeiger.zx - zeiger.x) * 0.06; zeiger.y += (zeiger.zy - zeiger.y) * 0.06; zeiger.z += (zeiger.zz - zeiger.z) * 0.04;
  }

  /* ---------- Kontrastschicht und Wortmarke an das Layout anpassen ---------- */
  const kopf = document.querySelector('[data-kopf]'), held = document.querySelector('[data-held]');
  function schleierMessen() {
    const b = buehne.getBoundingClientRect(), k = kopf.getBoundingClientRect(), h = held.getBoundingClientRect();
    buehne.style.setProperty('--oben', Math.round(k.bottom - b.top + 8) + 'px');
    buehne.style.setProperty('--unten', Math.round(b.bottom - h.top + 18) + 'px'); werteZeigen();
  }
  const wort = document.querySelector('.wort'), fitEl = document.querySelector('[data-fit]');
  function fit() {
    const breite = buehne.clientWidth - 2 * parseFloat(getComputedStyle(buehne).paddingLeft);
    wort.style.fontSize = '100px';
    const w = fitEl.getBoundingClientRect().width || 1;
    const maxH = buehne.clientHeight * 0.44 / 0.8;               // höchstens 44 % der Bühnenhöhe
    wort.style.fontSize = Math.floor(Math.min(100 * breite / w, maxH)) + 'px';
    schleierMessen();
  }
  (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => { fit(); zeichnen(); });

  /* ---------- Varianten (Regel 13) ---------- */
  const knoepfe = [...document.querySelectorAll('.pult [data-v]')];
  const fallbackKnopf = document.querySelector('[data-fallback]');
  let wechselUhr = 0;
  function setzen(v, sanft) {
    variante = V[v] ? v : 'marmor';
    const tauschen = () => {
      html.setAttribute('data-v', variante);
      standbild.src = MEDIEN + V[variante].poster;
      knoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === variante)));
      const q = new URLSearchParams(location.search); q.set('v', variante);
      history.replaceState(null, '', location.pathname + '?' + q.toString() + location.hash);
      zustandPruefen(); zeichnen();
      grund.classList.remove('wechsel');
    };
    clearTimeout(wechselUhr);
    if (sanft && glAktiv() && !reduziert()) { grund.classList.add('wechsel'); wechselUhr = setTimeout(tauschen, 200); }
    else tauschen();
  }
  knoepfe.forEach(b => b.addEventListener('click', () => setzen(b.dataset.v, true)));
  fallbackKnopf.addEventListener('click', () => {
    erzwungenOhneGl = !erzwungenOhneGl;
    fallbackKnopf.setAttribute('aria-pressed', String(erzwungenOhneGl));
    zustandPruefen();
  });
  if (!glDa) { fallbackKnopf.disabled = true; fallbackKnopf.title = 'WebGL ist hier nicht verfügbar, das Standbild ist schon aktiv'; }
  // Standbilder der anderen Varianten im Leerlauf vorladen
  addEventListener('load', () => setTimeout(() => Object.values(V).forEach(x => { const i = new Image(); i.src = MEDIEN + x.poster; }), 1500));

  /* ---------- Werte-Anzeige ---------- */
  const zahl = n => String(n).replace('.', ',');
  function status() {
    if (reduziert()) return 'Standbild (reduzierte Bewegung)';
    if (!glAktiv()) return erzwungenOhneGl ? 'Standbild, CSS-Drift' : (glDa ? 'Standbild (Bild lädt)' : 'Standbild (kein WebGL)');
    if (angehalten) return 'angehalten';
    if (!tabDa) return 'pausiert (Tab versteckt)';
    if (!imBild) return 'pausiert (außer Sicht)';
    return 'läuft';
  }
  function werteZeigen() {
    const v = V[variante], st = status();
    werteEl.innerHTML = [
      ['Status', st, st === 'läuft'],
      ['Warp', zahl(v.warp)], ['Skala', zahl(v.skala)], [v.modus === 4 ? 'Versatz' : 'Weich', zahl(v.weich)],
      ['Zeit', zahl(v.tempo) + '/s'], ['Auflösung', Math.round(faktor * 100) + ' %'], ['fps', '≤ ' + FPS_DECKEL],
      ['Schleier', zahl(parseFloat(getComputedStyle(html).getPropertyValue('--schleier-deckkraft')))], ['Kontrast', '≥ ' + v.kontrast + ' : 1'],
    ].map(([k, w, an]) => `<span>${k} <b${an ? ' class="an"' : ''}>${w}</b></span>`).join('');
  }

  /* ---------- Pause-Knopf und API ---------- */
  function pause() {
    angehalten = true; vorfuehrung = false; html.classList.remove('vorfuehrung'); zeiger.zz = 0;
    pauseKnopf.setAttribute('aria-pressed', 'true');
    zustandPruefen();
  }
  function weiterLaufen() {
    angehalten = false;
    pauseKnopf.setAttribute('aria-pressed', 'false');
    zustandPruefen();
  }
  pauseKnopf.addEventListener('click', () => angehalten ? weiterLaufen() : pause());
  function neustart() { zeit = T0; zeiger.x = zeiger.y = zeiger.z = zeiger.zz = 0; weiterLaufen(); zeichnen(); }
  function vorfuehren() {
    // Zeigt die aktuelle Variante: Fließen dreifach schneller, ein gedachter Zeiger zieht einen sanften Wirbel
    neustart(); vorfuehrung = true; html.classList.add('vorfuehrung');
  }

  setzen(variante, false);
  fit();

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