Zum Inhalt
← Hover

Form-Morph-Buttons (Clip-Path)

Der Button bricht aus dem Rechteck aus: Beim Hover morpht seine Fläche per clip-path in eine zweite Form, als Blob, Pfeilspitze oder Wölbung.

Abschluss-CTA eines Kreativstudios: die Fläche morpht beim Hover in eine zweite Form, Unterumschalter Blob, Pfeil, Wölbung.

Die Fläche des Buttons ist ein eigenes Element oder Pseudo-Element mit clip-path: path(), polygon() oder ellipse(). Der Link selbst bleibt ungeclippt. Beim Hover wechselt die Form zu einer zweiten Form mit gleicher Punktzahl, zum Beispiel Blob zu Blob, Rechteck zu Pfeilspitze oder flache zu stärkerer Wölbung. Der Browser interpoliert Punkt für Punkt. Eine Kurve mit y über 1 lässt die Form federn. Farbe und Pfeil-Icon wechseln mit, der Text bleibt ruhig und lesbar.

Der Button bricht mit der Rechteck-Norm und reagiert wie ein Körper statt wie eine Fläche. Das wirkt verspielt und organisch, braucht aber nur CSS und kostet kaum Leistung.

Wann einsetzen

Kreative und verspielte Marken, Kultur- und Kinderangebote, Studios. Am besten für einen einzelnen Haupt-CTA im Hero oder am Seitenende, auch für eine Showreel-Fläche.

Wann nicht

In seriösen B2B-Kontexten, in Formularen und bei mehreren Buttons nebeneinander, weil das unruhig wird.

Varianten

  • Blob-Morph per clip-path: path(), 500 ms mit Überschwing-Kurve cubic-bezier(0.585, 2.5, 0.645, 0.55) (Codrops ‚Janus‘)
  • Rechteck zu Pfeilspitze per polygon(), 400 ms cubic-bezier(0.2, 1, 0.8, 1) (Codrops ‚Anthe‘)
  • Wölbung per ellipse(100% 50%) zu ellipse(80% 50%), 750 ms cubic-bezier(0.65, 0.05, 0, 1), gemessen auf einer Video-Fläche
  • Organischer border-radius-Blob als Kontur, etwa 58% 42% 55% 45% / 56% 45% 55% 44%
  • Rotierender Kreis-Badge mit Text auf einem SVG-Pfad
  • corner-shape (squircle, scoop, notch) für neue Eckenformen: ab Chrome 139, in Safari und Firefox nur als Vorschau (MDN, Stand 31.10.2026), siehe eigener Eintrag corner-shape-eckenformen
  • Gooey: SVG-Filter aus Blur und Alpha-Matrix, ein Tropfen löst sich als Icon-Blase (Codrops „Creative Gooey Effects“), Rückfall ohne Filter

Umsetzung

.btn { position: relative; isolation: isolate; } .btn .form { position: absolute; inset: 0; z-index: -1; background: #f2f2f2; clip-path: path('M253,77.6 C…Z'); transition: clip-path .5s cubic-bezier(.585, 2.5, .645, .55), background-color .2s linear; } .btn:hover .form, .btn:focus-visible .form { clip-path: path('M236.2,97.5 C…Z'); background-color: #ff5a1f; }. Beide Pfade brauchen dieselbe Befehlsfolge und Punktzahl, sonst springt die Form. path() kennt nur absolute Koordinaten: feste Buttongröße und per scale skalieren, oder die neuere Funktion shape() mit relativen Werten nutzen. polygon() und ellipse() sind mit Prozent und calc() responsiv. Bei einer Überschwing-Kurve die Farbe getrennt und linear überblenden, sonst schießt sie über die Zielfarbe hinaus. Den Fokusring nie auf das geclippte Element legen.

Robust umsetzen

Telefon. Ohne Hover spielt ein Tippen den Formwechsel kurz ab (Klasse per pointerdown setzen, nach Dauer plus etwa 450 ms wieder entfernen). Alternativ statisch lassen.

Ohne Bewegung. Formwechsel ohne Überschwingen, sofort oder in 150 ms linear. Laufende Inhalte in der Fläche anhalten.

Barrierefrei. Fokusstil auf das ungeclippte Element legen und :focus-visible denselben Formwechsel geben wie :hover. Die Form ist Dekoration (aria-hidden), der Link braucht einen klaren Text oder aria-label. Kontrast von Text zu beiden Flächenfarben prüfen, auch wenn mix-blend-mode: difference im Spiel ist. Gooey: den Filter nur auf die Formen legen, nie auf Text.

Code

<!DOCTYPE html>
<!--
  Form-Morph-Buttons (Clip-Path)
  Fundus · Hover · https://cecodesign.de/lab/fundus/hover/clip-path-form-hover/
  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>Form-Morph-Buttons (Clip-Path) · 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; --hell: #f2f2f2; --weiss: #ffffff; --meta: #8a8a8a; --linie: rgba(242, 242, 242, .14);
  --signal: #ff5a1f;
  /* Werte je Variante; JS setzt sie am <body>, damit die Werte-Anzeige stimmt */
  --kurve: cubic-bezier(0.585, 2.5, 0.645, 0.55);
  --dauer: 500ms;
  --farbe: 200ms linear;   /* Farbwechsel: bei der Überschwing-Kurve getrennt, sonst würde die Farbe über Orange hinaus ins Rot schießen */
  --k: 1;   /* Maßstab des Blob-Knopfs: path() kennt nur absolute Koordinaten */
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--weiss); font-family: 'TikTok Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: flex; flex-direction: column; overflow-x: hidden; background: var(--grund); }
.mono { font: 500 11px/1.35 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; color: var(--meta); }

