Zum Inhalt
← Cursor

Invertierender Cursor (mix-blend-mode: difference)

Ein weißer Kreis mit mix-blend-mode: difference folgt dem Zeiger, invertiert die Regie-Namen und wächst über ihnen; über dem Werkbild schaltet er sich ab und wird zum Ring.

Ein weißer Kreis folgt dem Zeiger. Er sitzt in einem festen Wrapper mit mix-blend-mode: difference. Über dunkler Schrift auf Hell und heller Schrift auf Dunkel kehrt er die Farben um: Im Kreis wird die Schrift zum Negativ. Über großen Headlines wächst er, über Links schrumpft er. Varianten machen ihn zum weichen Lichtfleck oder zu mehreren farbigen, versetzt folgenden Kreisen.

Grafisch starker Effekt in monochromen, typolastigen Layouts. Der Kreis wirkt wie eine Lupe oder ein Fotonegativ und macht große Schrift zum Spielfeld.

Wann einsetzen

Monochrome Seiten mit großen Headlines, Namenslisten, Roster oder Indexseiten. Am besten auf einen Bereich begrenzt.

Wann nicht

Über farbigen Bildern und Werkfotos: Die Farben werden unvorhersehbar, das Werk wird getönt. Über Formularen und Fließtext. Als seitenweiter Standard wirkt er 2026 abgegriffen.

Varianten

  • Kleiner Invert-Punkt, der über Headlines wächst und über Links schrumpft
  • Großer, weichgezeichneter Lichtfleck über der Headline (30 → 400 px, Unschärfe bis 30 px)
  • Mehrere farbige Kreise, die mit Verzögerung folgen
  • Über Bildern ausgeschaltet und durch einen Ring ohne Blend-Modus ersetzt

Umsetzung

.cursor { position: fixed; inset: 0; pointer-events: none; mix-blend-mode: difference } .cursor i { position: absolute; width: 400px; height: 400px; margin: -200px; border-radius: 50%; background: #fff }. Position und Größe per JS in einem rAF-Takt: transform: translate(x, y) scale(d / 400), Position per Lerp, Größe als Tween. Den Blend-Modus auf den Wrapper legen, nicht auf den Kreis.

Fallstricke: Der Blend-Modus mischt nur mit dem, was im selben Stacking Context darunter liegt. Liegt der Cursor in einem Element mit isolation, transform oder opacity, bricht der Effekt; deshalb als direktes Kind von body. In Safari kann will-change: transform auf dem Cursor den Blend-Modus abschalten. Den Systemzeiger nur mit (hover: hover) and (pointer: fine) ausblenden.

Robust umsetzen

Telefon. Entfällt. Auf Touch gibt es keinen Zeiger.

Ohne Bewegung. Abschalten und den Systemzeiger zeigen.

Barrierefrei. Die Invertierung macht Kontraste unvorhersehbar, deshalb nie über Formularen oder Fließtext. Der Cursor ist dekorativ (aria-hidden), Fokus-Rahmen bleiben sichtbar, alle Links funktionieren ohne ihn.

Code

<!DOCTYPE html>
<!--
  Invertierender Cursor (mix-blend-mode: difference)
  Fundus · Cursor · https://cecodesign.de/lab/fundus/cursor/blend-difference-cursor/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-variante="punkt">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Invertierender Cursor (mix-blend-mode: difference) · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Fragment Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fragment-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

:root {
  --grund: #0e0e0e; --text: #f2f2f2; --leise: #8a8a8a; --linie: rgba(242, 242, 242, .14); --platz: #1c1c1c;
  --kurve: cubic-bezier(0.455, 0.03, 0.515, 0.955);   /* easeInOutQuad, Rezept (Nadav Kander) */
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; grid-template-columns: minmax(0, 1fr); }
a { color: inherit; text-decoration: none; }
.mono { font: 400 12px/1.4 'Fragment Mono', ui-monospace, monospace; letter-spacing: .02em; color: var(--leise); font-variant-numeric: tabular-nums; }
.serif { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; }

/* Ecken-Navigation */
.kopf { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); align-items: baseline; padding: 20px clamp(16px, 2.6vw, 36px); font-size: 14px; }
.kopf .marke { grid-column: 1 / 7; font-family: 'Instrument Serif', Georgia, serif; font-size: 25px; line-height: 1; letter-spacing: -.005em; }
.kopf .marke i { font-style: italic; }
.kopf nav { grid-column: 9 / 18; display: flex; gap: 28px; }
.kopf nav a { color: var(--leise); transition: color 200ms var(--kurve); }
.kopf nav a[aria-current], .kopf nav a:hover { color: var(--text); }
.kopf .ort { grid-column: 18 / 25; text-align: right; }

