Zum Inhalt
← Hero & Section-Layouts

Übergroßer Schrift-Hero (randlos eingepasste Wortmarke)

Eine Wortmarke füllt die Viewportbreite bis auf die Kante, Buchstaben steigen aus Masken, beim Scrollen wird sie überdeckt, schrumpft ins Header-Logo oder zerfällt in gestaffelte Tempi.

Kreativstudio, Marke mit kurzem Namen: ein Wort über die volle Breite, per JS randlos eingepasst (Einpassen, Übergabe, Gestaffelt).

Der Hero besteht aus einem einzigen kurzen Wort: Studio, Person oder Produkt. Es wird so groß gesetzt, dass es genau die Innenbreite des Viewports füllt, fast ohne Seitenrand. Kleine Begleittexte (Claim, Ort, Jahr, Scroll-Hinweis) stehen in den Ecken oder an Rasterspalten. Beim Laden steigen die Buchstaben einzeln aus einer Maske. Beim Scrollen bleibt die Wortmarke stehen und wird vom Inhalt überdeckt, schrumpft ins Header-Logo oder ihre Buchstaben laufen mit unterschiedlichem Tempo davon.

Der Kontrast zwischen riesig und winzig schafft sofort Hierarchie und wirkt selbstbewusst. Die Schrift ist zugleich Logo, Bild und Layout, ein Foto braucht es nicht. Die Schriftwahl wird zur Markenaussage. Wix nennt das für 2026 „Exaggerated hierarchy“.

Wann einsetzen

Portfolios, Studios, Personal Brands und Launch-Seiten mit kurzem Namen oder starker Wortmarke. Auch als Footer mit riesigem Namen oder als Übergang vom Hero zum Logo in der Navigation.

Wann nicht

Bei langen Namen oder Firmennamen aus mehreren Wörtern: Auf dem Telefon wird die Schrift unlesbar klein. Bei Seiten, auf denen Nutzer sofort Information brauchen (Shop-Startseite, Behörde). Wenn Markenrichtlinien das Skalieren des Logos verbieten.

Varianten

  • Einpassen: Wortmarke steht still (sticky), der Inhalt schiebt sich darüber
  • Logo-Übergabe: Wortmarke schrumpft beim Scrollen von Vollbreite ins Header-Logo (scroll-driven transform oder GSAP Flip)
  • Gestaffelt: Buchstaben laufen beim Scrollen mit unterschiedlichem Tempo
  • Kinetisch: Buchstaben dehnen oder stauchen sich über variable Achsen (wdth, wght)
  • Mehrzeiliger Typo-Block mit gemischten Schnitten (Serif kursiv und Grotesk), zeilenweise eingerückt
  • Riesentypo über Vollbild-Video (siehe cinematic-video-hero) oder als Maske für Bild und Video (siehe typo-maske-medien)
  • Endlosliste riesiger Wörter mit Bildvorschau zum aktiven Wort
  • Footer-Name, der beim Erreichen des Seitenendes hochwächst
  • Mehrzeiliger Plakat-Titel, jede Zeile randvoll per text-fit: grow (Chrome 150), Rückfall über die Messung in JS

Umsetzung

Einpassen per JS: Text bei 100px messen, dann font-size = Platz / Breite in em. Bei fonts.ready und Resize neu messen. Für optische Randlosigkeit die Seitenlinien von erstem und letztem Zeichen abziehen (Canvas measureText, actualBoundingBox). Ohne JS: font-size in vw einmal pro Schrift und Wort kalibrieren, oder container-type: inline-size mit cqi. Alternativ SVG <text> mit textLength und viewBox. Künftig CSS text-fit: grow (CSS Text 5, laut MDN experimentell, noch nicht Baseline) mit @supports-Rückfall. Wichtig: line-height 0.8 bis 0.9, letter-spacing leicht negativ (−0.05em), Leerraum unter der Grundlinie per negativem margin abziehen. Reveal: Buchstaben teilen (SplitText oder eigenes Skript), jeder in eigener Maske, translateY 100 % → 0 mit expo.out. Beim Teilen geht das Paar-Kerning verloren; es lässt sich je Paar messen und als margin zurückgeben. Logo-Übergabe: Start- und Zielrechteck messen, transform: translate() scale() an den Scroll koppeln (ScrollTrigger mit scrub oder animation-timeline: scroll(), animation-range 0 60vh), transform-origin oben links. Immer transform skalieren, nie font-size animieren (kein Reflow).

Robust umsetzen

Telefon. Die Wortmarke bleibt randlos, wird aber klein. Lange Namen auf zwei Zeilen umbrechen (eigene mobile Aufteilung) oder um 90° gedreht setzen. Scroll-Kinetik reduzieren. Das Wort muss kurz genug sein.

Ohne Bewegung. Schrift sofort statisch zeigen, kein Buchstaben-Reveal, kein Scroll-Schrumpfen. Die Logo-Übergabe wird zur kurzen Überblendung.

Barrierefrei. Echtes <h1> verwenden, aber nur, wenn es wirklich die Seitenüberschrift ist. Kein Bild oder Canvas ohne Textalternative. Gesplittete Buchstaben: aria-label am Elternelement, aria-hidden auf den Spans, sonst buchstabiert der Screenreader. Bei 200 % Zoom darf nichts abgeschnitten werden, darum Fit-Text mit Mindestgröße.

Code

<!DOCTYPE html>
<!--
  Übergroßer Schrift-Hero (randlos eingepasste Wortmarke)
  Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/schrift-hero-viewportbreit/
  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>Übergroßer Schrift-Hero · 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: '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; }