/* Kopf */
.kopf { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; padding: 22px clamp(16px, 3.4vw, 48px); }
.marke { font-weight: 800; font-stretch: 75%; font-size: 22px; letter-spacing: -.02em; text-transform: uppercase; color: var(--weiss); text-decoration: none; }
.marke span { color: var(--signal); }
.kopf .mitte { text-align: center; }
.kopf .rechts { text-align: right; }

/* Bühne: Abschluss-CTA eines Studios */
.buehne { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: clamp(28px, 4vw, 52px); padding: clamp(12px, 3vw, 40px) clamp(16px, 3.4vw, 48px) clamp(20px, 3vw, 36px); position: relative; }
.satz { font-weight: 800; font-stretch: 75%; text-transform: uppercase; font-size: clamp(3.5rem, 10vw, 11rem); line-height: .9; letter-spacing: -.03em; color: var(--weiss); }
.satz h2 { display: inline; font: inherit; }
.satz .leise { color: var(--meta); }
.ueber { display: block; margin-bottom: clamp(14px, 1.6vw, 22px); }

/* Knopf-Gerüst, gilt für alle Varianten:
   der Knopf selbst bleibt ungeclippt (Fokusring!), geclippt wird nur die Fläche .form darin */
.knopf-ort { display: inline-block; vertical-align: middle; margin-left: .12em; position: relative; top: -.04em; }
.knopf {
  position: relative; isolation: isolate; display: grid; place-items: center;
  color: var(--grund); text-decoration: none; text-transform: none; letter-spacing: 0;
  font: 600 20px/1 'TikTok Sans', system-ui, sans-serif; font-stretch: 100%;
  -webkit-tap-highlight-color: transparent; outline: none; border-radius: 10px;
}
.knopf[hidden] { display: none; }
.knopf .form { position: absolute; inset: 0; z-index: -1; background: var(--hell);
  transition: clip-path var(--dauer) var(--kurve), background-color var(--farbe); }
.knopf .text { display: inline-flex; align-items: center; gap: 10px; transition: translate var(--dauer) var(--kurve); }
.knopf .pfeil { width: 18px; height: 18px; transition: rotate var(--dauer) var(--kurve); rotate: -45deg; }
.knopf:hover .form, .knopf.an .form, .knopf:focus-visible .form { background-color: var(--signal); }
.knopf:hover .pfeil, .knopf.an .pfeil, .knopf:focus-visible .pfeil { rotate: 0deg; }
.knopf:focus-visible { outline: 2px solid var(--signal); outline-offset: 8px; }
.knopf.blob:focus-visible { outline-offset: -4px; border-radius: 24px; }   /* die Blob-Form sitzt schon gut 20 px innerhalb der Box */

