Abschluss-Footer mit großem CTA und Riesen-Wortmarke
Großer Abschluss mit direkter Ansprache, E-Mail zum Kopieren und randloser, unten angeschnittener Wortmarke, die sich aufbaut, aufgedeckt wird oder dem Zeiger folgt.
Die Seite endet mit einer bildschirmfüllenden Abschlusssektion. Oben steht eine große, direkte Ansprache mit der E-Mail-Adresse als riesigem Link und einem klaren Kontakt-Knopf. Daneben liegen kleine Spalten mit Adresse, Telefon, Social-Links und Newsletter. Am unteren Rand läuft die Wortmarke über die volle Breite und ist unten angeschnitten.
Die Seite bekommt einen Schlussakkord. Die Marke tritt am Ende noch einmal groß auf, wie im Abspann eines Films. Der CTA ist unübersehbar, und das Seitenende wird vom Abstellraum zum Konversionspunkt. Typografisch stark, persönlich statt formularhaft und günstig umzusetzen.
Wann einsetzen
Bei Studios, Agenturen, Freelancern, Dienstleistern, Schriftanbietern und Events, also überall, wo Kontaktaufnahme das Ziel ist. Am besten mit kurzer, prägnanter Wortmarke.
Wann nicht
Bei langen Firmennamen ohne Kurzmarke, die Schrift wird sonst winzig oder bricht um. Bei schwacher Wortmarke. Wenn viele Pflicht-Links nötig sind und der Platz fehlt (dann zweistufig). Bei Shops und Produkten mit anderem Hauptziel, dort Newsletter oder Service-Links nach vorn.
Varianten
- Statische Wortmarke, unten angeschnitten
- Buchstaben-Aufbau beim Hereinscrollen (Stagger aus der Kante)
- Vorhang-Footer: klebt hinter dem Inhalt und wird aufgedeckt
- Wortmarke reagiert auf Cursor oder Scroll (Anheben, Gewicht, Breite)
- Outline/Fill-Wechsel beim Hover
- Wortmarke als SVG-Logo statt Text
- Riesige E-Mail-Adresse als CTA, mit Kopieren-Knopf
- Headline, große E-Mail und runder Magnet-Knopf
- Headline, New-Business-E-Mail und Newsletter
- Kontakt-Tabs New Business / Allgemein
- Laufband mit großem CTA im Footer
- Markenclaim als Schlusssatz
Umsetzung
Footer als Grid: oben Ansprache als h2 (clamp(3rem, 10vw, 12rem) oder kleiner), darunter E-Mail-Link und Knopf, daneben Spalten, dann Rechtliches, ganz unten die Wortmarke.
Wortmarke randlos: Text per JS auf Breite rechnen (bei 100px messen, font-size = Breite / Messwert × 100, bei Resize neu) oder inline-SVG mit passender viewBox und width: 100%. Alternativ font-size in cqi mit empirischem Faktor je Schrift. Künftig nativ mit text-fit: grow (experimentell, mit @supports absichern). Anschnitt: Container mit overflow: hidden und fester Höhe in em (etwa 0.7 × Schriftgröße).
Aufbau: Buchstaben einzeln in Spans, yPercent 100 → 0, expo.out 1000–1400 ms, Stagger 30–60 ms, ScrollTrigger start 'top 85%'. Überschrift mit SplitText lines und mask: 'lines'. Ligaturen prüfen, wenn per Buchstabe geteilt wird.
Vorhang: Footer position: sticky; bottom: 0 hinter main, main mit z-index und eigener Hintergrundfarbe. Funktioniert nur, wenn der Footer nicht höher als der Viewport ist.
E-Mail: <a href="mailto:"> plus Kopieren-Knopf mit navigator.clipboard.writeText und kurzem „Kopiert“ in aria-live=polite. Mailto allein frustriert Nutzer ohne Mail-Programm. Magnet-Knopf: pointermove-Versatz × 0.3–0.4 mit gsap.quickTo, beim Verlassen elastisch zurück.
Robust umsetzen
Telefon. Die Wortmarke bleibt randlos und wird klein, ist aber als Bild lesbar. Überschrift 12–16vw, E-Mail umbrechbar (overflow-wrap: anywhere), Knopf volle Breite und mindestens 44 px hoch. Den Vorhang nur einsetzen, wenn der Footer kürzer als der Viewport ist. Bei langen Namen zwei Zeilen oder Bildmarke.
Ohne Bewegung. Wortmarke und Überschrift statisch anzeigen: kein Buchstaben-Aufbau, kein Zeilen-Reveal, kein Magnet, keine Cursor-Reaktion.
Barrierefrei. Die Ansprache ist eine echte Überschrift (h2), nicht h1. Die Wortmarke ist dekorativ und steht im DOM nach den Footer-Inhalten: role="img" mit aria-label, die Buchstaben-Spans aria-hidden, oder ganz aria-hidden, wenn die Marke im Header schon genannt ist. Footer-Links in <nav aria-label="…">. E-Mail als mailto mit sichtbarem Text, Kopier-Feedback per aria-live ansagen, Knopfziel klar benennen.
Code
<!DOCTYPE html>
<!--
Abschluss-Footer mit großem CTA und Riesen-Wortmarke
Fundus · Footer · https://cecodesign.de/lab/fundus/footer/footer-cta-riesen-wortmarke/
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>Abschluss-Footer mit großem CTA und Riesen-Wortmarke · 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 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: '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: '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; }
@font-face { font-family: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; font-display: swap; }
:root {
--papier: #ebe5d9; --tinte: #171411; --tinte-leise: rgba(23, 20, 17, .66); --tinte-linie: rgba(23, 20, 17, .14);
--grund: #171411; --text: #f2ede3; --leise: rgba(242, 237, 227, .56); --linie: rgba(242, 237, 227, .14);
--akzent: #ff5b2e;
--kurve: cubic-bezier(0.16, 1, 0.3, 1); /* expo.out */
--leiste: 56px; /* Höhe der Demo-Leiste, per JS gemessen */
--rand: clamp(18px, 3.2vw, 44px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { overflow-x: clip; }
a { color: inherit; text-decoration: none; }
img { display: block; max-width: 100%; height: auto; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.weg { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* ---------- Demo-Leiste (nicht Teil der erfundenen Seite) ---------- */
.steuer { position: fixed; inset: 0 0 auto; z-index: 20; display: flex; align-items: center; gap: 10px 24px; flex-wrap: wrap;
padding: 12px var(--rand); color: var(--leise); background: rgba(23, 20, 17, .9); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); border-bottom: 1px solid var(--linie); }
.steuer-titel { color: var(--text); }
.werte { flex: 1 1 320px; display: flex; flex-wrap: wrap; gap: 4px 16px; }
.werte b { color: var(--text); font-weight: 500; }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; transition: color .15s, background-color .15s, border-color .15s; }
.modi button:hover { color: var(--text); border-color: rgba(242, 237, 227, .32); }
.modi button[aria-pressed='true'] { background: var(--text); color: var(--grund); border-color: var(--text); }
.modi button:focus-visible, .fuss a:focus-visible, .fuss button:focus-visible, .fuss input:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; }
/* ---------- Letzter Inhaltsabschnitt der Seite ---------- */
.seite { position: relative; z-index: 1; background: var(--papier); color: var(--tinte); }
.schluss { min-height: 72svh; padding: calc(var(--leiste) + clamp(28px, 5vw, 64px)) var(--rand) clamp(28px, 4vw, 48px); display: grid; grid-template-rows: auto 1fr auto; gap: clamp(20px, 3vw, 36px); }
.schluss-kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; color: var(--tinte-leise); border-bottom: 1px solid var(--tinte-linie); padding-bottom: 14px; }
.schluss-kopf h2 { font: 600 clamp(26px, 3.4vw, 48px)/1 'Instrument Sans', sans-serif; letter-spacing: -.035em; text-transform: none; color: var(--tinte); }
.schluss-kopf h2 em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: -.01em; }
.arbeiten { display: grid; grid-template-columns: 1.25fr 1fr 1fr; gap: clamp(12px, 1.6vw, 24px); align-items: start; }
.arbeit figure { overflow: hidden; border-radius: 4px; background: #ddd5c6; }
.arbeit img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; transition: transform .9s var(--kurve); }
.arbeit:hover img { transform: scale(1.035); }
.arbeit:nth-child(2) { margin-top: clamp(24px, 5vw, 72px); }
.arbeit:nth-child(3) { margin-top: clamp(10px, 2vw, 28px); }
.arbeit figcaption { display: flex; justify-content: space-between; gap: 12px; padding-top: 10px; font-size: 14px; }
.arbeit figcaption span:last-child { color: var(--tinte-leise); }
.ende { display: flex; justify-content: space-between; color: var(--tinte-leise); }
/* ---------- Der Footer ---------- */
.fuss { position: relative; z-index: 0; min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto auto; background: var(--grund); color: var(--text); overflow: hidden; }
.fuss-kopf { display: flex; justify-content: space-between; gap: 8px 24px; flex-wrap: wrap; color: var(--leise); padding: calc(var(--leiste) + 20px) var(--rand) 0; }
.fuss-kopf .frei::before { content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--akzent); margin-right: 9px; vertical-align: 1px; box-shadow: 0 0 0 4px rgba(255, 91, 46, .18); }
.fuss-haupt { align-self: start; display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: 36px 6vw; align-items: start; padding: clamp(22px, 4vh, 56px) var(--rand) clamp(26px, 5vh, 64px); }
.cta { position: relative; }
.ansprache { font-size: clamp(42px, 6vw, 112px); font-weight: 600; line-height: .94; letter-spacing: -.048em; max-width: 12ch; padding-bottom: clamp(18px, 2.6vw, 34px); }
.ansprache em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: -.02em; color: var(--akzent); padding-right: .05em; }
.zeile-mask { padding-block: .08em; margin-block: -.08em; }
.js .ansprache { visibility: hidden; }
.mail-zeile { position: relative; display: flex; align-items: center; flex-wrap: wrap; gap: 12px 18px; border-top: 1px solid var(--linie); padding-top: clamp(16px, 2vw, 26px); padding-right: clamp(120px, 13vw, 190px); }
.mail { font-size: clamp(20px, 2.5vw, 38px); font-weight: 500; letter-spacing: -.025em; overflow-wrap: anywhere; background: linear-gradient(currentColor, currentColor) 0 100% / 100% 1px no-repeat; padding-bottom: 3px; transition: background-size .5s var(--kurve), color .15s; }
.mail:hover { color: var(--akzent); background-size: 0 1px; background-position: 100% 100%; }
.kopieren { color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 32px; padding: 0 12px; cursor: pointer; transition: color .15s, border-color .15s; }
.kopieren:hover { color: var(--text); border-color: rgba(242, 237, 227, .34); }
.status { color: var(--akzent); min-width: 8ch; }
/* Runder Magnet-Knopf sitzt auf der Trennlinie */
.magnet { position: absolute; right: 0; top: 0; translate: 0 -50%; width: clamp(108px, 11.5vw, 168px); aspect-ratio: 1; border-radius: 50%; display: grid; place-items: center; text-align: center;
background: var(--akzent); color: var(--tinte); font-size: clamp(14px, 1.2vw, 17px); font-weight: 600; line-height: 1.1; letter-spacing: -.01em; will-change: transform; }
.magnet span { display: block; pointer-events: none; }
.magnet::after { content: ''; position: absolute; inset: 0; border-radius: inherit; background: var(--text); transform: scale(0); transition: transform .5s var(--kurve); z-index: -1; }
.magnet { isolation: isolate; }
.magnet:hover::after { transform: scale(1); }
.spalten { display: grid; grid-template-columns: 1fr 1fr; gap: 28px clamp(16px, 2.4vw, 40px); font-size: 15px; line-height: 1.5; }
.spalten h3 { color: var(--leise); margin-bottom: 10px; font-weight: 500; }
.spalten ul { list-style: none; }
.spalten a { transition: color .15s; }
.spalten li a { display: inline-block; padding-block: 3px; margin-block: -3px; } /* Zielgröße ≥ 24 px ohne neue Abstände */
.spalten a:hover { color: var(--akzent); }
.spalten address { font-style: normal; }
.logbuch p { color: var(--leise); font-size: 14px; margin-bottom: 10px; }
.logbuch form { display: flex; border-bottom: 1px solid rgba(242, 237, 227, .34); }
.logbuch input { flex: 1; min-width: 0; font: inherit; color: var(--text); background: none; border: 0; padding: 6px 0; }
.logbuch input::placeholder { color: var(--leise); }
.logbuch button { font: inherit; color: var(--text); background: none; border: 0; cursor: pointer; padding: 6px 0 6px 10px; transition: color .15s, translate .3s var(--kurve); }
.logbuch button:hover { color: var(--akzent); translate: 3px 0; }
.recht { position: relative; z-index: 2; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px 24px; color: var(--leise); margin: 0 var(--rand); padding: 16px 0; border-top: 1px solid var(--linie); }
.recht nav { display: flex; gap: 22px; }
.recht a:hover { color: var(--text); }
/* Riesen-Wortmarke: randlos, unten angeschnitten, Schriftgröße per JS auf Breite gerechnet.
--luft: Platz nach oben (unsichtbar, per negativem Rand), damit sich Buchstaben in der Variante Cursor heben können. */
.marke { --fs: 26vw; --luft: .16; position: relative; z-index: 1; height: calc(var(--fs) * (var(--sicht, .72) + var(--luft))); margin-top: calc(var(--fs) * var(--luft) * -1); overflow: hidden; touch-action: pan-y; user-select: none; }
.marke-zeile { position: absolute; left: 0; top: calc(var(--fs) * (var(--luft) - .02)); display: inline-flex; white-space: nowrap;
font: 800 var(--fs)/1 'Archivo', 'Instrument Sans', sans-serif; font-stretch: 100%; letter-spacing: -.045em; font-kerning: normal; }
.marke .z { display: inline-block; will-change: transform; }
.marke .z.punkt { color: var(--akzent); }
/* Vorhang: Footer klebt unten hinter dem Inhalt und wird aufgedeckt */
body.vorhang .fuss { position: sticky; bottom: 0; }
body.vorhang .seite { box-shadow: 0 40px 80px rgba(0, 0, 0, .45); }
.fuss::after { content: ''; position: absolute; inset: 0; background: #000; opacity: var(--deck, 0); pointer-events: none; z-index: 3; }
/* Gedachter Zeiger der Vorführung */
.zeiger { position: fixed; left: 0; top: 0; z-index: 30; width: 22px; height: 22px; margin: -11px 0 0 -11px; border: 1.5px solid var(--text); border-radius: 50%; background: rgba(242, 237, 227, .15); pointer-events: none; opacity: 0; transition: opacity .3s; }
.zeiger.an { opacity: 1; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
/* Messung der Wortmarke braucht sofortige Werte (die Regel oben würde font-size um 0,01 ms verzögern) */
:root.reduziert .marke-zeile, :root.reduziert .marke .z { transition: none !important; }
@media (max-width: 1100px) { .steuer-titel { display: none; } }
@media (max-width: 860px) {
.fuss-haupt { grid-template-columns: 1fr; align-items: start; }
.arbeiten { grid-template-columns: 1fr 1fr; }
.arbeit:nth-child(3) { display: none; }
}
@media (max-width: 560px) {
.steuer { gap: 8px 14px; padding-block: 10px; }
.steuer-titel { display: none; }
.werte { order: 2; flex-basis: 100%; font-size: 10px; gap: 2px 12px; }
.schluss-kopf { flex-direction: column; gap: 8px; }
.arbeiten { grid-template-columns: 1fr; }
.arbeit:nth-child(2) { margin-top: 0; }
.arbeit:nth-child(2) ~ .arbeit, .arbeit:nth-child(2) { display: none; }
.ansprache { font-size: clamp(42px, 13.5vw, 64px); }
.mail-zeile { padding-right: 0; }
.magnet { position: relative; translate: none; width: 100%; aspect-ratio: auto; height: 56px; border-radius: 999px; margin-top: 6px; }
.magnet br { display: none; }
.spalten { font-size: 14px; gap: 24px 16px; }
.fuss-kopf .spaet { display: none; }
}
@media (hover: none) {
.arbeit:hover img { transform: none; }
}
/* Vorschau in der Galerie: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .ende { visibility: hidden; }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } .logbuch input { border: 1px solid CanvasText !important; } }
</style>
</head>
<body>
<div class="steuer mono" data-steuer>
<p class="steuer-titel">Footer · Schlussakkord</p>
<p class="werte" data-werte aria-live="polite"></p>
<div class="modi" role="group" aria-label="Variante">
<button type="button" data-modus="aufbau" aria-pressed="true">Aufbau</button>
<button type="button" data-modus="vorhang" aria-pressed="false">Vorhang</button>
<button type="button" data-modus="cursor" aria-pressed="false">Cursor</button>
</div>
</div>
<main class="seite">
<section class="schluss" aria-labelledby="arbeiten-titel">
<div class="schluss-kopf mono">
<h2 id="arbeiten-titel">Zuletzt <em>abgeschlossen</em></h2>
<span>Marke · Signet · Editorial</span>
</div>
<div class="arbeiten">
<a class="arbeit" href="#"><figure><img src="bilder/marke-roestkontor.jpg" alt="Signet der Kaffeerösterei Röstkontor: Kaffeebohne im Kreis über cremefarbenen Versalien auf Ziegelrot" width="1200" height="800"></figure><figcaption><span>Röstkontor</span><span>Signet, 2026</span></figcaption></a>
<a class="arbeit" href="#"><figure><img src="bilder/marke-krume.jpg" alt="Wortmarke der Backstube Krume: fette dunkelbraune Kleinbuchstaben unter einem Brotlaib auf Senfgelb" width="1200" height="800"></figure><figcaption><span>Krume</span><span>Marke, 2025</span></figcaption></a>
<a class="arbeit" href="#"><figure><img src="bilder/magazin-doppelseite.jpg" alt="Aufgeschlagenes Magazin: Doppelseite „Die Ruhe der Dinge“ mit Keramikfoto auf Terrakotta" width="1200" height="800"></figure><figcaption><span>Stillstand, Heft 3</span><span>Editorial, 2025</span></figcaption></a>
</div>
<p class="ende mono"><span>Alle 48 Projekte ansehen</span><span>Weiter ↓</span></p>
</section>
</main>
<footer class="fuss" data-fuss>
<div class="fuss-kopf mono">
<span class="frei">Freie Kapazität ab Februar 2027</span>
<span class="spaet">Werft · Studio für Marke und Gestaltung</span>
<span>Hamburg <time data-uhr>--:--</time></span>
</div>
<div class="fuss-haupt">
<div class="cta">
<h2 class="ansprache" data-ansprache>Lass uns etwas <em>Bleibendes</em> bauen.</h2>
<div class="mail-zeile">
<a class="mail" href="mailto:hallo@werft-studio.de">hallo@werft-studio.de</a>
<button type="button" class="kopieren mono" data-kopieren>Adresse kopieren</button>
<span class="status mono" data-status aria-live="polite"></span>
<a class="magnet" href="mailto:hallo@werft-studio.de?subject=Projektanfrage" data-magnet><span>Projekt<br> anfragen</span></a>
</div>
</div>
<div class="spalten">
<div>
<h3 class="mono">Studio</h3>
<address>Werft GmbH<br>Kaiweg 4<br>20457 Hamburg</address>
</div>
<div>
<h3 class="mono">Direkt</h3>
<ul><li><a href="tel:+49403069420">040 306 94 20</a></li><li><a href="mailto:presse@werft-studio.de">presse@werft-studio.de</a></li><li><a href="#">Jobs (2 offen)</a></li></ul>
</div>
<nav aria-label="Soziale Netzwerke">
<h3 class="mono">Folgen</h3>
<ul><li><a href="#">Instagram</a></li><li><a href="#">LinkedIn</a></li><li><a href="#">Behance</a></li></ul>
</nav>
<div class="logbuch">
<h3 class="mono">Logbuch</h3>
<p>Viermal im Jahr: neue Arbeiten, keine Werbung.</p>
<form onsubmit="event.preventDefault(); this.querySelector('input').value=''; this.querySelector('input').placeholder='Danke, bis bald.'">
<label class="weg" for="logbuch-mail">E-Mail für das Logbuch</label>
<input id="logbuch-mail" type="email" placeholder="Deine E-Mail" autocomplete="email" required>
<button type="submit" aria-label="Logbuch abonnieren">→</button>
</form>
</div>
</div>
</div>
<div class="recht mono">
<span>© 2026 Werft GmbH</span>
<nav aria-label="Rechtliches"><a href="#">Impressum</a><a href="#">Datenschutz</a><a href="#" data-hoch>Nach oben ↑</a></nav>
</div>
<div class="marke" data-marke role="img" aria-label="Werft">
<div class="marke-zeile" aria-hidden="true"><span class="z">w</span><span class="z">e</span><span class="z">r</span><span class="z">f</span><span class="z">t</span><span class="z punkt">.</span></div>
</div>
</footer>
<div class="zeiger" data-zeiger aria-hidden="true"></div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/SplitText.min.js"></script>
<script>
(() => {
gsap.registerPlugin(ScrollTrigger, SplitText);
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s);
// Alle Werte an einer Stelle. Kurve überall expo.out = cubic-bezier(0.16, 1, 0.3, 1).
const W = {
buchstaben: { dauer: 1.2, versatz: 0.045, von: 100 }, // yPercent 100 → 0
zeilen: { dauer: 1.0, versatz: 0.08, von: 105 },
start: 'top 85%', // ScrollTrigger am Footer
anschnitt: 0.72, // sichtbare Höhe = 0,72 × Schriftgröße
anschnittMin: 0.56, // Untergrenze, wenn der Viewport niedrig ist
hub: 12, breite: 0.9, lerp: 0.12, // Cursor: max. −12 yPercent, Gauß-Sigma in Buchstaben
magnet: 0.35, magnetDauer: 0.5, // Versatz-Faktor, quickTo-Dauer
};
const fuss = $('[data-fuss]'), marke = $('[data-marke]'), zeile = marke.querySelector('.marke-zeile');
const zeichen = [...marke.querySelectorAll('.z')];
const ansprache = $('[data-ansprache]'), werte = $('[data-werte]'), zeiger = $('[data-zeiger]');
let modus = 'aufbau', schrift = 0, sicht = 0.72, vorhangPasst = true, teilung = null, zeilen = [];
/* ---- Wortmarke auf volle Breite rechnen (Fit-to-width) ---- */
function einpassen() {
zeile.style.fontSize = '100px';
const b = zeile.getBoundingClientRect().width;
zeile.style.fontSize = '';
schrift = Math.round(marke.clientWidth / b * 100 * 10) / 10;
marke.style.setProperty('--fs', schrift + 'px');
// Anschnitt: Standard 0,72 em. Reicht die Höhe nicht, bis 0,56 em nachgeben, damit der Footer in den Viewport passt.
const rest = ['.fuss-kopf', '.fuss-haupt', '.recht'].reduce((n, s) => n + $(s).offsetHeight, 0) + 16;
const frei = (innerHeight - rest) / schrift;
sicht = frei >= W.anschnitt ? W.anschnitt : frei >= W.anschnittMin ? Math.floor(frei * 100) / 100 : W.anschnitt;
marke.style.setProperty('--sicht', sicht);
}
/* ---- Demo-Leiste messen, damit sie nichts verdeckt ---- */
function leisteMessen() { html.style.setProperty('--leiste', $('[data-steuer]').offsetHeight + 'px'); }
/* ---- Vorhang nur, wenn der Footer in den Viewport passt ---- */
function vorhangPruefen() {
vorhangPasst = fuss.offsetHeight <= innerHeight + 1;
document.body.classList.toggle('vorhang', modus === 'vorhang' && vorhangPasst);
}
function zeigeWerte() {
const fs = `<span>Wortmarke <b>${Math.round(schrift)} px</b> fit</span><span>sichtbar <b>${String(sicht).replace('.', ',')} em</b></span>`;
let t;
if (modus === 'aufbau') t = reduziert()
? '<span>Reduziert: <b>statisch, kein Stagger</b></span>'
: `<span><b>expo.out ${W.buchstaben.dauer * 1000} ms</b></span><span>Versatz <b>${W.buchstaben.versatz * 1000} ms</b></span><span><b>yPercent 100 → 0</b></span>`;
else if (modus === 'vorhang') t = vorhangPasst
? '<span><b>sticky; bottom: 0</b></span><span>Abdunkeln <b>0,6 → 0</b> scrub</span>'
: '<span>Vorhang <b>aus</b>: Footer höher als der Viewport</span>';
else t = reduziert()
? '<span>Reduziert: <b>Wortmarke statisch</b></span>'
: `<span>Hub <b>−${W.hub} yPercent</b></span><span>Gauß <b>σ ${String(W.breite).replace('.', ',')} Zeichen</b></span><span>Lerp <b>${String(W.lerp).replace('.', ',')}</b></span>`;
werte.innerHTML = t + fs;
}
/* ---- Variante Aufbau: Zeilen aus Masken, dann Buchstaben von unten ---- */
function teilen() {
teilung && teilung.revert();
teilung = SplitText.create(ansprache, { type: 'lines', mask: 'lines', linesClass: 'zeile', aria: 'auto' });
zeilen = teilung.lines;
gsap.set(ansprache, { visibility: 'visible' });
}
let aufbauTl = null;
function aufbauVerbergen() {
aufbauTl && aufbauTl.kill();
if (reduziert()) { gsap.set([zeichen, zeilen], { yPercent: 0 }); return; }
gsap.set(zeilen, { yPercent: W.zeilen.von });
gsap.set(zeichen, { yPercent: W.buchstaben.von });
}
function aufbauSpielen() {
aufbauTl && aufbauTl.kill();
if (reduziert()) { gsap.set([zeichen, zeilen], { yPercent: 0 }); return gsap.timeline(); }
aufbauTl = gsap.timeline()
.fromTo(zeilen, { yPercent: W.zeilen.von }, { yPercent: 0, duration: W.zeilen.dauer, stagger: W.zeilen.versatz, ease: 'expo.out' })
.fromTo(zeichen, { yPercent: W.buchstaben.von }, { yPercent: 0, duration: W.buchstaben.dauer, stagger: W.buchstaben.versatz, ease: 'expo.out' }, 0.15);
return aufbauTl;
}
/* ---- Variante Cursor: Buchstaben in Zeigernähe heben sich aus dem Anschnitt ---- */
const ist = zeichen.map(() => 0);
let zeigerX = null, mitten = [], schritt = 1;
function mittenMessen() {
const r = marke.getBoundingClientRect();
mitten = zeichen.map(z => { const b = z.getBoundingClientRect(); return b.left - r.left + b.width / 2; });
schritt = r.width / zeichen.length;
}
function cursorTakt() {
if (modus !== 'cursor') return;
const r = marke.getBoundingClientRect();
zeichen.forEach((z, i) => {
let ziel = 0;
if (zeigerX !== null && !reduziert()) {
const d = (zeigerX - r.left - mitten[i]) / schritt;
ziel = -W.hub * Math.exp(-(d * d) / (2 * W.breite * W.breite));
}
ist[i] += (ziel - ist[i]) * W.lerp;
if (Math.abs(ist[i]) < 0.01) ist[i] = 0;
gsap.set(z, { yPercent: ist[i] });
});
}
gsap.ticker.add(cursorTakt);
fuss.addEventListener('pointermove', e => { if (!vorf) zeigerX = e.clientX; });
fuss.addEventListener('pointerleave', () => { if (!vorf) zeigerX = null; });
marke.addEventListener('pointerdown', e => { if (!vorf) zeigerX = e.clientX; });
['pointerup', 'pointercancel'].forEach(t => marke.addEventListener(t, e => { if (e.pointerType !== 'mouse' && !vorf) zeigerX = null; }));
/* ---- Variante Vorhang: Abdunkeln beim Aufdecken (scrub) ---- */
let deckTrigger = null;
function deckAufsetzen() {
deckTrigger && deckTrigger.kill(); deckTrigger = null;
fuss.style.setProperty('--deck', 0);
if (modus !== 'vorhang' || !vorhangPasst || reduziert()) return;
deckTrigger = ScrollTrigger.create({
trigger: '.seite', start: 'bottom bottom', end: 'bottom top', scrub: true,
onUpdate: s => fuss.style.setProperty('--deck', (0.6 * (1 - s.progress)).toFixed(3)),
});
fuss.style.setProperty('--deck', (0.6 * (1 - deckTrigger.progress)).toFixed(3));
}
/* ---- Varianten umschalten ---- */
function setzeModus(neu) {
modus = neu;
document.querySelectorAll('[data-modus]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === modus)));
aufbauTl && aufbauTl.kill();
gsap.set([zeichen, zeilen], { yPercent: 0 });
ist.fill(0);
vorhangPruefen();
ScrollTrigger.refresh();
deckAufsetzen();
if (modus === 'aufbau') {
const r = fuss.getBoundingClientRect();
if (r.top < innerHeight * 0.85) { aufbauVerbergen(); aufbauSpielen(); } else aufbauVerbergen();
}
zeigeWerte();
}
document.querySelector('.modi').addEventListener('click', e => {
const b = e.target.closest('[data-modus]');
if (!b) return;
stopp();
setzeModus(b.dataset.modus);
});
/* ---- Magnet-Knopf (nur mit feinem Zeiger) ---- */
const magnet = $('[data-magnet]'), magnetText = magnet.querySelector('span');
if (matchMedia('(hover: hover) and (pointer: fine)').matches) {
const mx = gsap.quickTo(magnet, 'x', { duration: W.magnetDauer, ease: 'expo.out' }), my = gsap.quickTo(magnet, 'y', { duration: W.magnetDauer, ease: 'expo.out' });
const tx = gsap.quickTo(magnetText, 'x', { duration: W.magnetDauer, ease: 'expo.out' }), ty = gsap.quickTo(magnetText, 'y', { duration: W.magnetDauer, ease: 'expo.out' });
magnet.addEventListener('pointermove', e => {
if (reduziert()) return;
const r = magnet.getBoundingClientRect(), dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2);
mx(dx * W.magnet); my(dy * W.magnet); tx(dx * 0.15); ty(dy * 0.15);
});
magnet.addEventListener('pointerleave', () => gsap.to([magnet, magnetText], { x: 0, y: 0, duration: 1.2, ease: 'elastic.out(1, 0.35)', overwrite: true }));
}
/* ---- E-Mail kopieren mit Ansage ---- */
const status = $('[data-status]');
let statusUhr;
$('[data-kopieren]').addEventListener('click', async () => {
const adresse = 'hallo@werft-studio.de';
try { await navigator.clipboard.writeText(adresse); status.textContent = 'Kopiert'; }
catch (e) { status.textContent = 'Bitte markieren'; getSelection().selectAllChildren($('.mail')); }
clearTimeout(statusUhr); statusUhr = setTimeout(() => { status.textContent = ''; }, 1800);
});
$('[data-hoch]').addEventListener('click', e => { e.preventDefault(); scrollTo({ top: 0, behavior: reduziert() ? 'auto' : 'smooth' }); });
/* ---- Ortszeit ---- */
const uhrEl = $('[data-uhr]');
const zeit = () => { uhrEl.textContent = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin' }).format(new Date()); };
zeit(); setInterval(zeit, 15000);
/* ---- Auslöser beim Scrollen (Variante Aufbau) ---- */
function triggerAufsetzen() {
ScrollTrigger.create({
trigger: fuss, start: W.start,
onEnter: () => { if (modus === 'aufbau' && !vorf) aufbauSpielen(); },
onLeaveBack: () => { if (modus === 'aufbau' && !vorf) aufbauVerbergen(); },
});
}
/* ---- Vorführung: Aufbau → Cursor → Vorhang, in Schleife ---- */
let vorf = false, ablauf = null;
const unten = () => scrollTo(0, document.documentElement.scrollHeight);
function zeigerFahrt(tl) {
const p = { x: 0.06 };
const r = () => marke.getBoundingClientRect();
const setze = () => {
const b = r(), x = b.left + b.width * p.x, y = b.top + b.height * 0.42;
zeigerX = x; gsap.set(zeiger, { x, y });
};
tl.call(() => { p.x = 0.06; setze(); zeiger.classList.add('an'); })
.to(p, { x: 0.94, duration: 2.2, ease: 'sine.inOut', onUpdate: setze })
.to(p, { x: 0.32, duration: 1.4, ease: 'sine.inOut', onUpdate: setze })
.call(() => { zeiger.classList.remove('an'); zeigerX = null; }, null, '+=0.5');
}
function vorhangFahrt(tl) {
const s = { y: 0 };
tl.call(() => { scrollTo(0, 0); s.y = 0; }, null, '+=0.1')
.to(s, { y: () => document.documentElement.scrollHeight - innerHeight, duration: reduziert() ? 0 : 2, ease: 'power2.inOut', onUpdate: () => scrollTo(0, s.y) }, '+=0.7');
}
function vorfuehren() {
stopp();
vorf = true; html.classList.add('vorfuehrung');
ablauf = gsap.timeline({ repeat: -1 });
ablauf.call(() => { setzeModus('aufbau'); unten(); aufbauVerbergen(); })
.call(() => { aufbauSpielen(); }, null, '+=0.25')
.call(() => { setzeModus('cursor'); unten(); }, null, '+=3.2');
zeigerFahrt(ablauf);
ablauf.call(() => { setzeModus('vorhang'); }, null, '+=0.2');
vorhangFahrt(ablauf);
ablauf.to({}, { duration: 1.8 });
}
function stopp() {
if (ablauf) { ablauf.kill(); ablauf = null; }
vorf = false; zeigerX = null; zeiger.classList.remove('an');
html.classList.remove('vorfuehrung');
if (!aufbauTl || !aufbauTl.isActive()) gsap.set([zeichen, zeilen], { yPercent: 0 }); // nichts versteckt zurücklassen
}
function neustart() {
stopp();
if (modus === 'vorhang' && vorhangPasst) { const tl = gsap.timeline(); vorhangFahrt(tl); return; }
unten();
if (modus === 'cursor') { vorf = true; const tl = gsap.timeline({ onComplete: () => { vorf = false; } }); zeigerFahrt(tl); return; }
aufbauVerbergen(); aufbauSpielen();
}
// Eingriff des Nutzers beendet die Vorführung (nicht in der Galerie-Vorschau)
['wheel', 'touchstart', 'keydown'].forEach(t => addEventListener(t, () => { if (vorf && !html.classList.contains('vorschau')) stopp(); }, { passive: true }));
/* ---- Start ---- */
function messen() { leisteMessen(); einpassen(); mittenMessen(); vorhangPruefen(); zeigeWerte(); }
const bereit = Promise.all([document.fonts.load('800 100px Archivo'), document.fonts.load('600 40px "Instrument Sans"'), document.fonts.load('italic 40px "Instrument Serif"')]).catch(() => {}).then(() => document.fonts.ready).then(() => {
messen();
teilen();
aufbauVerbergen();
triggerAufsetzen();
ScrollTrigger.refresh();
let breite = innerWidth;
new ResizeObserver(() => {
messen();
if (innerWidth !== breite) { breite = innerWidth; const sichtbar = !zeilen.length || gsap.getProperty(zeilen[0], 'yPercent') === 0; teilen(); if (!sichtbar && modus === 'aufbau') gsap.set(zeilen, { yPercent: W.zeilen.von }); }
deckAufsetzen();
}).observe(document.body);
});
})();
</script>
</body>
</html>