/* Roster links, Bild rechts */
.roster { display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); column-gap: clamp(8px, 1vw, 16px); padding: clamp(24px, 3vw, 48px) clamp(16px, 2.6vw, 36px) clamp(24px, 3vw, 48px); align-items: center; align-content: center; }
.namen { grid-column: 1 / 11; }
.namen .kopfzeile { display: flex; justify-content: space-between; padding-bottom: 12px; border-bottom: 1px solid var(--linie); }
.namen ol { list-style: none; padding: 0; }
.namen li a { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: 16px; padding: clamp(8px, 1vw, 14px) 0 clamp(10px, 1.1vw, 16px); border-bottom: 1px solid var(--linie); }
.namen .name { font-family: 'Instrument Serif', Georgia, serif; font-size: clamp(2.25rem, 3.9vw, 4.25rem); line-height: 1; letter-spacing: -.01em; white-space: nowrap; }
.namen .name em { font-style: italic; }
.namen .fach { text-align: right; }
.namen li a:focus-visible { outline: 1px solid var(--text); outline-offset: 4px; }

.buehne { grid-column: 12 / 25; }
.bild { position: relative; aspect-ratio: 3 / 2; background: var(--platz); overflow: hidden; }
.bild svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; opacity: 0; transition: opacity 600ms var(--kurve); }
.bild svg.an { opacity: 1; }
.unterzeile { display: grid; grid-template-columns: 1fr auto; gap: 16px; padding-top: 12px; }
.unterzeile .titel { color: var(--text); }
.fuss .nw { white-space: nowrap; }
.fuss { display: flex; justify-content: space-between; gap: 16px; padding: 18px clamp(16px, 2.6vw, 36px) 20px; border-top: 1px solid var(--linie); }