/* Variante 1 · Blob: path() → path(), gleiche Befehlsfolge (M + 8 C + Z), 280 × 140 */
.knopf-ort[data-v="blob"] { width: calc(280px * var(--k)); height: calc(140px * var(--k)); }
.knopf.blob { width: 280px; height: 140px; scale: var(--k); transform-origin: 0 0; }
.knopf.blob .form { clip-path: path('M253.0,77.6 C249.0,88.9 225.6,101.3 203.8,108.6 C182.1,116.0 147.8,123.5 122.4,121.9 C97.1,120.3 67.7,109.0 51.7,99.1 C35.6,89.1 21.7,73.5 25.9,62.3 C30.1,51.1 54.9,39.1 76.9,31.8 C98.8,24.5 132.3,17.0 157.4,18.6 C182.5,20.2 211.5,31.4 227.4,41.2 C243.4,51.1 256.9,66.4 253.0,77.6 Z'); }
.knopf.blob:hover .form, .knopf.blob.an .form, .knopf.blob:focus-visible .form { clip-path: path('M236.2,97.5 C220.0,109.7 196.2,125.8 170.2,128.8 C144.3,131.8 106.3,122.8 80.4,115.4 C54.6,107.9 21.1,96.1 15.1,84.0 C9.2,71.9 29.0,54.7 44.9,42.8 C60.7,30.8 84.9,15.1 110.3,12.3 C135.8,9.5 171.6,18.8 197.7,26.1 C223.8,33.3 260.8,43.9 267.2,55.8 C273.6,67.7 252.3,85.3 236.2,97.5 Z'); }

/* Variante 2 · Pfeil: polygon() mit 6 Punkten, Rechteck → Pfeilspitze */
.knopf.spitze { width: clamp(270px, 26vw, 380px); height: clamp(86px, 8vw, 112px); font-size: clamp(18px, 1.5vw, 22px); }
.knopf.spitze .form { clip-path: polygon(0 0, 100% 0, 100% 50%, 100% 100%, 0 100%, 0 50%); }
.knopf.spitze:hover .form, .knopf.spitze.an .form, .knopf.spitze:focus-visible .form { clip-path: polygon(0 0, calc(100% - 46px) 0, 100% 50%, calc(100% - 46px) 100%, 0 100%, 30px 50%); }
.knopf.spitze:hover .text, .knopf.spitze.an .text, .knopf.spitze:focus-visible .text { translate: 8px 0; }

/* Variante 3 · Wölbung: ellipse(100% 50%) → ellipse(80% 50%) auf einer Showreel-Fläche */
.knopf.reel { width: clamp(300px, 30vw, 440px); aspect-ratio: 11 / 6; overflow: hidden; }
.knopf.reel .form { clip-path: ellipse(100% 50%); overflow: hidden; display: flex; align-items: center; }
.knopf.reel:hover .form, .knopf.reel.an .form, .knopf.reel:focus-visible .form { clip-path: ellipse(80% 50%); }
.band { display: flex; white-space: nowrap; font-weight: 800; font-stretch: 75%; text-transform: uppercase; font-size: clamp(64px, 6.4vw, 96px); line-height: 1; letter-spacing: -.03em; color: var(--grund); animation: laufen 9s linear infinite; }
.band span { padding-right: .3em; }
.band i { font-style: normal; color: var(--signal); transition: color var(--dauer) var(--kurve); }
.knopf.reel:hover .band i, .knopf.reel.an .band i, .knopf.reel:focus-visible .band i { color: var(--weiss); }
@keyframes laufen { to { transform: translateX(-50%); } }
.knopf.reel .text { position: absolute; left: 50%; bottom: 13%; translate: -50% 0; font: 500 11px/1 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; background: var(--grund); color: var(--hell); padding: 7px 11px 6px; border-radius: 99px; white-space: nowrap; }
.knopf.reel .text svg { width: 9px; height: 9px; }

/* Variante gooey · SVG-Goo-Filter (Codrops: Blur 10, Alpha-Matrix 19 −9, feComposite atop) nur auf den Formen.
   Text und Pfeil liegen ungefiltert darüber und bleiben scharf. Ein Tropfen löst sich aus der Pille und wird zur Icon-Blase. */
.knopf.goo { --gk: var(--k); --gh: calc(72px * var(--gk)); --gw: calc(250px * var(--gk)); --gweg: calc(var(--gh) + 12px * var(--gk));
  width: calc(var(--gw) + var(--gweg)); height: var(--gh); place-items: center start; border-radius: calc(var(--gh) / 2); font-size: calc(20px * var(--gk)); }