@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: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; 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 {
  --papier: #ebe8e1; --tinte: #141414; --leise: rgba(20, 20, 20, .66); --linie: rgba(20, 20, 20, .16);
  --nacht: #121212; --hell: #ebe8e1; --hell-leise: rgba(235, 232, 225, .56); --hell-linie: rgba(235, 232, 225, .14);
  --mennige: #e0491d;
  --kurve: cubic-bezier(0.16, 1, 0.3, 1);   /* expo.out */
  --rand: clamp(10px, 1.25vw, 24px);         /* fast randlos: eine Kante für alles */
  /* Wortmarke: --fs setzt das Einpassen per JS; 31vw ist der kalibrierte CSS-Rückfall */
  --fs: 40.8vw; --ls: -.05em; --lh: .83; --unten: .065;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
img { display: block; max-width: 100%; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }

/* ---------- Kopfzeile (Differenz-Mischung: dunkel auf Papier, hell auf Nacht) ---------- */
.leiste { position: fixed; inset: 0 0 auto; z-index: 5; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; padding: 18px var(--rand); color: #fff; mix-blend-mode: difference; }
.logo-platz { justify-self: start; font: 800 24px/var(--lh) 'Archivo', sans-serif; font-stretch: 87.5%; letter-spacing: var(--ls); color: inherit; text-decoration: none; opacity: 0; transition: opacity .6s var(--kurve); }
.gedeckt .logo-platz { opacity: 1; }
.uebergabe .logo-platz { opacity: 0; visibility: hidden; }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: #fff; background: none; border: 1px solid rgba(255, 255, 255, .28); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; transition: background .15s, color .15s, border-color .15s; }
.modi button:hover { border-color: #fff; }
.modi button[aria-pressed='true'] { background: #fff; color: #000; border-color: #fff; }
.nav { justify-self: end; display: flex; gap: clamp(14px, 2vw, 28px); list-style: none; padding: 0; }
.nav a { color: inherit; text-decoration: none; }
/* Band hinter der Kopfzeile, sobald der dunkle Inhalt den Hero deckt; die Differenz-Mischung macht die Schrift darauf hell */
.band { position: fixed; inset: 0 0 auto; height: 66px; z-index: 4; background: var(--nacht); border-bottom: 1px solid var(--hell-linie); opacity: 0; pointer-events: none; transition: opacity .15s linear; }
.bandan .band { opacity: 1; }

/* ---------- Hero: steht still (sticky), der Inhalt schiebt sich darüber ---------- */
.held { position: sticky; top: 0; z-index: 1; height: 100svh; padding: 0 var(--rand); display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--rand); grid-template-rows: auto auto 1fr; align-content: start; }
.etiketten { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; padding-top: 84px; color: var(--leise); }
.etiketten p:nth-child(1) { grid-column: 1 / 5; color: var(--tinte); display: flex; align-items: center; gap: 10px; }
.etiketten p:nth-child(1)::before { content: ''; width: 8px; height: 8px; background: var(--mennige); flex: none; }
.etiketten p:nth-child(2) { grid-column: 5 / 8; }
.etiketten p:nth-child(3) { grid-column: 8 / 10; }
.etiketten p:nth-child(4) { grid-column: 10 / 13; justify-self: end; }
.sockel { grid-column: 1 / -1; grid-row: 3; align-self: end; display: grid; grid-template-columns: subgrid; align-items: end; row-gap: 22px; padding-bottom: calc(var(--fs) * (var(--lh) - var(--unten)) + var(--rand) + clamp(16px, 2.6vh, 28px)); }
.aussage { grid-column: 7 / 13; font-size: clamp(20px, min(2.1vw, 3.6vh), 34px); line-height: 1.12; font-weight: 500; letter-spacing: -.025em; max-width: 23ch; text-wrap: balance; }
.aussage em { font: italic 400 1.1em/1 'Instrument Serif', Georgia, serif; letter-spacing: -.01em; color: var(--mennige); }
.werte { grid-column: 1 / 7; grid-row: 1; display: flex; flex-wrap: wrap; gap: 4px 16px; color: var(--leise); max-width: 560px; }
.aussage { grid-row: 1; }
.eng .aussage { display: none; }
.werte b { color: var(--tinte); font-weight: 500; }
.etiketten, .aussage, .werte { transition: opacity .9s var(--kurve) .5s, translate .9s var(--kurve) .5s; }
.js:not(.reduziert) .held:not(.da) .etiketten, .js:not(.reduziert) .held:not(.da) .aussage { opacity: 0; translate: 0 12px; }

/* ---------- Die Wortmarke ---------- */
.marke-ebene { position: fixed; left: 0; right: 0; bottom: 0; z-index: 1; padding: 0 var(--rand) var(--rand); pointer-events: none; color: var(--tinte); }
/* Übergabe: über allem, Differenz-Mischung wie die Kopfzeile (dunkel auf Papier, hell auf Nacht) */
.uebergabe .marke-ebene { z-index: 6; color: #fff; mix-blend-mode: difference; }
.marke { font: 800 var(--fs)/var(--lh) 'Archivo', sans-serif; font-stretch: 87.5%; white-space: nowrap; margin-bottom: calc(var(--unten) * -1em); }   /* Grundlinie auf die Kante: Leerraum unter der Schrift abziehen */
.zug { display: inline-block; transform-origin: 0 0; will-change: transform; margin-left: var(--optik, 0em); }
.uebergabe.oben .zug { pointer-events: auto; cursor: pointer; }
/* Jeder Buchstabe in eigener Maske; clip-path statt overflow, damit Seitenlinien nicht abgeschnitten werden */
.m { display: inline-block; clip-path: inset(-.25em -.25em 0 -.25em); }
.b { display: inline-block; transition: transform 1.2s var(--kurve); transition-delay: calc(var(--i) * 60ms + 80ms); }
.js:not(.reduziert) .marke:not(.da) .b { transform: translateY(104%); transition: none; }
/* Reduziert: Logo-Übergabe als Überblendung */
.marke-ebene, .logo-platz { transition: opacity .6s var(--kurve); }

/* ---------- Inhalt, der den Hero überdeckt ---------- */
.inhalt { position: relative; z-index: 2; background: var(--nacht); color: var(--hell); padding: clamp(96px, 14vh, 160px) var(--rand) 0; display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--rand); row-gap: clamp(40px, 7vh, 88px); }
.inhalt .mono { color: var(--hell-leise); }
.kapitel { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 16px; padding-bottom: 14px; border-bottom: 1px solid var(--hell-linie); }
.kapitel span:first-child { color: var(--hell); }
.satz { grid-column: 1 / 9; font-size: clamp(30px, 4.4vw, 68px); line-height: 1.02; font-weight: 500; letter-spacing: -.035em; max-width: 17ch; }
.satz em { font: italic 400 1.06em/1 'Instrument Serif', Georgia, serif; letter-spacing: -.01em; }
.arbeit { display: grid; gap: 12px; }
.arbeit :is(img, svg) { display: block; width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; }
.arbeit figcaption { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.arbeit figcaption b { color: var(--hell); font-weight: 500; }
.a1 { grid-column: 1 / 8; }
.a2 { grid-column: 9 / 13; align-self: end; }
.a2 :is(img, svg) { aspect-ratio: 4 / 5; }
.a3 { grid-column: 2 / 6; }
.a3 :is(img, svg) { aspect-ratio: 4 / 5; }
.a4 { grid-column: 7 / 13; align-self: center; }
.fuss { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 22px 0 var(--rand); border-top: 1px solid var(--hell-linie); margin-top: 6vh; }

/* ---------- Vorschau in der Galerie: nur die Bühne ---------- */
.vorschau body { overflow: hidden; }
.vorschau .nav { visibility: hidden; }

/* ---------- Telefon ---------- */
@media (max-width: 720px) {
  .leiste { grid-template-columns: 1fr auto; row-gap: 10px; padding-top: 14px; }
  .nav { display: none; }
  .modi { grid-column: 1 / -1; grid-row: 2; }
  .modi button { padding: 0 10px; height: 28px; }
  .band { height: 92px; }
  .etiketten { padding-top: 104px; row-gap: 6px; }
  .etiketten p:nth-child(1) { grid-column: 1 / 13; }
  .etiketten p:nth-child(2) { grid-column: 1 / 13; }
  .etiketten p:nth-child(3) { grid-column: 1 / 9; }
  .etiketten p:nth-child(4) { grid-column: 9 / 13; }
  .aussage { grid-column: 1 / 13; grid-row: 1; font-size: 30px; max-width: 15ch; }
  .sockel { row-gap: 18px; }
  .werte { grid-column: 1 / 13; grid-row: 2; }
  .satz { grid-column: 1 / 13; }
  .a1, .a2, .a3, .a4 { grid-column: 1 / 13; }
}
@media (hover: none) {
  .modi button:hover { border-color: rgba(255, 255, 255, .28); }
  .modi button[aria-pressed='true']:hover { border-color: #fff; }
}
/* Reduziert: kein Buchstaben-Reveal, kein Einfahren; nur die Überblendung bleibt */
:root.reduziert .b, :root.reduziert .etiketten, :root.reduziert .aussage, :root.reduziert .werte { transition: none !important; }
:root.reduziert .marke-ebene, :root.reduziert .logo-platz { transition-duration: .3s !important; }
/* ===================== Varianten (Regel 13: ?v=basis | textfit) ===================== */
.vpult { position: fixed; left: 14px; bottom: 14px; z-index: 30; display: grid; gap: 4px; padding: 6px; border-radius: 16px; background: rgba(18, 18, 20, .86); color: #f3f2ee; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); box-shadow: 0 6px 24px rgba(0, 0, 0, .16); font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.vpult > div { display: flex; align-items: center; gap: 2px; }
.vpult > div > span { min-width: 70px; padding: 0 8px; color: rgba(243, 242, 238, .55); }
.vpult button { font: inherit; letter-spacing: inherit; text-transform: inherit; height: 28px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: rgba(243, 242, 238, .78); cursor: pointer; transition: background-color .15s, color .15s; }
.vpult button:hover { color: #fff; }
.vpult button[aria-pressed='true'] { background: #f3f2ee; color: #121214; }
.vpult button:focus-visible { outline: 2px solid #e0491d; outline-offset: 2px; }
.vpult button:disabled { opacity: .4; cursor: default; }
.vpult [data-weiche-zeile] { display: none; }
html[data-v='textfit'] .vpult [data-weiche-zeile] { display: flex; }
@media (hover: none) { .vpult button { height: 36px; padding: 0 14px; } }
html[data-v='textfit'] :is(.band, .leiste, .held, .marke-ebene, .inhalt) { display: none !important; }
.welt-tf[hidden] { display: none; }

/* ---------- Variante „text-fit“: Rezept kultur-programm-haltung, mehrzeiliger Plakat-Titel, jede Zeile randvoll ---------- */
html[data-v='textfit'], html[data-v='textfit'] body { background: #efede9; color: #0b0b0b; }
.welt-tf { --tf-kurve: cubic-bezier(0.215, 0.61, 0.355, 1); --tf-musik: #c4421a; --tf-theater: #3d5f9a; --tf-kunst: #2f7d4a; --tf-film: #b0186b; --tf-rand: clamp(12px, 1.6vw, 28px);
  font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; }
.tf-kopf { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 16px var(--tf-rand); border-bottom: 1px solid #0b0b0b; }
.tf-marke { font: 800 20px/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.02em; color: inherit; text-decoration: none; }
.tf-kopf nav { display: flex; gap: 22px; font-size: 1rem; }
.tf-kopf nav a { color: inherit; text-decoration: none; background: linear-gradient(currentColor, currentColor) 0 100% / 0 2px no-repeat; transition: background-size 200ms var(--tf-kurve); }
.tf-kopf nav a:hover { background-size: 100% 2px; }
.tf-ticket { display: inline-flex; align-items: center; height: 40px; padding: 0 18px; background: #0b0b0b; color: #efede9; font: 700 15px/1 'Schibsted Grotesk', sans-serif; text-decoration: none; transition: background-color 150ms var(--tf-kurve); }
.tf-ticket:hover { background: var(--tf-musik); }
.welt-tf a:focus-visible, .welt-tf button:focus-visible { outline: 3px solid var(--tf-theater); outline-offset: 3px; }
.tf-held { min-height: calc(100svh - 73px); display: grid; grid-template-rows: auto 1fr auto; gap: clamp(14px, 2.4svh, 28px); padding: clamp(14px, 2.4svh, 26px) var(--tf-rand) clamp(16px, 3svh, 32px); }
.tf-oben { display: flex; justify-content: space-between; gap: 8px 24px; flex-wrap: wrap; font: 500 .8125rem/1.3 'IBM Plex Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }
/* Breite begrenzt durch die Höhe: der Titel ist etwa 0,38 × so hoch wie breit, darunter brauchen Kopf und Fuß ca. 330 px */
.tf-buehne { align-self: center; margin-inline: auto; width: 100%; max-inline-size: min(var(--tf-breite, 100%), calc((100svh - 330px) * 2.6)); }
.tf-unten .tf-ticket { justify-self: end; }
/* Der Titel: eine Zeile je Block; text-fit: grow füllt jede Zeile auf die Breite (Chrome 150) */
.tf-titel { font: 900 20px/.84 'Schibsted Grotesk', sans-serif; letter-spacing: -.025em; text-transform: uppercase; margin: 0; }
.tf-maske { display: block; overflow: clip; overflow-clip-margin: .06em; padding-bottom: .02em; }
.tf-z { display: block; white-space: nowrap; transition: transform 450ms var(--tf-kurve); transition-delay: calc(var(--i) * 70ms + 60ms); }
.tf-z:nth-child(1) { color: inherit; }
@supports (text-fit: grow) {
  .tf-titel:not(.messung) .tf-z { text-fit: grow; }
}
.js:not(.reduziert) .tf-titel:not(.da) .tf-z { transform: translateY(105%); transition: none; }
.tf-unten { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 4fr) auto; gap: 16px clamp(20px, 3vw, 48px); align-items: end; padding-top: 14px; border-top: 1px solid #0b0b0b; }
.tf-unten p { font-size: 1.125rem; line-height: 1.5; max-width: 44ch; }
.tf-sparten { display: flex; flex-wrap: wrap; gap: 6px; list-style: none; padding: 0; margin: 0; }
.tf-sparten li { display: inline-flex; align-items: center; height: 30px; padding: 0 12px; color: #fff; font: 700 .875rem/1 'Schibsted Grotesk', sans-serif; }
.tf-programm { padding: clamp(32px, 6vw, 72px) var(--tf-rand) 96px; display: grid; gap: 0; }
.tf-programm h2 { font: 800 clamp(1.75rem, 1.2rem + 2vw, 2.75rem)/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.02em; margin-bottom: 18px; }
.tf-programm ol { list-style: none; margin: 0; padding: 0; border-top: 1px solid #0b0b0b; }
.tf-programm li { display: grid; grid-template-columns: 110px 1fr auto; gap: 16px; align-items: baseline; padding: 16px 0; border-bottom: 1px solid rgba(11, 11, 11, .2); }
.tf-programm time { font: 500 .8125rem/1.3 'IBM Plex Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }
.tf-programm b { font: 700 clamp(1.375rem, 2.2vw, 2rem)/1.1 'Schibsted Grotesk', sans-serif; letter-spacing: -.01em; }
.tf-programm li span { font-size: .9375rem; }
.tf-programm li i { font-style: normal; display: inline-block; padding: 4px 8px; color: #fff; font: 700 .75rem/1 'Schibsted Grotesk', sans-serif; margin-left: 10px; vertical-align: .3em; }
.tf-werte { display: flex; flex-wrap: wrap; gap: 4px 16px; color: rgba(11, 11, 11, .62); }
.tf-werte b { color: #0b0b0b; font-weight: 500; }
.reduziert .tf-z { transition: none !important; transform: none !important; }
.vorschau .tf-kopf nav, .vorschau .tf-unten p, .vorschau .tf-werte { display: none; }
.vorschau .tf-held { min-height: calc(100svh - 73px); }
@media (max-width: 900px) { .tf-kopf nav { display: none; } .tf-unten { grid-template-columns: 1fr auto; } .tf-unten p { grid-column: 1 / -1; } }
@media (max-width: 640px) {
  .tf-unten { grid-template-columns: 1fr; }
  .tf-programm li { grid-template-columns: 1fr; gap: 4px; }
  .tf-held { min-height: auto; padding-bottom: 28px; }
  .vpult { left: 8px; right: 8px; bottom: 8px; }
}
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<div class="band" aria-hidden="true"></div>
<header class="leiste">
  <a class="logo-platz" href="#oben" data-logo aria-label="Kerbe, zum Anfang">Kerbe</a>
  <div class="modi mono" role="group" aria-label="Variante">
    <button type="button" data-variante="einpassen" aria-pressed="true">Einpassen</button>
    <button type="button" data-variante="uebergabe" aria-pressed="false">Übergabe</button>
    <button type="button" data-variante="gestaffelt" aria-pressed="false">Gestaffelt</button>
  </div>
  <ul class="nav mono">
    <li><a href="#arbeiten">Arbeiten</a></li>
    <li><a href="#arbeiten">Studio</a></li>
    <li><a href="#kontakt">Kontakt</a></li>
  </ul>
</header>

<section class="held" id="oben" data-held>
  <div class="etiketten mono">
    <p>Studio für Marke &amp; Schrift</p>
    <p>Köln-Ehrenfeld, 50.95° N</p>
    <p>Seit 2014</p>
    <p>Arbeiten ↓</p>
  </div>
  <div class="sockel">
    <p class="werte mono" data-werte aria-live="polite"></p>
    <p class="aussage">Wir zeichnen Erscheinungsbilder, die man noch aus <em>zwanzig Metern</em> erkennt.</p>
  </div>
</section>

<div class="marke-ebene" data-ebene>
  <h1 class="marke" data-marke aria-label="Kerbe"><span class="zug" data-zug>Kerbe</span></h1>
</div>

<main class="inhalt" id="arbeiten">
  <p class="kapitel mono"><span>(01) Ausgewählte Arbeiten</span><span>2022–2026</span></p>
  <p class="satz">Ein Name, eine Form, eine Haltung. Alles andere ist <em>Ausstattung.</em></p>
  <figure class="arbeit a1">
    <svg viewBox="0 0 1200 800" role="img" aria-label="Erscheinungsbild Brauhaus Lindwurm: Bierdeckel und Etikett auf orangem Grund">
      <rect width="1200" height="800" fill="#d9662f"/>
      <g transform="rotate(-8 830 420)"><rect x="700" y="170" width="300" height="440" rx="6" fill="#1d1a17"/><rect x="726" y="196" width="248" height="388" rx="3" fill="none" stroke="#f3eadb" stroke-width="2"/>
        <text x="850" y="330" text-anchor="middle" font-family="Instrument Serif" font-style="italic" font-size="68" fill="#f3eadb">Lindwurm</text>
        <text x="850" y="372" text-anchor="middle" font-family="Archivo" font-weight="700" font-size="17" letter-spacing="5" fill="#d9662f">HELLES · 4,9 %</text>
        <path d="M790 440q60-46 120 0t60 60" fill="none" stroke="#f3eadb" stroke-width="3"/><text x="850" y="548" text-anchor="middle" font-family="Archivo" font-size="14" letter-spacing="4" fill="#f3eadb">KÖLN · SEIT 1912</text></g>
      <circle cx="430" cy="430" r="250" fill="#f3eadb"/><circle cx="430" cy="430" r="224" fill="none" stroke="#1d1a17" stroke-width="2" stroke-dasharray="3 7"/>
      <text x="430" y="420" text-anchor="middle" font-family="Instrument Serif" font-style="italic" font-size="104" fill="#1d1a17">Lindwurm</text>
      <text x="430" y="484" text-anchor="middle" font-family="Archivo" font-weight="700" font-size="22" letter-spacing="9" fill="#d9662f">BRAUHAUS</text>
      <path d="M330 330q50-40 100 0t100 0" fill="none" stroke="#1d1a17" stroke-width="5" stroke-linecap="round"/>
    </svg>
    <figcaption class="mono"><b>Brauhaus Lindwurm</b><span>Erscheinungsbild · 2025</span></figcaption>
  </figure>
  <figure class="arbeit a2">
    <svg viewBox="0 0 800 1000" role="img" aria-label="Wortmarke ofen, weiß auf Kobaltblau">
      <rect width="800" height="1000" fill="#1f3fbf"/>
      <text x="400" y="560" text-anchor="middle" font-family="Archivo" font-weight="800" style="font-stretch:87.5%" font-size="300" letter-spacing="-15" fill="#f4f1ea">ofen</text>
      <rect x="352" y="610" width="96" height="10" fill="#e0491d"/>
      <text x="400" y="900" text-anchor="middle" font-family="Archivo" font-weight="600" font-size="20" letter-spacing="8" fill="#f4f1ea" fill-opacity=".7">KOLLEKTIV FÜR KERAMIK</text>
    </svg>
    <figcaption class="mono"><b>Ofen Kollektiv</b><span>Wortmarke · 2024</span></figcaption>
  </figure>
  <figure class="arbeit a3">
    <svg viewBox="0 0 800 1000" role="img" aria-label="Aufgeschlagenes Stadtmagazin Rheinblatt mit großer Überschrift und Textspalten">
      <rect width="800" height="1000" fill="#cfc8bb"/>
      <g transform="rotate(-4 400 500)">
        <rect x="70" y="190" width="660" height="560" fill="#000" opacity=".18" transform="translate(10 16)"/>
        <rect x="70" y="190" width="330" height="560" fill="#f6f3ec"/><rect x="400" y="190" width="330" height="560" fill="#fbf9f4"/>
        <rect x="390" y="190" width="20" height="560" fill="#000" opacity=".06"/>
        <text x="100" y="330" font-family="Instrument Serif" font-size="128" fill="#141414">Rhein</text>
        <text x="100" y="380" font-family="Instrument Serif" font-style="italic" font-size="34" fill="#e0491d">die stille Seite</text>
        <path d="M100 430h250M100 446h250M100 462h250M100 478h220M100 510h250M100 526h250M100 542h250M100 558h250M100 574h190M100 606h250M100 622h250M100 638h240" stroke="#141414" stroke-opacity=".38" stroke-width="5"/>
        <rect x="430" y="220" width="270" height="330" fill="#2b3d36"/><path d="M430 470q70-60 135-20t135-40V550H430Z" fill="#e0491d" opacity=".85"/>
        <path d="M430 590h270M430 606h270M430 622h270M430 638h210M430 670h270M430 686h270M430 702h150" stroke="#141414" stroke-opacity=".38" stroke-width="5"/>
        <text x="100" y="728" font-family="Geist Mono" font-size="13" letter-spacing="3" fill="#141414" fill-opacity=".6">RHEINBLATT 07 · 2023</text>
      </g>
    </svg>
    <figcaption class="mono"><b>Rheinblatt</b><span>Editorial · 2023</span></figcaption>
  </figure>
  <figure class="arbeit a4">
    <svg viewBox="0 0 1200 800" role="img" aria-label="Rundes Signet Salon Kamm, hell auf dunklem Grün">
      <defs><path id="kamm-kreis" d="M600 400m-205 0a205 205 0 1 1 410 0a205 205 0 1 1-410 0"/></defs>
      <rect width="1200" height="800" fill="#183a2e"/>
      <circle cx="600" cy="400" r="250" fill="none" stroke="#efe6d4" stroke-width="3"/><circle cx="600" cy="400" r="160" fill="none" stroke="#efe6d4" stroke-width="1.5"/>
      <text font-family="Archivo" font-weight="700" font-size="30" letter-spacing="11" fill="#efe6d4"><textPath href="#kamm-kreis">SALON KAMM · HAAR UND BART · SEIT 1958 ·</textPath></text>
      <rect x="520" y="352" width="160" height="30" rx="4" fill="#efe6d4"/>
      <path d="M532 382v58M548 382v58M564 382v58M580 382v58M596 382v58M612 382v58M628 382v58M644 382v58M660 382v58" stroke="#efe6d4" stroke-width="7" stroke-linecap="round"/>
    </svg>
    <figcaption class="mono"><b>Salon Kamm</b><span>Signet · 2022</span></figcaption>
  </figure>
  <p class="fuss mono" id="kontakt"><span>Kerbe Studio GmbH · Venloer Straße 241 · 50823 Köln</span><span>hallo@kerbe-studio.de</span></p>
</main>

<!-- Variante „text-fit“: Kulturhaus, mehrzeiliger Plakat-Titel, jede Zeile per text-fit randvoll -->
<div class="welt-tf" data-welt="textfit" hidden>
  <header class="tf-kopf">
    <a class="tf-marke" href="#">Halle Oststadt</a>
    <nav aria-label="Hauptnavigation"><a href="#">Programm</a><a href="#">Besuch</a><a href="#">Barrierefrei</a><a href="#">Leichte Sprache</a></nav>
    <a class="tf-ticket" href="#">Tickets</a>
  </header>
  <section class="tf-held" aria-labelledby="tf-titel">
    <p class="tf-oben"><span>Sa 14.11.2026 · 18 bis 2 Uhr</span><span>42 Ateliers · 1 Ticket · Shuttle alle 15 Min.</span></p>
    <div class="tf-buehne" data-tf-buehne>
      <h1 class="tf-titel" id="tf-titel" data-tf-titel aria-label="Lange Nacht der offenen Ateliers">
        <span class="tf-maske" aria-hidden="true"><span class="tf-z" style="--i:0">Lange Nacht</span></span>
        <span class="tf-maske" aria-hidden="true"><span class="tf-z" style="--i:1">der offenen</span></span>
        <span class="tf-maske" aria-hidden="true"><span class="tf-z" style="--i:2">Ateliers</span></span>
      </h1>
    </div>
    <div class="tf-unten">
      <p>Eine Nacht lang stehen in der Oststadt die Türen offen: Malerei, Druck, Keramik, Klang. Das Ticket gilt für alle Ateliers und den Shuttle.</p>
      <ul class="tf-sparten" aria-label="Sparten"><li style="background:#2f7d4a">Kunst</li><li style="background:#c4421a">Musik</li><li style="background:#3d5f9a">Tanz</li><li style="background:#b0186b">Film</li></ul>
      <a class="tf-ticket" href="#">Ticket 12 €</a>
    </div>
    <p class="tf-werte mono" data-tf-werte aria-live="polite"></p>
  </section>
  <section class="tf-programm" aria-labelledby="tf-prog">
    <h2 id="tf-prog">Höhepunkte</h2>
    <ol>
      <li><time datetime="2026-11-14T18:00">18:00 Uhr</time><b>Eröffnung im Hof<i style="background:#c4421a">Musik</i></b><span>Halle 1</span></li>
      <li><time datetime="2026-11-14T19:30">19:30 Uhr</time><b>Druckwerkstatt zum Mitmachen<i style="background:#2f7d4a">Kunst</i></b><span>Atelier 14</span></li>
      <li><time datetime="2026-11-14T21:00">21:00 Uhr</time><b>Kurzfilme aus den Ateliers<i style="background:#b0186b">Film</i></b><span>Kino im Keller</span></li>
      <li><time datetime="2026-11-14T23:30">23:30 Uhr</time><b>Tanz im Lichthof<i style="background:#3d5f9a">Tanz</i></b><span>Lichthof</span></li>
    </ol>
  </section>
</div>

<div class="vpult" role="group" aria-label="Demo-Steuerung">
  <div role="group" aria-label="Variante"><span>Variante</span>
    <button type="button" data-v-wahl="basis" aria-pressed="true">Wortmarke</button>
    <button type="button" data-v-wahl="textfit" aria-pressed="false">text-fit</button>
  </div>
  <div role="group" aria-label="Weiche" data-weiche-zeile><span>Weiche</span>
    <button type="button" data-weiche="nativ" aria-pressed="true">text-fit: grow</button>
    <button type="button" data-weiche="messung" aria-pressed="false">Fallback: Messung</button>
  </div>
</div>

<script>
var STEUER = {};   // je Variante ein Steuerobjekt { start, stopp, neustart, vorfuehren, pause, weiter }
/* ---------- Grundvariante „Wortmarke“ (Studio Kerbe): Einpassen, Übergabe, Gestaffelt ---------- */
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const aktiv = () => html.dataset.v !== 'textfit';
  const held = document.querySelector('[data-held]');
  const ebene = document.querySelector('[data-ebene]');
  const marke = document.querySelector('[data-marke]');
  const zug = document.querySelector('[data-zug]');
  const logo = document.querySelector('[data-logo]');
  // Unsichtbares Logo (opacity 0) nicht per Tab erreichbar machen
  const logoFokus = () => { const an = html.classList.contains('gedeckt'); logo.tabIndex = an ? 0 : -1; logo.toggleAttribute('aria-hidden', !an); };
  new MutationObserver(logoFokus).observe(html, { attributes: true, attributeFilter: ['class'] }); logoFokus();
  const werte = document.querySelector('[data-werte]');
  const sockel = document.querySelector('.sockel');
  const inhalt = document.querySelector('.inhalt');
  const band = document.querySelector('.band');

  const WORT = zug.textContent;
  const SPERRUNG = -0.05;          // letter-spacing in em (14islands, Instrument)
  const STRECKE = 0.6;             // Logo-Übergabe über 0–60vh Scroll
  const TEMPO = [0.35, 1.15, 0.6, 1.4, 0.85];   // Gestaffelt: Tempo je Buchstabe relativ zum Scroll (über 1 entkommt er dem Inhalt)
  const SCHNITT = '800', BREITE = 'semi-condensed';  // = font-stretch 87.5 %

  let variante = 'einpassen', fsPx = 0, start = null, ziel = null;

  // 1) Teilen: jeder Buchstabe in eine Maske; Screenreader lesen das aria-label am <h1>
  zug.innerHTML = [...WORT].map((z, i) => `<span class="m" aria-hidden="true"><span class="b" style="--i:${i}">${z}</span></span>`).join('');
  const masken = [...zug.querySelectorAll('.m')];

  // 2) Einpassen: Breite bei 100px messen, Kerning der Paare und Seitenlinien (Tinte) berücksichtigen
  const leinwand = document.createElement('canvas').getContext('2d');
  function tinte(text) {
    leinwand.font = `${BREITE} ${SCHNITT} 100px Archivo`;
    const m = leinwand.measureText(text);
    return { breite: m.width, links: -m.actualBoundingBoxLeft, rechts: m.actualBoundingBoxRight };
  }
  function einpassen() {
    if (!ebene.clientWidth) return;   // ausgeblendet (andere Variante)
    // Zurichtung: Sperrung plus Paar-Kerning, das durch das Teilen verloren geht
    masken.forEach((m, i) => {
      const a = WORT[i], b = WORT[i + 1];
      const kern = b ? (tinte(a + b).breite - tinte(a).breite - tinte(b).breite) / 100 : 0;
      m.style.marginRight = b ? `${(SPERRUNG + kern).toFixed(4)}em` : '0';
    });
    // optisch randlos: linke Seitenlinie des ersten, rechte des letzten Zeichens abziehen
    const erstes = tinte(WORT[0]), letztes = tinte(WORT.at(-1));
    const lsb = erstes.links / 100, rsb = (letztes.breite - letztes.rechts) / 100;
    marke.style.fontSize = '100px';
    zug.style.setProperty('--optik', '0em');
    const roh = zug.getBoundingClientRect().width / 100;   // in em
    const platz = ebene.clientWidth - 2 * parseFloat(getComputedStyle(ebene).paddingLeft);
    fsPx = platz / (roh - lsb - rsb);
    marke.style.fontSize = '';
    html.style.setProperty('--fs', `${fsPx}px`);
    zug.style.setProperty('--optik', `${(-lsb).toFixed(4)}em`);
    messen();
    zeigeWerte();
    platzPruefen();
  }
  // Sehr flache Fenster: Aussage weglassen, bevor sie unter die Wortmarke rutscht
  function platzPruefen() {
    html.classList.remove('eng');
    const etik = document.querySelector('.etiketten').getBoundingClientRect();
    const sockel = document.querySelector('.sockel').getBoundingClientRect();
    if (sockel.top < etik.bottom + 16) html.classList.add('eng');
  }

  // 3) Start- und Zielrechteck für die Logo-Übergabe
  function messen() {
    const alt = zug.style.transform;
    zug.style.transform = 'none';
    start = zug.getBoundingClientRect();
    const v = logo.style.visibility; logo.style.visibility = 'hidden';
    ziel = logo.getBoundingClientRect();
    logo.style.visibility = v;
    zug.style.transform = alt;
  }

  function zeigeWerte() {
    const vw = (fsPx / innerWidth * 100).toFixed(2).replace('.', ',');
    const s = ziel && start ? (ziel.height / start.height).toFixed(3).replace('.', ',') : '';
    const basis = `<span>font-size <b>${vw}vw</b> gemessen</span><span>line-height <b>.83</b></span><span>Sperrung <b>−.05em</b></span><span>Archivo <b>800 · wdth 87,5</b></span>`;
    const je = {
      einpassen: '<span>Reveal <b>expo.out 1200 ms</b></span><span>Versatz <b>60 ms</b></span><span>Hero <b>sticky, wird überdeckt</b></span>',
      uebergabe: `<span>Übergabe <b>scale 1 → ${s}</b></span><span>Strecke <b>0–60vh, scrub</b></span>`,
      gestaffelt: '<span>Tempo <b>.35 / 1.15 / .6 / 1.4 / .85 × Scroll</b></span><span>Hero <b>sticky</b></span>',
    };
    werte.innerHTML = reduziert()
      ? basis + '<span>Reduziert <b>statisch, Übergabe als Überblendung 300 ms</b></span>'
      : basis + je[variante];
  }

  // 4) Scroll: eine Funktion zeichnet alle Varianten
  let geplant = false;
  function zeichnen() {
    geplant = false;
    if (!aktiv()) return;
    const y = scrollY, vh = innerHeight;
    html.classList.toggle('gedeckt', variante !== 'uebergabe' && y > Math.min(vh * 0.82, ebene.offsetHeight * 0.85));   // Logo erscheint, sobald die Wortmarke fast verdeckt ist
    html.classList.toggle('bandan', inhalt.getBoundingClientRect().top <= band.offsetHeight);
    if (variante === 'uebergabe' && start && ziel) {
      const p = Math.min(1, Math.max(0, y / (vh * STRECKE)));
      html.classList.toggle('oben', p >= 1);
      if (reduziert()) {   // Überblendung statt Bewegung
        zug.style.transform = 'none';
        ebene.style.opacity = p < 0.5 ? 1 : 0;
        logo.style.visibility = 'visible';
        logo.style.opacity = p < 0.5 ? 0 : 1;
        return;
      }
      const s = 1 + (ziel.height / start.height - 1) * p;
      const dx = (ziel.left - start.left) * p, dy = (ziel.top - start.top) * p;
      zug.style.transform = `translate3d(${dx}px, ${dy}px, 0) scale(${s})`;
      sockel.style.opacity = Math.max(0, 1 - p * 2.5);
    } else if (variante === 'gestaffelt' && !reduziert()) {
      const weg = Math.min(y, vh * 1.2);
      masken.forEach((m, i) => { m.style.transform = `translate3d(0, ${(-weg * TEMPO[i]).toFixed(1)}px, 0)`; });
      sockel.style.opacity = Math.max(0, 1 - weg / (vh * 0.4));
    }
  }
  const anfordern = () => { if (!geplant) { geplant = true; requestAnimationFrame(zeichnen); } };
  addEventListener('scroll', anfordern, { passive: true });

  function setzeVariante(neu) {
    variante = neu;
    html.classList.toggle('uebergabe', neu === 'uebergabe');
    html.classList.remove('oben', 'gedeckt', 'bandan');
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === neu)));
    zug.style.transform = '';
    masken.forEach(m => { m.style.transform = ''; });
    ebene.style.opacity = ''; logo.style.opacity = ''; logo.style.visibility = '';
    sockel.style.opacity = '';
    zeigeWerte();
    zeichnen();
  }

  // 5) Auftritt: Buchstaben steigen aus ihren Masken
  function auftritt() {
    marke.classList.remove('da'); held.classList.remove('da');
    void marke.offsetWidth;
    marke.classList.add('da'); held.classList.add('da');
  }

  // Klick auf die gelandete Wortmarke oder das Logo: nach oben
  const nachOben = e => { e.preventDefault(); scrollTo({ top: 0, behavior: reduziert() ? 'auto' : 'smooth' }); };
  zug.addEventListener('click', nachOben);
  logo.addEventListener('click', nachOben);

  document.querySelector('.modi').addEventListener('click', e => {
    const b = e.target.closest('[data-variante]');
    if (!b) return;
    pause();
    scrollTo(0, 0);
    setzeVariante(b.dataset.variante);
    auftritt();
  });

  // Selbst-Scroll für die Vorführung (simuliert den Nutzer, darum easeInOut)
  let fahrt = 0;
  function fahre(nach, ms) {
    return new Promise(fertig => {
      cancelAnimationFrame(fahrt);
      const von = scrollY, t0 = performance.now();
      const schritt = t => {
        const k = Math.min(1, (t - t0) / ms);
        const e = k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2;
        scrollTo(0, von + (nach - von) * e);
        if (k < 1) fahrt = requestAnimationFrame(schritt); else fertig();
      };
      fahrt = requestAnimationFrame(schritt);
    });
  }

  let uhr = null, laeuft = false, lauf = 0;
  const warte = ms => new Promise(r => { uhr = setTimeout(r, ms); });
  async function vorfuehren() {
    pause();
    laeuft = true;
    const id = ++lauf;
    const reihe = ['einpassen', 'uebergabe', 'gestaffelt'];
    let i = reihe.indexOf(variante);
    while (laeuft && id === lauf) {
      scrollTo(0, 0);
      setzeVariante(reihe[i]);
      auftritt();
      await warte(reduziert() ? 1400 : 1900);
      if (id !== lauf) return;
      // Einpassen: nur so weit, bis der Inhalt die Wortmarke fast deckt und das Logo oben auftaucht
      const tief = reihe[i] === 'einpassen' ? Math.min(innerHeight * 0.72, ebene.offsetHeight * 0.9)
        : innerHeight * (reihe[i] === 'uebergabe' ? 0.78 : 0.72);
      if (reduziert()) { scrollTo(0, tief); } else { await fahre(tief, 2000); }
      if (id !== lauf) return;
      await warte(1300);
      if (id !== lauf) return;
      if (reduziert()) { scrollTo(0, 0); } else { await fahre(0, 1300); }
      if (id !== lauf) return;
      await warte(400);
      i = (i + 1) % reihe.length;
    }
  }
  function pause() { laeuft = false; lauf++; clearTimeout(uhr); cancelAnimationFrame(fahrt); }
  function neustart() { pause(); scrollTo(0, 0); setzeVariante(variante); auftritt(); }

  // Start: erst nach den Schriften messen, sonst passt die Wortmarke nicht
  const bereit = Promise.all([document.fonts.load(`${SCHNITT} 100px Archivo`), document.fonts.ready]);
  bereit.then(() => {
    einpassen();
    setzeVariante(variante);
    requestAnimationFrame(auftritt);
  });
  new ResizeObserver(() => { if (fsPx) { einpassen(); zeichnen(); } }).observe(ebene);
  addEventListener('resize', () => { if (fsPx) { messen(); zeichnen(); } });

  STEUER.basis = {
    start() { scrollTo(0, 0); bereit.then(() => { einpassen(); setzeVariante(variante); auftritt(); }); },
    stopp() {
      pause();
      html.classList.remove('uebergabe', 'oben', 'gedeckt', 'bandan', 'eng');
      zug.style.transform = ''; masken.forEach(m => { m.style.transform = ''; }); sockel.style.opacity = ''; ebene.style.opacity = '';
    },
    neustart, vorfuehren, pause,
    weiter() { if (html.classList.contains('vorschau') && !laeuft) vorfuehren(); },
  };
})();

/* ---------- Variante „text-fit“ (Kulturhaus): jede Titelzeile randvoll, nativ per text-fit, Rückfall über die Messung ---------- */
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const welt = document.querySelector('[data-welt="textfit"]');
  const titel = welt.querySelector('[data-tf-titel]'), buehne = welt.querySelector('[data-tf-buehne]');
  const zeilen = [...titel.querySelectorAll('.tf-z')];
  const werteEl = welt.querySelector('[data-tf-werte]');
  const nativOk = CSS.supports('text-fit', 'grow');
  const W = { schrift: 20, zeilenhoehe: 0.84, sperrung: -0.025, reveal_ms: 450, versatz_ms: 70 };
  let weiche = nativOk ? 'nativ' : 'messung';
  // Rückfall wie in der Grundvariante: Breite bei 100 px messen, Seitenlinien (Tinte) per Canvas abziehen
  const leinwand = document.createElement('canvas').getContext('2d');
  function messen() {
    const platz = titel.clientWidth;
    if (!platz) return;
    zeilen.forEach(z => {
      z.style.fontSize = '100px'; z.style.marginLeft = '0px';
      const r = document.createRange(); r.selectNodeContents(z);
      const roh = r.getBoundingClientRect().width / 100;                       // Laufweite in em, mit Sperrung
      leinwand.font = '900 100px "Schibsted Grotesk"'; leinwand.letterSpacing = (W.sperrung * 100) + 'px';
      const m = leinwand.measureText(z.textContent.toUpperCase());
      const links = -m.actualBoundingBoxLeft / 100, rechts = (m.width - m.actualBoundingBoxRight) / 100;
      const fs = platz / (roh - links - rechts);
      z.style.fontSize = fs.toFixed(2) + 'px';
      z.style.marginLeft = (-links * fs).toFixed(2) + 'px';                      // optisch randlos
    });
  }
  function werte() {
    const f = zeilen.map(z => {
      const r = document.createRange(); r.selectNodeContents(z);
      return (r.getBoundingClientRect().width ? z.getBoundingClientRect().height / (parseFloat(getComputedStyle(z).fontSize) * W.zeilenhoehe) : 0);
    });
    const px = zeilen.map(z => Math.round(parseFloat(z.style.fontSize) || 0));
    werteEl.innerHTML = (weiche === 'nativ'
      ? `<span>Zeilen <b>text-fit: grow</b> je Block (ab Chrome 150)</span><span>Faktor <b>${f.map(x => x.toFixed(1).replace('.', ',') + '×').join(' / ')}</b></span><span>Basis <b>20 px, Zeilenhöhe .84</b></span><span>Kante <b>Laufweite</b></span>`
      : `<span>Rückfall <b>Messung per JS</b></span><span>font-size <b>${px.join(' / ')} px</b></span><span>Kante <b>Tinte, Seitenlinien abgezogen</b></span>`)
      + '<span>Schibsted Grotesk <b>900, −.025em, versal</b></span><span>Reveal <b>translateY 105 % → 0 · 450 ms cubic-bezier(0.215, 0.61, 0.355, 1), +70 ms</b></span>'
      + (nativOk ? '' : '<span>Browser <b>ohne text-fit</b></span>');
  }
  function setzeWeiche(w) {
    weiche = (w === 'nativ' && !nativOk) ? 'messung' : w;
    titel.classList.toggle('messung', weiche === 'messung');
    zeilen.forEach(z => { z.style.fontSize = ''; z.style.marginLeft = ''; });
    if (weiche === 'messung') messen();
    document.querySelectorAll('[data-weiche]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.weiche === weiche)));
    werte();
  }
  document.querySelectorAll('[data-weiche]').forEach(b => {
    if (b.dataset.weiche === 'nativ' && !nativOk) { b.disabled = true; b.title = 'Dieser Browser kennt text-fit noch nicht'; }
    b.addEventListener('click', () => setzeWeiche(b.dataset.weiche));
  });
  new ResizeObserver(() => { if (welt.hidden) return; if (weiche === 'messung') messen(); werte(); }).observe(titel);
  function auftritt() { titel.classList.remove('da'); void titel.offsetWidth; titel.classList.add('da'); }

  // Vorführung: Auftritt, dann wird die Bühne schmaler und wieder breit; jede Zeile passt sich live neu ein
  let uhr = null, laeuft = false, lauf = 0, fahrt = 0;
  const warte = ms => new Promise(r => { uhr = setTimeout(r, ms); });
  const sanft = k => k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2;
  function breite(von, nach, ms) {
    return new Promise(fertig => {
      const t0 = performance.now();
      const schritt = t => { const k = Math.min(1, (t - t0) / ms); buehne.style.setProperty('--tf-breite', (von + (nach - von) * sanft(k)).toFixed(2) + '%'); if (k < 1) fahrt = requestAnimationFrame(schritt); else fertig(); };
      fahrt = requestAnimationFrame(schritt);
    });
  }
  async function vorfuehren() {
    pause(); laeuft = true; const id = ++lauf; scrollTo(0, 0);
    while (laeuft && id === lauf) {
      if (reduziert()) {               // ohne Bewegung: die Weiche im Wechsel zeigen
        setzeWeiche(weiche === 'nativ' && nativOk ? 'messung' : 'nativ'); await warte(3200); continue;
      }
      buehne.style.removeProperty('--tf-breite'); auftritt();
      await warte(1500); if (id !== lauf) return;
      await breite(100, 60, 2200); if (id !== lauf) return;
      await warte(900); if (id !== lauf) return;
      await breite(60, 100, 2200); if (id !== lauf) return;
      await warte(1400);
    }
  }
  function pause() { laeuft = false; lauf++; clearTimeout(uhr); cancelAnimationFrame(fahrt); }
  const fertig = document.fonts ? Promise.all([document.fonts.load('900 100px "Schibsted Grotesk"'), document.fonts.ready]) : Promise.resolve();
  STEUER.textfit = {
    start() { scrollTo(0, 0); fertig.then(() => { setzeWeiche(weiche); auftritt(); }); },
    stopp() { pause(); buehne.style.removeProperty('--tf-breite'); },
    neustart() { pause(); buehne.style.removeProperty('--tf-breite'); scrollTo(0, 0); setzeWeiche(weiche); auftritt(); },
    vorfuehren, pause,
    weiter() { if (html.classList.contains('vorschau') && !laeuft) vorfuehren(); }
  };
})();

/* ---------- Varianten-Umschalter (Regel 13): ?v=basis | textfit ---------- */
(() => {
  const html = document.documentElement;
  const IDS = ['basis', 'textfit'];
  let v = new URLSearchParams(location.search).get('v');
  if (!IDS.includes(v)) v = IDS[0];
  let vorf = false;
  const knoepfe = [...document.querySelectorAll('[data-v-wahl]')];
  function setze(neu, url) {
    const alt = v;
    v = neu; html.dataset.v = neu;
    if (STEUER[alt]) STEUER[alt].stopp();
    document.querySelector('[data-welt="textfit"]').hidden = neu !== 'textfit';
    knoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === neu)));
    if (url) {
      const q = new URLSearchParams(location.search); q.set('v', neu);
      history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + location.hash);
    }
    STEUER[neu].start();
  }
  knoepfe.forEach(b => b.addEventListener('click', () => { setze(b.dataset.vWahl, true); if (vorf) STEUER[v].vorfuehren(); }));
  setze(v, false);

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