/* Der Cursor: Wrapper trägt den Blend-Modus (wie OFF+BRAND), Kreise sind weiß */
.cursor { position: fixed; inset: 0; z-index: 50; pointer-events: none; mix-blend-mode: difference; opacity: 0; transition: opacity 350ms var(--kurve); }
.cursor.da { opacity: 1; }
.cursor i { position: absolute; left: 0; top: 0; width: 400px; height: 400px; margin: -200px 0 0 -200px; border-radius: 50%; background: #fff; transform: scale(0); }
.cursor i + i { display: none; }
[data-variante="farben"] .cursor i + i { display: block; }
[data-variante="farben"] .cursor i:nth-child(1) { background: #ff6a3d; }
[data-variante="farben"] .cursor i:nth-child(2) { background: #3ad1c4; }
[data-variante="farben"] .cursor i:nth-child(3) { background: #f2d03b; }
/* Über dem Werkbild: kein Blend (das Bild wird nie getönt), stattdessen ein schlichter Ring */
.ring { position: fixed; left: 0; top: 0; z-index: 51; width: 76px; height: 76px; margin: -38px 0 0 -38px; border-radius: 50%; border: 1px solid rgba(242, 242, 242, .85); display: grid; place-items: center; pointer-events: none; opacity: 0; transition: opacity 350ms var(--kurve); color: var(--text); }
.ring.da { opacity: 1; }
.ring span { font: 400 10.5px/1 'Fragment Mono', monospace; letter-spacing: .08em; text-transform: uppercase; }
.cursor-an, .cursor-an a, .cursor-an button { cursor: none; }

/* Pult */
.pult { position: fixed; left: 16px; bottom: 72px; z-index: 60; display: grid; gap: 8px; padding: 11px 13px; background: rgba(28, 28, 28, .9); border: 1px solid var(--linie); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
.pult .m, .pult button { font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.pult .knoepfe { display: flex; gap: 4px; }
.pult button { padding: 7px 9px; border: 1px solid var(--linie); background: none; color: var(--text); cursor: pointer; transition: background-color 200ms var(--kurve), color 200ms var(--kurve); }
.pult button[aria-pressed="true"] { background: var(--text); color: var(--grund); border-color: var(--text); }
.pult .werte { text-transform: none; letter-spacing: .04em; }

.vorschau body { overflow: hidden; }
.vorschau .fuss { display: none; }

@media (max-width: 900px) {
  .kopf nav { display: none; }
  .kopf .marke { grid-column: 1 / 15; }
  .kopf .ort { grid-column: 15 / 25; }
  .roster { grid-template-columns: 1fr; row-gap: 28px; }
  .namen, .buehne { grid-column: 1; }
  .buehne { position: relative; top: 0; order: -1; }
  .fuss { flex-direction: column; }
}
@media (max-width: 520px) {
  .koord { display: none; }
  .namen .fach { display: none; }
  .namen li a { grid-template-columns: 1fr; }
}
/* Touch: kein Cursor (entfällt), Tippen auf einen Namen wechselt das Bild */
@media (hover: none) { .cursor, .ring, .pult { display: none; } }
.reduziert .cursor, .reduziert .ring, .reduziert .pult { display: none; }
.reduziert .bild svg { transition: 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">
  <a class="marke" href="#">Freiheit <i>Film</i></a>
  <nav aria-label="Hauptmenü"><a href="#" aria-current="page" data-link>Regie</a><a href="#" data-link>Arbeiten</a><a href="#" data-link>Studio</a><a href="#" data-link>Kontakt</a></nav>
  <p class="mono ort">Köln-Deutz<span class="koord"> · 50°56′ N 6°58′ O</span></p>
</header>

<main class="roster">
  <section class="namen" aria-label="Regie">
    <div class="kopfzeile"><span class="mono">Regie</span><span class="mono">06</span></div>
    <ol data-liste>
      <li><a href="#" data-i="0"><span class="name">Mareike Oyelaran</span><span class="mono fach">Werbung, Musikvideo</span></a></li>
      <li><a href="#" data-i="1"><span class="name">Jonte <em>Weißkirchen</em></span><span class="mono fach">Werbung</span></a></li>
      <li><a href="#" data-i="2"><span class="name">Ilka Rosendahl</span><span class="mono fach">Musikvideo, Kurzfilm</span></a></li>
      <li><a href="#" data-i="3"><span class="name">Bastian <em>Ngo</em></span><span class="mono fach">Dokumentarisch</span></a></li>
      <li><a href="#" data-i="4"><span class="name">Esra Kalkan</span><span class="mono fach">Werbung, Mode</span></a></li>
      <li><a href="#" data-i="5"><span class="name">Pim <em>de Graaf</em></span><span class="mono fach">Kurzfilm</span></a></li>
    </ol>
  </section>

  <figure class="buehne" aria-live="polite">
    <div class="bild" data-bild>
      <!-- 0 Blaue Stunde am Rheinufer -->
      <svg class="an" viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Standbild: Rheinufer zur blauen Stunde, Brückenbögen gegen den Himmel">
        <defs><linearGradient id="s0" x2="0" y2="1"><stop offset="0" stop-color="#0a1630"/><stop offset=".45" stop-color="#2c3f6c"/><stop offset=".62" stop-color="#b9877a"/><stop offset=".66" stop-color="#e7a777"/></linearGradient>
        <linearGradient id="w0" x2="0" y2="1"><stop offset="0" stop-color="#3a3f5a"/><stop offset="1" stop-color="#070a14"/></linearGradient></defs>
        <rect width="1600" height="660" fill="url(#s0)"/>
        <path d="M0 640h1600v40H0z" fill="#0a0d18"/>
        <path transform="translate(290 0)" d="M980 640V430l8-30 8 30v-40l10-60 10 60v40l8-30 8 30V640zM1060 640V420l8-30 8 30v-40l10-70 10 70v40l8-30 8 30V640zM1032 640V500h28v140z" fill="#0a0d18"/>
        <path d="M120 640V556h1080v84h-40a160 66 0 0 0-320 0h-40a160 66 0 0 0-320 0h-40a160 66 0 0 0-320 0z" fill="#0b0f1d"/>
        <path d="M120 558h1080v-12H120z" fill="#0b0f1d"/><path d="M200 546l140-60 140 60M520 546l140-60 140 60M840 546l140-60 140 60" stroke="#0b0f1d" stroke-width="8" fill="none"/>
        <rect y="680" width="1600" height="320" fill="url(#w0)"/>
        <g fill="#f0b07a" opacity=".55"><rect x="300" y="700" width="90" height="3"/><rect x="420" y="728" width="60" height="2"/><rect x="760" y="712" width="140" height="3"/><rect x="1010" y="744" width="70" height="2"/><rect x="1120" y="705" width="110" height="3"/><rect x="560" y="770" width="50" height="2"/></g>
        <circle cx="1240" cy="602" r="5" fill="#ffd9a8"/><circle cx="760" cy="606" r="4" fill="#ffd9a8"/>
      </svg>
      <!-- 1 Nachtschicht: Natriumlicht, nasse Straße -->
      <svg viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Standbild: Straßenlaterne bei Nacht, Licht spiegelt sich auf nassem Asphalt">
        <defs><radialGradient id="l1" cx="1120" cy="250" r="620" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffcf8a"/><stop offset=".08" stop-color="#f2933c"/><stop offset=".4" stop-color="#5a2a0e"/><stop offset="1" stop-color="#0b0705"/></radialGradient>
        <linearGradient id="r1" x2="0" y2="1"><stop offset="0" stop-color="#ffb05a" stop-opacity=".7"/><stop offset="1" stop-color="#ffb05a" stop-opacity="0"/></linearGradient></defs>
        <rect width="1600" height="1000" fill="url(#l1)"/>
        <rect x="1110" y="250" width="10" height="460" fill="#120a05"/><rect x="1060" y="236" width="80" height="16" rx="6" fill="#ffe2b0"/>
        <rect y="700" width="1600" height="300" fill="#0d0805" opacity=".82"/>
        <rect x="1090" y="710" width="50" height="290" fill="url(#r1)"/>
        <g fill="#2a1508"><rect x="120" y="380" width="300" height="330"/><rect x="460" y="300" width="220" height="410"/></g>
        <g fill="#ffcf8a" opacity=".75"><rect x="160" y="430" width="40" height="56"/><rect x="250" y="430" width="40" height="56"/><rect x="520" y="360" width="34" height="48"/></g>
        <path d="M760 710c0-60 10-120 30-150 14-20 40-20 50 0 16 30 22 90 22 150z" fill="#0a0604"/><circle cx="815" cy="530" r="26" fill="#0a0604"/>
      </svg>
      <!-- 2 Rotes Studio -->
      <svg viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Standbild: rote Studiohohlkehle mit einem Stuhl im Lichtkegel">
        <defs><linearGradient id="h2" x2="0" y2="1"><stop offset="0" stop-color="#3a0709"/><stop offset=".6" stop-color="#a51d20"/><stop offset=".72" stop-color="#c8332c"/><stop offset="1" stop-color="#4a0a0b"/></linearGradient>
        <radialGradient id="k2" cx="800" cy="720" r="420" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff9a7a" stop-opacity=".55"/><stop offset="1" stop-color="#ff9a7a" stop-opacity="0"/></radialGradient></defs>
        <rect width="1600" height="1000" fill="url(#h2)"/><rect width="1600" height="1000" fill="url(#k2)"/>
        <path d="M640 0h320l120 760H520z" fill="#ffd0b8" opacity=".09"/>
        <g fill="#1d0405"><rect x="760" y="560" width="90" height="12"/><rect x="760" y="470" width="12" height="102"/><rect x="760" y="572" width="10" height="130"/><rect x="840" y="572" width="10" height="130"/></g>
        <ellipse cx="805" cy="712" rx="120" ry="14" fill="#2a0607" opacity=".6"/>
      </svg>
      <!-- 3 Kantine: Leuchtstoffröhren -->
      <svg viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Standbild: leere Kantine unter grünlichem Neonlicht">
        <defs><linearGradient id="g3" x2="0" y2="1"><stop offset="0" stop-color="#0f1d1a"/><stop offset=".5" stop-color="#2d4a42"/><stop offset="1" stop-color="#0c1513"/></linearGradient></defs>
        <rect width="1600" height="1000" fill="url(#g3)"/>
        <g stroke="#d9f5e3" stroke-linecap="round"><path d="M200 60 700 300" stroke-width="16"/><path d="M1400 60 900 300" stroke-width="16"/><path d="M560 40 760 300" stroke-width="12"/><path d="M1040 40 840 300" stroke-width="12"/><path d="M790 30v270" stroke-width="10"/></g>
        <g fill="#0a1311"><path d="M0 760 600 560h400l600 200v240H0z"/></g>
        <g fill="#9fc7b0" opacity=".25"><path d="M260 740 640 600h120l-260 160z"/><path d="M1340 740 960 600H840l260 160z"/></g>
        <rect x="0" y="0" width="1600" height="1000" fill="#bff0d0" opacity=".04"/>
      </svg>
      <!-- 4 Messehalle -->
      <svg viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Standbild: leere Messehalle mit Lichtraster und einer einzelnen Person">
        <rect width="1600" height="1000" fill="#11161c"/>
        <g fill="#e9f1fb"><rect x="100" y="80" width="140" height="10"/><rect x="420" y="80" width="140" height="10"/><rect x="740" y="80" width="140" height="10"/><rect x="1060" y="80" width="140" height="10"/><rect x="1380" y="80" width="140" height="10"/>
        <rect x="250" y="200" width="100" height="7"/><rect x="510" y="200" width="100" height="7"/><rect x="770" y="200" width="100" height="7"/><rect x="1030" y="200" width="100" height="7"/><rect x="1290" y="200" width="100" height="7"/>
        <rect x="380" y="290" width="70" height="5"/><rect x="590" y="290" width="70" height="5"/><rect x="790" y="290" width="70" height="5"/><rect x="990" y="290" width="70" height="5"/><rect x="1190" y="290" width="70" height="5"/></g>
        <path d="M0 1000 640 420h320l640 580z" fill="#2a333d"/>
        <path d="M0 1000 640 420h320l640 580z" fill="#d9e6f5" opacity=".06"/>
        <path d="M792 600c0-40 6-70 14-82 6-8 18-8 22 0 8 12 12 42 12 82z" fill="#0b0f13"/><circle cx="817" cy="508" r="12" fill="#0b0f13"/>
      </svg>
      <!-- 5 Rheinpark im Sommer -->
      <svg viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Standbild: überbelichtete Sommerwiese im Gegenlicht">
        <defs><radialGradient id="s5" cx="1180" cy="300" r="900" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fffaf0"/><stop offset=".2" stop-color="#f6e2b8"/><stop offset=".6" stop-color="#cdb27c"/><stop offset="1" stop-color="#7a6a3c"/></radialGradient>
        <linearGradient id="g5" x2="0" y2="1"><stop offset="0" stop-color="#9c9a4f"/><stop offset="1" stop-color="#3f4a22"/></linearGradient></defs>
        <rect width="1600" height="1000" fill="url(#s5)"/>
        <path d="M0 640c260-30 520-40 800-30s560 30 800 10v380H0z" fill="url(#g5)"/>
        <path d="M0 640c260-30 520-40 800-30s560 30 800 10v40c-300 20-560 0-800-6S240 640 0 680z" fill="#f6e6b8" opacity=".35"/>
        <g fill="#4a4520" opacity=".8"><path d="M300 600c10-120 30-200 70-240 40 40 60 120 70 240z"/><path d="M180 620c6-80 20-130 46-160 26 30 40 80 46 160z"/></g>
        <circle cx="1180" cy="300" r="70" fill="#fff" opacity=".9"/>
      </svg>
    </div>
    <figcaption class="unterzeile"><span class="mono titel" data-titel>Mareike Oyelaran · Musikvideo „Brückenlicht“ für Die Halbtöne</span><span class="mono" data-zaehler>01 / 06</span></figcaption>
  </figure>
</main>

<footer class="fuss"><span class="mono">Freiheit Film GmbH · <span class="nw">Deutz-Mülheimer Straße 30 · 50679 Köln</span></span><span class="mono">Impressum · Datenschutz · Vertretung Wien, Zürich</span></footer>

<div class="cursor" data-cursor aria-hidden="true"><i></i><i></i><i></i></div>
<div class="ring" data-ring aria-hidden="true"><span>Reel</span></div>

<div class="pult">
  <div class="knoepfe" role="group" aria-label="Variante">
    <button type="button" data-v="punkt" aria-pressed="true">Punkt</button>
    <button type="button" data-v="fleck" aria-pressed="false">Lichtfleck</button>
    <button type="button" data-v="farben" aria-pressed="false">Drei Farben</button>
  </div>
  <span class="m werte" data-werte></span>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const feinerZeiger = matchMedia('(hover: hover) and (pointer: fine)');
  const cursor = document.querySelector('[data-cursor]');
  const kreise = [...cursor.children];
  const ring = document.querySelector('[data-ring]');
  const BASIS = 400;                                    // px, Kreis wird nur skaliert

  /* Varianten: Durchmesser in px je Zustand, Weichzeichnung über der Headline, Folgefaktor je Kreis */
  const V = {
    punkt:  { normal: 22, name: 120, link: 22 * .55, blur: 0,  folge: [.2],             text: '22 → 120 px · Link × .55 · lerp .2' },
    fleck:  { normal: 30, name: 400, link: 30 * .55, blur: 30, folge: [.2],             text: '30 → 400 px · blur 0 → 30 px · lerp .2' },
    farben: { normal: 26, name: 220, link: 26 * .55, blur: 0,  folge: [.24, .14, .08], text: '3 Kreise · 26 → 220 px · lerp .24 / .14 / .08' }
  };
  const DAUER = 350;                                    // ms Größenwechsel, easeInOutQuad
  const inOut = q => q < .5 ? 2 * q * q : 1 - Math.pow(-2 * q + 2, 2) / 2;
  const v = () => V[html.dataset.variante] || V.punkt;
  const werte = document.querySelector('[data-werte]');
  const zeigeWerte = () => { werte.textContent = v().text + ' · 350 ms easeInOutQuad'; };
  zeigeWerte();

  /* Zustand */
  const ziel = { x: innerWidth / 2, y: innerHeight / 2 };
  const ist = kreise.map(() => ({ x: ziel.x, y: ziel.y }));
  let zustand = 'normal', groesse = { von: 0, nach: 0, t0: 0 }, sichtbar = false, laeuft = false, letzte = performance.now();

  function setzeZustand(z) {
    const nach = z === 'bild' ? 0 : v()[z];
    if (z === zustand && groesse.nach === nach) return;
    const jetzt = performance.now();
    groesse = { von: aktuelleGroesse(jetzt), nach, t0: jetzt };
    zustand = z;
    ring.classList.toggle('da', z === 'bild' && sichtbar);
    starten();
  }
  function aktuelleGroesse(t) { const q = Math.min(1, (t - groesse.t0) / DAUER); return groesse.von + (groesse.nach - groesse.von) * inOut(q); }

  function bild(t) {
    const dt = Math.min(64, t - letzte) / 16.667; letzte = t;
    const d = aktuelleGroesse(t), s = d / BASIS, cfg = v();
    // Weichzeichnung wächst mit der Größe; der Filter wird mitskaliert, daher durch s teilen
    const anteil = cfg.blur ? Math.max(0, (d - cfg.normal) / (cfg.name - cfg.normal)) : 0;
    kreise.forEach((k, n) => {
      const f = 1 - Math.pow(1 - (cfg.folge[n] || cfg.folge[0]), dt);
      ist[n].x += (ziel.x - ist[n].x) * f; ist[n].y += (ziel.y - ist[n].y) * f;
      k.style.transform = `translate(${ist[n].x}px, ${ist[n].y}px) scale(${s})`;
      k.style.filter = anteil > .001 && s > 0 ? `blur(${(cfg.blur * anteil / s).toFixed(1)}px)` : '';
    });
    ring.style.transform = `translate(${ist[0].x}px, ${ist[0].y}px)`;
    const ruhig = Math.abs(ziel.x - ist[ist.length - 1].x) < .1 && Math.abs(ziel.y - ist[ist.length - 1].y) < .1 && t - groesse.t0 > DAUER;
    if (ruhig && !uhr) { laeuft = false; return; }
    requestAnimationFrame(bild);
  }
  function starten() { if (!laeuft) { laeuft = true; letzte = performance.now(); requestAnimationFrame(bild); } }

  /* Was liegt unter dem Zeiger? */
  function zustandAus(el) {
    if (!el) return 'normal';
    if (el.closest('[data-bild]')) return 'bild';
    if (el.closest('.namen li a')) return 'name';
    if (el.closest('a, button')) return 'link';
    return 'normal';
  }

  /* Bildwechsel: nur Opazität (Rezept), Zähler und Unterzeile in Mono */
  const bilder = [...document.querySelectorAll('[data-bild] svg')];
  const TITEL = [
    'Mareike Oyelaran · Musikvideo „Brückenlicht“ für Die Halbtöne',
    'Jonte Weißkirchen · Werbefilm „Nachtschicht“ für Kaffeerösterei Hafenbohne',
    'Ilka Rosendahl · Musikvideo „Rot sehen“ für Lina Marek',
    'Bastian Ngo · Dokumentarfilm „Kantine“, 28 Min.',
    'Esra Kalkan · Kampagne „Halle 11“ für Atelier Oszwald',
    'Pim de Graaf · Kurzfilm „Rheinpark, 17 Uhr“, 14 Min.'
  ];
  const titel = document.querySelector('[data-titel]'), zaehler = document.querySelector('[data-zaehler]');
  let aktiv = 0;
  function zeigeBild(i) {
    if (i === aktiv) return;
    aktiv = i;
    bilder.forEach((b, j) => b.classList.toggle('an', j === i));
    titel.textContent = TITEL[i];
    zaehler.textContent = String(i + 1).padStart(2, '0') + ' / 06';
  }
  const namen = [...document.querySelectorAll('[data-i]')];
  namen.forEach(a => {
    a.addEventListener('pointerenter', () => zeigeBild(+a.dataset.i));
    a.addEventListener('focus', () => zeigeBild(+a.dataset.i));
    a.addEventListener('click', e => { e.preventDefault(); zeigeBild(+a.dataset.i); });
  });
  document.querySelectorAll('.kopf a, .fuss a').forEach(a => a.addEventListener('click', e => e.preventDefault()));

  /* Echte Maus */
  const aktivMoeglich = () => feinerZeiger.matches && !reduziert();
  function an() { sichtbar = true; cursor.classList.add('da'); html.classList.add('cursor-an'); }
  addEventListener('pointermove', e => {
    if (e.pointerType !== 'mouse' || !aktivMoeglich() || uhr) return;
    if (!sichtbar) { ist.forEach(p => { p.x = e.clientX; p.y = e.clientY; }); an(); setzeZustand('normal'); }
    ziel.x = e.clientX; ziel.y = e.clientY;
    setzeZustand(zustandAus(e.target));
    starten();
  });
  document.addEventListener('pointerleave', () => { if (uhr) return; sichtbar = false; cursor.classList.remove('da'); ring.classList.remove('da'); });

  /* Pult */
  document.querySelectorAll('[data-v]').forEach(b => b.addEventListener('click', () => waehle(b.dataset.v)));
  function waehle(name) {
    html.dataset.variante = name;
    document.querySelectorAll('[data-v]').forEach(x => x.setAttribute('aria-pressed', x.dataset.v === name));
    zeigeWerte();
    groesse = { von: aktuelleGroesse(performance.now()), nach: zustand === 'bild' ? 0 : v()[zustand], t0: performance.now() };
    starten();
  }

  /* Vorführung: der Kreis fährt Namen, Navigation und Bild ab; jede Runde eine andere Variante */
  let uhr = null, S = [], i = 0, ab = 0, von = { x: 0, y: 0 }, runde = 0;
  const mitte = (el, fx = .5, fy = .55) => { const r = el.getBoundingClientRect(); return { x: r.left + r.width * fx, y: r.top + r.height * fy }; };
  function stationen() {
    const nm = n => namen[n].querySelector('.name');
    const b = document.querySelector('[data-bild]');
    const nav = document.querySelector('.kopf nav a:nth-child(2)') || document.querySelector('.kopf .marke');
    return [
      { ...mitte(document.querySelector('.namen .kopfzeile'), .75, 2.2), ms: 900 },
      { ...mitte(nm(0), .3), ms: 1500 },
      { ...mitte(nm(0), .72), ms: 900 },
      { ...mitte(nm(2), .5), ms: 1400 },
      { ...mitte(b, .55, .5), ms: 1500 },
      { ...mitte(nav), ms: 1100 },
      { ...mitte(nm(4), .4), ms: 1500 },
    ];
  }
  function schritt(t) {
    if (t - ab > S[i].ms) {
      von = { x: ziel.x, y: ziel.y }; i = (i + 1) % S.length; ab = t;
      if (i === 0) { runde++; waehle(['punkt', 'fleck', 'farben'][runde % 3]); S = stationen(); }
    }
    const q = Math.min(1, (t - ab) / 700), e = inOut(q);
    ziel.x = von.x + (S[i].x - von.x) * e; ziel.y = von.y + (S[i].y - von.y) * e;
    const el = document.elementFromPoint(ziel.x, ziel.y);
    setzeZustand(zustandAus(el));
    const a = el && el.closest('[data-i]'); if (a) zeigeBild(+a.dataset.i);
    uhr = requestAnimationFrame(schritt);
  }
  function vorfuehren() {
    pause();
    if (reduziert()) { let n = 0; uhr = setInterval(() => zeigeBild(++n % 6), 2200); return; }
    html.classList.add('vorfuehrung');
    S = stationen(); i = 0; ab = performance.now();
    von = { x: S[0].x, y: S[0].y }; ziel.x = von.x; ziel.y = von.y; ist.forEach(p => { p.x = ziel.x; p.y = ziel.y; });
    an(); setzeZustand('normal');
    uhr = requestAnimationFrame(schritt); starten();
  }
  function pause() { cancelAnimationFrame(uhr); clearInterval(uhr); uhr = null; }
  function neustart() {
    pause(); html.classList.remove('vorfuehrung');
    sichtbar = false; cursor.classList.remove('da'); ring.classList.remove('da'); html.classList.remove('cursor-an');
    zeigeBild(0); setzeZustand('normal');
  }

})();
</script>