.goo-form { position: absolute; inset: 0; z-index: -1; filter: url(#goo); }
.goo-pille, .goo-tropfen { position: absolute; top: 0; height: var(--gh); background: var(--hell); transition: background-color var(--farbe), translate var(--dauer) var(--kurve), scale var(--dauer) var(--kurve); }
.goo-pille { left: 0; width: var(--gw); border-radius: calc(var(--gh) / 2); }
.goo-tropfen { left: calc(var(--gw) - var(--gh)); width: var(--gh); border-radius: 50%; }
.knopf.goo .text { padding-left: calc(30px * var(--gk)); }
.goo-icon { position: absolute; top: 0; left: calc(var(--gw) - var(--gh)); width: var(--gh); height: var(--gh); display: grid; place-items: center; color: var(--grund); transition: translate var(--dauer) var(--kurve); }
.goo-icon svg { width: calc(20px * var(--gk)); height: calc(20px * var(--gk)); rotate: -45deg; transition: rotate var(--dauer) var(--kurve); }
.knopf.goo:is(:hover, .an, :focus-visible) :is(.goo-pille, .goo-tropfen) { background-color: var(--signal); }
.knopf.goo:is(:hover, .an, :focus-visible) :is(.goo-tropfen, .goo-icon) { translate: var(--gweg) 0; }
.knopf.goo:is(:hover, .an, :focus-visible) .goo-pille { scale: .985 1; }
.knopf.goo:is(:hover, .an, :focus-visible) .goo-icon svg { rotate: 0deg; }
.knopf.goo:active .goo-tropfen { scale: .92; }
.knopf.goo:focus-visible { outline-offset: 6px; }
/* Rückfall ohne Filter (auch per Schalter): zwei getrennte Formen, die Blase gleitet ohne Brücke hinaus */
[data-goo="aus"] .goo-form { filter: none; }
@media (forced-colors: active) { .goo-form { filter: none; } .knopf.goo { border: 1px solid ButtonText; } .goo-pille, .goo-tropfen { border: 1px solid ButtonText; } }
.goo-defs { position: absolute; width: 0; height: 0; overflow: hidden; }
.unterumschalter[hidden] { display: none; }

/* Fuß der Bühne: Fakten, Umschalter, Werte */
.fakten { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; padding-top: 18px; border-top: 1px solid var(--linie); }
.fakten .frei { color: var(--signal); }
.fakten .frei::before { content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--signal); margin-right: 8px; vertical-align: 1px; }
.fakten p + p { margin-top: 6px; }
.fakten .wert { font-size: 17px; line-height: 1.35; color: var(--hell); font-weight: 400; }
.fakten a { color: inherit; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .2em; }

.steuer { display: flex; justify-content: space-between; align-items: center; gap: 16px 24px; flex-wrap: wrap; padding: 0 clamp(16px, 3.4vw, 48px) 18px; position: relative; z-index: 2; }
.umschalter { display: flex; border: 1px solid var(--linie); border-radius: 999px; padding: 3px; }
.umschalter button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--meta); background: none; border: 0; border-radius: 999px; padding: 7px 13px 6px; cursor: pointer; transition: color .15s, background-color .15s; }
.umschalter button:hover { color: var(--weiss); }
.umschalter button[aria-pressed="true"] { background: var(--hell); color: var(--grund); }
.umschalter button:focus-visible { outline: 1px solid var(--signal); outline-offset: 2px; }
.werte span { white-space: nowrap; }
.werte { text-align: right; color: rgba(242, 242, 242, .54); font-size: 10px; max-width: 74ch; text-transform: none; letter-spacing: .02em; }

/* Riesen-Wortmarke als Abschluss, angeschnitten */
.wortmarke { font-weight: 800; font-stretch: 75%; text-transform: uppercase; font-size: 25.6vw; line-height: .74; letter-spacing: -.045em; color: #191919; white-space: nowrap; text-align: center; height: .5em; overflow: hidden; user-select: none; margin-top: -8px; }

/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 20; width: 22px; height: 22px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }
.vorschau body { overflow: hidden; }
.vorschau .fakten, .vorschau .kopf .mitte, .vorschau .halt { display: none; }
/* Laufband der Variante Wölbung anhalten (WCAG 2.2.2): nur sichtbar, solange die Variante läuft */
.halt { color: var(--meta); background: none; border: 1px solid var(--linie); border-radius: 999px; padding: 7px 13px 6px; cursor: pointer; transition: color .15s; margin-right: auto; }
.halt[hidden] { display: none; }
.halt:hover, .halt[aria-pressed="true"] { color: var(--weiss); }
.halt:focus-visible { outline: 1px solid var(--signal); outline-offset: 2px; }
.still .band { animation-play-state: paused; }
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }

/* Telefon und schmale Fenster */
@media (min-width: 760px) { :root { --k: 1.15; } }
@media (min-width: 760px) and (max-width: 1179px) { .fakten { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1180px) { :root { --k: 1.35; } }
@media (max-width: 759px) {
  .kopf { grid-template-columns: 1fr auto; }
  .kopf .mitte { display: none; }
  .satz { font-size: clamp(3.5rem, 17vw, 6rem); }
  .knopf-ort { display: block; margin: 22px 0 0; top: 0; }
  .fakten { grid-template-columns: 1fr; gap: 14px; }
  .knopf-ort[data-v="blob"] { margin-left: -22px; }
  .band { font-size: 56px; }
  .werte { text-align: left; }
  .wortmarke { font-size: 27vw; }
}

/* Touch: kein Hover. Tippen spielt den Formwechsel kurz ab (Klasse .an, gesetzt per JS) */
@media (hover: none) {
  .knopf:hover .form { background-color: var(--hell); }
  .knopf.an .form { background-color: var(--signal); }
  .knopf.blob:hover:not(.an) .form { clip-path: path('M253.0,77.6 C249.0,88.9 225.6,101.3 203.8,108.6 C182.1,116.0 147.8,123.5 122.4,121.9 C97.1,120.3 67.7,109.0 51.7,99.1 C35.6,89.1 21.7,73.5 25.9,62.3 C30.1,51.1 54.9,39.1 76.9,31.8 C98.8,24.5 132.3,17.0 157.4,18.6 C182.5,20.2 211.5,31.4 227.4,41.2 C243.4,51.1 256.9,66.4 253.0,77.6 Z'); }
  .knopf.spitze:hover:not(.an) .form { clip-path: polygon(0 0, 100% 0, 100% 50%, 100% 100%, 0 100%, 0 50%); }
  .knopf.spitze:hover:not(.an) .text { translate: 0 0; }
  .knopf.reel:hover:not(.an) .form { clip-path: ellipse(100% 50%); }
  .knopf:hover:not(.an) .pfeil { rotate: -45deg; }
  .knopf.goo:hover:not(.an, :focus-visible) :is(.goo-pille, .goo-tropfen) { background-color: var(--hell); }
  .knopf.goo:hover:not(.an, :focus-visible) :is(.goo-tropfen, .goo-icon) { translate: 0 0; }
  .knopf.goo:hover:not(.an, :focus-visible) .goo-pille { scale: 1; }
  .knopf.goo:hover:not(.an, :focus-visible) .goo-icon svg { rotate: -45deg; }
}
@media (max-width: 380px) { .knopf.goo { --gk: .84; } }
.reduziert :is(.goo-pille, .goo-tropfen, .goo-icon, .goo-icon svg) { transition-duration: 150ms !important; transition-timing-function: linear !important; }
.reduziert .goo-form { filter: none; }

/* Reduzierte Bewegung: Formwechsel ohne Überschwinger in 150 ms, Laufband steht */
.reduziert .knopf .form, .reduziert .knopf .text, .reduziert .knopf .pfeil, .reduziert .band i { transition-duration: 150ms !important; transition-timing-function: linear !important; }
.reduziert .knopf .text { translate: none !important; }
.reduziert .knopf.reel .text { translate: -50% 0 !important; }
.reduziert .band { animation: none; }
.reduziert .zeiger { display: none; }
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#start">Ortspitze<span>.</span></a>
  <p class="mono mitte">Studio für Marke &amp; Bewegung</p>
  <p class="mono rechts">Passau · 48°34′ N 13°28′ O</p>
</header>

<main class="buehne" id="kontakt">
  <div class="satz">
    <span class="mono ueber">(05) Kontakt</span>
    <h2 id="frage">Habt ihr<br>was Lautes<br><span class="leise">vor?</span></h2><span class="knopf-ort" data-ort data-v="blob">
      <a class="knopf blob" href="#anfrage" data-knopf="blob" aria-describedby="frage">
        <span class="form" aria-hidden="true"></span>
        <span class="text">Projekt anfragen <svg class="pfeil" viewBox="0 0 18 18" aria-hidden="true"><path d="M2 9h13M10 4l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2"/></svg></span>
      </a>
      <a class="knopf spitze" href="#arbeiten" data-knopf="spitze" hidden>
        <span class="form" aria-hidden="true"></span>
        <span class="text">Alle 48 Arbeiten <svg class="pfeil" viewBox="0 0 18 18" aria-hidden="true"><path d="M2 9h13M10 4l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2"/></svg></span>
      </a>
      <a class="knopf goo" href="#anfrage" data-knopf="goo" aria-describedby="frage" hidden>
        <span class="goo-form" aria-hidden="true"><span class="goo-pille"></span><span class="goo-tropfen"></span></span>
        <span class="text">Projekt anfragen</span>
        <span class="goo-icon" aria-hidden="true"><svg viewBox="0 0 18 18"><path d="M2 9h13M10 4l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2"/></svg></span>
      </a>
      <a class="knopf reel" href="#showreel" data-knopf="reel" aria-label="Showreel 2026 abspielen, 1 Minute 12 Sekunden" hidden>
        <span class="form" aria-hidden="true"><span class="band"><span>Reel <i>2026</i></span><span>Reel <i>2026</i></span><span>Reel <i>2026</i></span><span>Reel <i>2026</i></span></span></span>
        <span class="text" aria-hidden="true"><svg viewBox="0 0 10 10"><path d="M1.5 1v8l7-4z" fill="currentColor"/></svg> 01:12 · Abspielen</span>
      </a>
    </span>
  </div>

  <div class="fakten">
    <div><p class="mono">Erstgespräch</p><p class="wert">30 Minuten, kostenlos. Bei uns in der Höllgasse oder per Video.</p></div>
    <div><p class="mono">Schreibt uns</p><p class="wert"><a href="#mail">hallo@ortspitze.studio</a><br>Antwort innerhalb von zwei Werktagen</p></div>
    <div><p class="mono">Studio</p><p class="wert">Höllgasse 9, 94032 Passau<br>Zwischen Donau, Inn und Ilz</p></div>
    <div><p class="mono frei">Frei ab Januar 2027</p><p class="wert">Noch zwei Plätze für Marke, Film oder Website</p></div>
  </div>
</main>

<div class="steuer">
  <div class="umschalter mono" role="group" aria-label="Variante">
    <button type="button" data-v-wahl="basis" aria-pressed="true">Clip-Path</button>
    <button type="button" data-v-wahl="gooey" aria-pressed="false">Gooey</button>
  </div>
  <div class="umschalter mono unterumschalter" role="group" aria-label="Form" data-form-gruppe>
    <button type="button" data-variante="blob" aria-pressed="true">Blob</button>
    <button type="button" data-variante="spitze" aria-pressed="false">Pfeil</button>
    <button type="button" data-variante="reel" aria-pressed="false">Wölbung</button>
  </div>
  <div class="umschalter mono unterumschalter" role="group" aria-label="Filter" data-goo-gruppe hidden>
    <button type="button" data-goo-wahl="an" aria-pressed="true">SVG-Filter</button>
    <button type="button" data-goo-wahl="aus" aria-pressed="false">Rückfall</button>
  </div>
  <button type="button" class="halt mono" data-halt aria-pressed="false" hidden>Laufband anhalten</button>
  <p class="mono werte" data-werte></p>
</div>
<div class="wortmarke" aria-hidden="true">Ortspitze</div>
<svg class="goo-defs" aria-hidden="true" focusable="false"><defs><filter id="goo" x="-20%" y="-60%" width="140%" height="220%" color-interpolation-filters="sRGB">
  <feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur"/>
  <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 19 -9" result="goo"/>
  <feComposite in="SourceGraphic" in2="goo" operator="atop"/>
</filter></defs></svg>

<svg class="zeiger" data-zeiger viewBox="0 0 18 18" aria-hidden="true"><path d="M2 1.5 15.5 9.2l-6 1.3-2.9 5.6Z" fill="#0b0b0b" stroke="#f2f2f2" stroke-width="1.2" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const ort = document.querySelector('[data-ort]');
  const knoepfe = { blob: null, spitze: null, reel: null, goo: null };
  document.querySelectorAll('[data-knopf]').forEach(k => { knoepfe[k.dataset.knopf] = k; });
  const schalter = [...document.querySelectorAll('[data-variante]')];
  const werteEl = document.querySelector('[data-werte]');
  const zeiger = document.querySelector('[data-zeiger]');
  const halt = document.querySelector('[data-halt]');

  // Die tatsächlich verwendeten Werte je Variante (gemessen bzw. aus dem Quell-CSS gelesen)
  const VARIANTEN = {
    blob:   { kurve: 'cubic-bezier(0.585, 2.5, 0.645, 0.55)', dauer: 500, farbe: '200ms linear', text: 'clip-path: path() → path() · 8 Kurvenpunkte · 500 ms · cubic-bezier(0.585, 2.5, 0.645, 0.55) · federt über · Farbe 200 ms linear' },
    spitze: { kurve: 'cubic-bezier(0.2, 1, 0.8, 1)', dauer: 400, farbe: '400ms cubic-bezier(0.2, 1, 0.8, 1)', text: 'clip-path: polygon() · 6 Punkte · Rechteck → Pfeilspitze 46 px, Kerbe 30 px · 400 ms · cubic-bezier(0.2, 1, 0.8, 1)' },
    reel:   { kurve: 'cubic-bezier(0.65, 0.05, 0, 1)', dauer: 750, farbe: '750ms cubic-bezier(0.65, 0.05, 0, 1)', text: 'clip-path: ellipse(100% 50%) → ellipse(80% 50%) · 750 ms · cubic-bezier(0.65, 0.05, 0, 1) · gemessen' },
    goo:    { kurve: 'cubic-bezier(0.16, 1, 0.3, 1)', dauer: 700, farbe: '200ms linear', text: 'filter: url(#goo) · feGaussianBlur 10 · feColorMatrix Alpha 19 −9 · feComposite atop · Tropfen 72 px wandert 84 px · 700 ms · cubic-bezier(0.16, 1, 0.3, 1)' }
  };
  let variante = 'blob';

  function setzeVariante(v) {
    variante = v;
    const w = VARIANTEN[v];
    document.body.style.setProperty('--kurve', w.kurve);
    document.body.style.setProperty('--dauer', w.dauer + 'ms');
    document.body.style.setProperty('--farbe', w.farbe);
    ort.dataset.v = v;
    Object.entries(knoepfe).forEach(([n, k]) => { k.hidden = n !== v; k.classList.remove('an'); });
    schalter.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
    halt.hidden = v !== 'reel' || reduziert();
    let t = reduziert() ? 'Reduzierte Bewegung · gleicher Formwechsel · 150 ms linear · ohne Überschwinger' : w.text;
    if (v === 'goo') t = reduziert() ? 'Reduzierte Bewegung · ohne Filter · Blase gleitet in 150 ms linear hinaus' : html.dataset.goo === 'aus' ? 'Rückfall ohne Filter · zwei Formen, keine Brücke · 700 ms · cubic-bezier(0.16, 1, 0.3, 1)' : w.text;
    werteEl.innerHTML = t.split(' · ').map(x => `<span>${x}</span>`).join(' · ');
  }
  schalter.forEach(b => b.addEventListener('click', () => { neustart(); setzeVariante(b.dataset.variante); }));

  // Varianten nach Regel 13: ?v=basis (Clip-Path: Blob, Pfeil, Wölbung) | gooey (SVG-Filter mit Rückfall)
  const WELTEN = ['basis', 'gooey'];
  const vKnoepfe = [...document.querySelectorAll('[data-v-wahl]')], gKnoepfe = [...document.querySelectorAll('[data-goo-wahl]')];
  const formGruppe = document.querySelector('[data-form-gruppe]'), gooGruppe = document.querySelector('[data-goo-gruppe]');
  let welt = 'basis', letzteForm = 'blob';
  function setzeWelt(w, url = true) {
    welt = w; html.dataset.v = w;
    vKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === w)));
    formGruppe.hidden = w === 'gooey'; gooGruppe.hidden = w !== 'gooey';
    if (url) { const u = new URL(location.href); u.searchParams.set('v', w); history.replaceState(history.state, '', u); }
    if (variante !== 'goo') letzteForm = variante;
    setzeVariante(w === 'gooey' ? 'goo' : letzteForm);
  }
  function setzeGoo(g) { html.dataset.goo = g; gKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.gooWahl === g))); setzeVariante(variante); }
  vKnoepfe.forEach(b => b.addEventListener('click', () => { neustart(); setzeWelt(b.dataset.vWahl); }));
  gKnoepfe.forEach(b => b.addEventListener('click', () => { neustart(); setzeGoo(b.dataset.gooWahl); }));

  // Touch: Tippen spielt den Formwechsel einmal ab, dann federt die Form zurück
  let tippUhr = null;
  Object.values(knoepfe).forEach(k => {
    k.addEventListener('pointerdown', e => {
      if (e.pointerType === 'mouse') return;
      clearTimeout(tippUhr);
      k.classList.add('an');
      tippUhr = setTimeout(() => k.classList.remove('an'), VARIANTEN[variante].dauer + 450);
    });
    k.addEventListener('click', e => e.preventDefault());
  });

  // Vorführung: ein gedachter Zeiger fährt auf den Knopf, bleibt, geht wieder; danach nächste Variante
  const REIHE = ['blob', 'spitze', 'reel'];
  const glatt = t => 1 - Math.pow(1 - t, 3);
  let uhr = null, start = 0, runde = 0, war = '';
  const ziel = () => { const r = knoepfe[variante].getBoundingClientRect(); return { x: r.left + r.width * 0.56, y: r.top + r.height * 0.58 }; };
  const ruhe = () => ({ x: innerWidth * 0.86, y: innerHeight * 0.9 });

  function schritt(t) {
    const p = (t - start) / 1000;              // Sekunden in dieser Runde
    const a = ruhe(), b = ziel();
    let x, y, an = false;
    if (p < 0.3) { x = a.x; y = a.y; }
    else if (p < 1.0) { const q = glatt((p - 0.3) / 0.7); x = a.x + (b.x - a.x) * q; y = a.y + (b.y - a.y) * q; }
    else if (p < 2.8) { x = b.x + Math.sin((p - 1) * 2.4) * 6; y = b.y + Math.cos((p - 1) * 2) * 4; an = true; }
    else if (p < 3.5) { const q = glatt((p - 2.8) / 0.7); x = b.x + (a.x - b.x) * q; y = b.y + (a.y - b.y) * q; }
    else { x = a.x; y = a.y; }
    zeiger.style.transform = `translate(${x - 2}px, ${y - 1}px)`;
    const k = knoepfe[variante];
    if (an !== k.classList.contains('an')) k.classList.toggle('an', an);
    if (p >= 4.2 && welt === 'gooey') {          // gooey: abwechselnd mit Filter und Rückfall
      setzeGoo(html.dataset.goo === 'aus' ? 'an' : 'aus'); start = t;
    } else if (p >= 4.2) {                       // Runde vorbei: nächste Variante
      runde = (runde + 1) % REIHE.length;
      setzeVariante(REIHE[runde]);
      start = t;
    }
    uhr = requestAnimationFrame(schritt);
  }
  function vorfuehren() {
    pause(); still(false);
    html.classList.add('vorfuehrung');
    runde = Math.max(0, REIHE.indexOf(variante));
    if (welt === 'gooey') setzeGoo('an');
    start = performance.now();
    uhr = requestAnimationFrame(schritt);
  }
  function pause() { cancelAnimationFrame(uhr); uhr = null; }
  // Knopf „Laufband anhalten“: dieselbe Pause wie FUNDUS_DEMO.pause(), dazu steht das Laufband
  function still(an) { html.classList.toggle('still', an); halt.setAttribute('aria-pressed', String(an)); halt.textContent = an ? 'Laufband abspielen' : 'Laufband anhalten'; }
  halt.addEventListener('click', () => { const an = !html.classList.contains('still'); still(an); if (an) pause(); else weiter(); });
  function weiter() { if (!uhr && html.classList.contains('vorfuehrung')) { still(false); vorfuehren(); } }
  function neustart() { pause(); html.classList.remove('vorfuehrung'); Object.values(knoepfe).forEach(k => k.classList.remove('an')); }

  html.dataset.goo = 'an';
  const ausUrl = new URLSearchParams(location.search).get('v');
  setzeWelt(WELTEN.includes(ausUrl) ? ausUrl : WELTEN[0], false);
  let warReduziert = reduziert();
  new MutationObserver(() => { if (reduziert() !== warReduziert) { warReduziert = reduziert(); setzeVariante(variante); } }).observe(html, { attributes: true, attributeFilter: ['class'] });

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