Enge Laufweite und Zeilenabstand bei riesigen Headlines
Große Headlines brauchen engere Laufweite und knapperen Zeilenabstand: je größer die Schrift, desto stärker, als Formel an die Größe gekoppelt.
Große Headlines mit negativer Laufweite (-0.03 bis -0.05em) und einem Zeilenabstand von 0.82 bis 0.95. Je größer die Schrift, desto enger Laufweite und Zeilenabstand.
Schriften sind für Lesegrößen zugerichtet. In Displaygröße wirken dieselben Abstände wie Löcher, die Zeilen fallen auseinander. Eng geführt wird die Headline zu einer geschlossenen Form und liest sich wie ein Wort-Bild.
Wann einsetzen
Aufmacher, Exposé- und Projekttitel, Kampagnen-Headlines, alle Headlines ab etwa 4rem.
Wann nicht
Fließtext und kleine Überschriften (dort bleibt die Laufweite nahe 0). Lange Headlines mit vielen Ober- und Unterlängen, die bei 0.85 kollidieren. Schriften, die schon eng zugerichtet sind.
Varianten
- Feste Werte für die größte Stufe, z. B. -0.04em und 0.9 (Fantasy) oder -0.04em und 0.85 (Studio Dumbar)
- An die Größe gekoppelt: Laufweite und Zeilenhöhe als Formel über eine Größenvariable
- Versal-Grotesk mit 0.85 und -0.04em (Build in Amsterdam)
- Sehr groß ab etwa 28vw mit -0.05em und 0.83 (Instrument)
Umsetzung
Die Größe als Zahl in einer Variable führen (--g in rem) und daraus font-size, letter-spacing und line-height rechnen: font-size calc(var(--g) * 1rem); letter-spacing calc((-0.005 - (var(--g) - 2.25) * 0.004828) * 1em); line-height calc(1.08 - (var(--g) - 2.25) * 0.02483). Je Überschriftenstufe oder Breakpoint nur --g setzen. Laufweite immer in em angeben, damit sie mitwächst. Ober- und Unterlängen der konkreten Wörter prüfen, bevor man unter 0.9 geht.
Robust umsetzen
Telefon. Die Formel lockert Laufweite und Zeilenabstand bei kleineren Größen von selbst. Eine feste -0.04em wäre auf dem Telefon zu eng.
Ohne Bewegung. Nicht relevant, das Muster bewegt sich nicht. Animierte Wechsel der Werte entfallen bei reduzierter Bewegung.
Barrierefrei. Laufweite nur bei großen Größen verengen; Buchstaben dürfen sich nicht berühren. Text als echten Text setzen, nicht als Bild. Die Headline bleibt ein <h1>, Zeilen nur per display: block trennen, nicht mit <br> in einzelne Überschriften zerlegen.
Code
<!DOCTYPE html>
<!--
Enge Laufweite und Zeilenabstand bei riesigen Headlines
Fundus · Typo-Techniken · https://cecodesign.de/lab/fundus/typo-techniken/headline-laufweite-zeilenabstand/
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>Enge Laufweite und Zeilenabstand bei riesigen Headlines · 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: 'Libre Caslon Display'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/libre-caslon-display@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Hanken Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hanken-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
:root {
/* Rezept „Immobilien: diskret und hochwertig“ */
--grund: #fcfaf5; --sand: #efe8df; --marke: #02385c; --text: #313131; --meta: #b5b2af; --cta: #b85a0c;
--leise: rgba(49, 49, 49, .7); --linie: rgba(2, 56, 92, .14);
--kurve: cubic-bezier(0.165, 0.84, 0.44, 1); /* easeOutQuart */
--ui: 200ms; --menue: 400ms; --lang: 900ms; --bild: 1100ms;
/* Das Muster: Größe als Zahl (rem), Laufweite und Zeilenabstand folgen ihr */
--g: 9.5;
--lw: calc((-0.005 - (var(--g) - 2.25) * 0.004828) * 1em); /* -0.005em bei 2.25rem … -0.04em bei 9.5rem */
--zh: calc(1.08 - (var(--g) - 2.25) * 0.02483); /* 1.08 bei 2.25rem … 0.90 bei 9.5rem */
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font: 400 17px/1.55 'Hanken Grotesk', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; padding: clamp(14px, 2.4vw, 32px) clamp(16px, 3.4vw, 52px) 64px; }
.label { font: 500 .75rem/1.4 'Hanken Grotesk', sans-serif; letter-spacing: .09em; text-transform: uppercase; color: var(--leise); }
.ziff { font-variant-numeric: tabular-nums; }
a { color: inherit; text-decoration: none; }
:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; }
/* ——— Bedienleiste (im Fluss) ——— */
.leiste { max-width: 1320px; margin: 0 auto; display: flex; flex-wrap: wrap; align-items: center; gap: 12px 26px; padding: 10px 0 18px; border-bottom: 1px solid var(--linie); }
.gruppe { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.wahl { display: inline-flex; gap: 2px; padding: 3px; border: 1px solid var(--linie); border-radius: 999px; }
.wahl button, .schalter {
font: inherit; letter-spacing: .06em; text-transform: none; color: var(--leise);
background: none; border: 0; border-radius: 999px; padding: 7px 13px; cursor: pointer;
transition: color var(--ui) var(--kurve), background-color var(--ui) var(--kurve);
}
.wahl button:hover, .schalter:hover { color: var(--marke); }
.wahl button[aria-pressed="true"] { background: var(--marke); color: var(--grund); }
.schalter { border: 1px solid var(--linie); display: inline-flex; align-items: center; gap: 8px; letter-spacing: .09em; text-transform: uppercase; }
.schalter::before { content: ""; width: 7px; height: 7px; border-radius: 50%; border: 1px solid currentColor; }
.schalter[aria-pressed="true"] { color: var(--marke); }
.schalter[aria-pressed="true"]::before { background: var(--marke); }
.wahl button { white-space: nowrap; }
.regler { display: flex; align-items: center; gap: 12px; }
.regler input { width: clamp(120px, 16vw, 220px); accent-color: var(--marke); }
.regler output { min-width: 6.2em; color: var(--marke); }
.leiste .rechts { margin-left: auto; }
/* ——— Exposé ——— */
.blatt { max-width: 1320px; margin: 0 auto; }
.kopf { display: flex; align-items: center; gap: 28px; padding: 22px 0; border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: baseline; gap: 12px; color: var(--marke); }
.marke b { font: 650 15px/1 'Hanken Grotesk', sans-serif; letter-spacing: .24em; text-transform: uppercase; }
.marke span { font-size: 13px; color: var(--leise); letter-spacing: .02em; }
.nav { display: flex; gap: 26px; margin-left: auto; font-size: 15px; }
.nav a { transition: color var(--ui) var(--kurve); }
.nav a:hover { color: var(--marke); }
.cta { display: inline-flex; align-items: center; gap: 10px; padding: 12px 20px; background: var(--cta); color: #fff; font: 600 14px/1 'Hanken Grotesk', sans-serif; letter-spacing: .02em; border-radius: 2px; transition: background-color var(--ui) var(--kurve); white-space: nowrap; }
.cta:hover { background: #a4500a; }
.aufmacher { position: relative; padding: clamp(28px, 4vw, 56px) 0 clamp(26px, 3.4vw, 48px); }
.aufmacher { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 32ch); column-gap: 40px; }
.aufmacher > .label { grid-column: 1 / -1; }
.titel { grid-column: 1 / -1; grid-row: 2; }
.vorspann { grid-column: 2; grid-row: 2; align-self: start; font-size: 16px; line-height: 1.55; color: var(--text); padding-top: 6px; position: relative; z-index: 1; }
.aufmacher > .label { display: flex; gap: 18px; flex-wrap: wrap; margin-bottom: clamp(18px, 2.2vw, 30px); }
.aufmacher > .label span + span::before { content: "·"; margin-right: 18px; color: var(--meta); }
/* Die Headline */
.titel {
font: 400 calc(var(--g) * 1rem)/var(--zh) 'Libre Caslon Display', Georgia, serif;
letter-spacing: var(--lw);
color: var(--marke); font-kerning: normal;
transition: font-size var(--lang) var(--kurve), letter-spacing var(--lang) var(--kurve), line-height var(--lang) var(--kurve);
}
.titel { padding-top: 30px; }
.titel .z { display: block; width: max-content; max-width: 100%; }
.titel .z2 { padding-left: .9em; }
[data-art="standard"] .titel { letter-spacing: 0; line-height: 1.2; }
[data-art="fest"] .titel { letter-spacing: -0.04em; line-height: .9; }
.ziehen .titel { transition: none; }
/* Messebene */
.mess { position: absolute; inset: 0; pointer-events: none; transition: opacity var(--menue) var(--kurve); }
.aus .mess { opacity: 0; }
.mess > * { position: absolute; }
.ml { height: 0; border-top: 1px solid rgba(2, 56, 92, .35); }
.ml.kap { border-top-style: dashed; }
.mass { border-left: 1px solid var(--marke); }
.mass::before, .mass::after { content: ""; position: absolute; left: -4px; width: 7px; border-top: 1px solid var(--marke); }
.mass::before { top: 0; } .mass::after { bottom: 0; }
.breite { height: 0; border-top: 1px solid var(--marke); }
.breite::before, .breite::after { content: ""; position: absolute; top: -4px; height: 7px; border-left: 1px solid var(--marke); }
.breite::before { left: 0; } .breite::after { right: 0; }
.schild { font: 600 11px/1 'Hanken Grotesk', sans-serif; letter-spacing: .06em; color: var(--grund); background: var(--marke); padding: 4px 6px; border-radius: 2px; white-space: nowrap; font-variant-numeric: tabular-nums; }
.schild i { font-style: normal; font-weight: 400; opacity: .7; }
/* Daten + Bild */
.unten { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: clamp(24px, 4vw, 64px); align-items: start; }
.daten { display: grid; grid-template-columns: auto 1fr; align-items: baseline; border-top: 1px solid var(--marke); }
.daten dt, .daten dd { padding: 11px 0; border-bottom: 1px solid var(--linie); }
.daten dt { padding-right: 24px; }
.daten dd { text-align: right; font-variant-numeric: tabular-nums; color: var(--text); }
.daten dd.preis { font-weight: 600; color: var(--marke); }
.pflicht { margin-top: 14px; font-size: 13px; line-height: 1.5; color: var(--leise); max-width: 46ch; }
.aktionen { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; margin-top: 22px; }
.zweit { font-weight: 500; color: var(--marke); box-shadow: inset 0 -1px 0 currentColor; padding-bottom: 2px; }
.ansicht { position: relative; background: var(--sand); aspect-ratio: 4 / 3; overflow: hidden; }
.ansicht svg { display: block; width: 100%; height: 100%; transform-origin: 50% 70%; }
.ansicht figcaption { position: absolute; left: 16px; bottom: 14px; }
.js .ansicht { clip-path: inset(0 0 100% 0); }
.js .ansicht svg { transform: scale(1.06); }
.js .ansicht.da { clip-path: inset(0 0 0 0); transition: clip-path var(--bild) var(--kurve); }
.js .ansicht.da svg { transform: none; transition: transform var(--bild) var(--kurve); }
/* ——— Werte und Größenleiter ——— */
.werte { max-width: 1320px; margin: 40px auto 0; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 28px 56px; padding-top: 22px; border-top: 1px solid var(--linie); font-size: 13px; }
.werte h2 { font: 600 .75rem/1.4 'Hanken Grotesk', sans-serif; letter-spacing: .09em; text-transform: uppercase; color: var(--marke); margin-bottom: 10px; }
.werte dl div { display: flex; gap: 14px; padding: 5px 0; border-bottom: 1px solid var(--linie); }
.werte dt { min-width: 11em; color: var(--leise); }
.werte dd { font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.leiter { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.leiter th { text-align: left; font-weight: 500; color: var(--leise); padding: 5px 0; border-bottom: 1px solid var(--linie); }
.leiter td { padding: 5px 0; border-bottom: 1px solid var(--linie); transition: color var(--ui) var(--kurve); }
.leiter tr.jetzt td { color: var(--marke); font-weight: 600; }
/* ——— Vorschau ——— */
.zustand { display: none; }
.vorschau body { overflow: hidden; padding: 22px 36px; }
.vorschau .leiste, .vorschau .werte, .vorschau .kopf .nav, .vorschau .vorspann { display: none; }
.vorschau .zustand { display: flex; justify-content: space-between; gap: 16px; max-width: 1320px; margin: 0 auto 8px; }
.vorschau .zustand b { color: var(--marke); font-weight: 600; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
:root.reduziert .ansicht { clip-path: none; }
:root.reduziert .ansicht svg { transform: none; }
@media (max-width: 1100px) {
.aufmacher { grid-template-columns: minmax(0, 1fr); }
.vorspann { grid-column: 1; grid-row: 3; max-width: 52ch; padding-top: clamp(18px, 3vw, 28px); }
}
@media (max-width: 900px) {
.unten { grid-template-columns: 1fr; }
.ansicht { order: -1; }
.nav { display: none; }
.kopf { justify-content: space-between; }
.werte { grid-template-columns: 1fr; }
}
@media (max-width: 600px) {
.marke span { display: none; }
.cta { padding: 11px 14px; font-size: 13px; }
.leiste { gap: 10px 14px; }
.leiste .rechts { margin-left: 0; }
.wahl button { padding: 8px 11px; }
.zus { display: none; }
.aufmacher > .label span:nth-child(3) { display: none; }
.regler input { width: 150px; }
.titel .z2 { padding-left: .5em; }
.werte dt { min-width: 8.5em; }
}
@media (hover: none) {
.wahl button:hover, .schalter:hover, .nav a:hover { color: inherit; }
.wahl button[aria-pressed="true"]:hover { color: var(--grund); }
.cta:hover { background: var(--cta); }
.regler input { height: 28px; }
}
/* 320 px (WCAG 1.4.10) */
@media (max-width: 360px) {
.kopf { gap: 12px; }
.marke b { letter-spacing: .16em; }
.cta { padding: 10px 11px; }
.regler input { width: 120px; }
.regler output { min-width: 5.6em; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<div class="leiste label" role="toolbar" aria-label="Steuerung der Demo">
<div class="gruppe">
<span>Laufweite + Zeilenabstand</span>
<div class="wahl" role="group" aria-label="Satzart der Headline">
<button type="button" data-art="standard" aria-pressed="false">Standard<span class="zus"> 0 / 1.2</span></button>
<button type="button" data-art="fest" aria-pressed="false">Fest<span class="zus"> −.04em / .9</span></button>
<button type="button" data-art="gleitend" aria-pressed="true">Folgt der Größe</button>
</div>
</div>
<label class="regler">Größe <input type="range" min="2.25" max="12" step="0.05" value="9.5" data-groesse aria-label="Schriftgröße der Headline in rem"> <output class="ziff" data-groesse-wert>9.50 rem</output></label>
<button type="button" class="schalter rechts" data-mess aria-pressed="true">Messlinien</button>
</div>
<p class="zustand label" aria-hidden="true"><span>Headline · <b data-zustand>Folgt der Größe</b></span><span class="ziff" data-zustand-wert></span></p>
<div class="blatt" data-blatt data-art="gleitend">
<header class="kopf">
<a class="marke" href="#start"><b>Hallgarten</b><span>Immobilien in Wiesbaden seit 1971</span></a>
<nav class="nav" aria-label="Beispielnavigation"><a href="#kaufen">Kaufen</a><a href="#verkaufen">Verkaufen</a><a href="#bewerten">Bewerten</a><a href="#almanach">Marktbericht</a></nav>
<a class="cta" href="#anfrage">Besichtigung anfragen</a>
</header>
<section class="aufmacher" data-aufmacher>
<p class="label"><span>Exposé W-2611</span><span>Wiesbaden-Nordost</span><span>Verfügbar ab Frühjahr 2027</span></p>
<p class="vorspann">Ein Haus aus der Gründerzeit, 2019 behutsam saniert: Deckenhöhen bis 3,60 m, Eichenparkett, ein Garten mit altem Baumbestand. Fünf Gehminuten bis zur Nerobergbahn.</p>
<h1 class="titel" data-titel><span class="z z1">Stadtvilla</span><span class="z z2">im Nerotal</span></h1>
<div class="mess" data-ebene aria-hidden="true"></div>
</section>
<div class="unten">
<div>
<dl class="daten">
<dt class="label">Wohnfläche</dt><dd>312 m²</dd>
<dt class="label">Zimmer</dt><dd>8</dd>
<dt class="label">Grundstück</dt><dd>1.140 m²</dd>
<dt class="label">Baujahr</dt><dd>1898, saniert 2019</dd>
<dt class="label">Kaufpreis</dt><dd class="preis">3.450.000 €</dd>
</dl>
<p class="pflicht ziff">Energieausweis: Verbrauch, 128 kWh/(m²·a), Klasse D, Gas. Käuferprovision 3,57 % inkl. MwSt. vom Kaufpreis.</p>
<div class="aktionen"><a class="zweit" href="#grundriss">Grundrisse ansehen</a></div>
</div>
<figure class="ansicht" data-ansicht>
<svg viewBox="0 0 400 300" role="img" aria-label="Zeichnung der Straßenansicht einer Gründerzeitvilla mit Mittelrisalit, Walmdach und Garten">
<defs>
<pattern id="schraffur" width="4" height="4" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="4" stroke="#02385c" stroke-width=".5" opacity=".35"/></pattern>
</defs>
<g fill="#e4dacc">
<circle cx="44" cy="190" r="40"/><circle cx="74" cy="160" r="34"/><circle cx="26" cy="150" r="26"/>
<circle cx="352" cy="176" r="38"/><circle cx="378" cy="206" r="30"/><circle cx="330" cy="214" r="24"/>
</g>
<g stroke="#02385c" stroke-width="1" fill="none" stroke-linejoin="round" vector-effect="non-scaling-stroke">
<!-- Kamine -->
<rect x="128" y="64" width="11" height="28" fill="#fcfaf5"/><rect x="262" y="70" width="11" height="22" fill="#fcfaf5"/>
<!-- Walmdach -->
<path d="M82 124 L122 88 L278 88 L318 124 Z" fill="url(#schraffur)"/>
<path d="M82 124 L122 88 L278 88 L318 124 Z"/>
<!-- Hauptbaukörper -->
<rect x="90" y="124" width="220" height="140" fill="#fcfaf5"/>
<line x1="86" y1="124" x2="314" y2="124"/><line x1="86" y1="127" x2="314" y2="127"/>
<line x1="90" y1="188" x2="310" y2="188"/>
<line x1="90" y1="248" x2="310" y2="248"/>
<!-- Mittelrisalit mit Giebel -->
<rect x="164" y="112" width="72" height="152" fill="#fcfaf5"/>
<path d="M156 114 L200 72 L244 114 Z" fill="#fcfaf5"/><path d="M164 112 L200 80 L236 112"/>
<circle cx="200" cy="99" r="7"/><circle cx="200" cy="99" r="3.5"/>
<!-- Fenster Obergeschoss -->
<g>
<path d="M104 178 V150 a9 9 0 0 1 18 0 V178 Z"/><path d="M134 178 V150 a9 9 0 0 1 18 0 V178 Z"/>
<path d="M248 178 V150 a9 9 0 0 1 18 0 V178 Z"/><path d="M278 178 V150 a9 9 0 0 1 18 0 V178 Z"/>
<path d="M176 178 V144 a24 24 0 0 1 48 0 V178 Z"/><line x1="192" y1="178" x2="192" y2="128"/><line x1="208" y1="178" x2="208" y2="128"/>
<line x1="100" y1="181" x2="156" y2="181"/><line x1="244" y1="181" x2="300" y2="181"/>
</g>
<!-- Balkon -->
<path d="M170 186 H230 V196 H170 Z" fill="#fcfaf5"/>
<g opacity=".8"><line x1="176" y1="186" x2="176" y2="196"/><line x1="184" y1="186" x2="184" y2="196"/><line x1="192" y1="186" x2="192" y2="196"/><line x1="200" y1="186" x2="200" y2="196"/><line x1="208" y1="186" x2="208" y2="196"/><line x1="216" y1="186" x2="216" y2="196"/><line x1="224" y1="186" x2="224" y2="196"/></g>
<!-- Erdgeschoss -->
<path d="M104 240 V210 a9 9 0 0 1 18 0 V240 Z"/><path d="M134 240 V210 a9 9 0 0 1 18 0 V240 Z"/>
<path d="M248 240 V210 a9 9 0 0 1 18 0 V240 Z"/><path d="M278 240 V210 a9 9 0 0 1 18 0 V240 Z"/>
<path d="M186 264 V218 a14 14 0 0 1 28 0 V264" fill="#efe8df"/><line x1="200" y1="216" x2="200" y2="264"/>
<!-- Treppe, Sockel, Gelände -->
<path d="M178 264 H222 M174 268 H226 M170 272 H230"/>
<line x1="20" y1="264" x2="380" y2="264"/>
<path d="M20 278 H160 M240 278 H380" opacity=".55"/>
<g opacity=".55"><path d="M30 278 V270 M40 278 V270 M50 278 V270 M60 278 V270 M70 278 V270 M80 278 V270 M90 278 V270 M100 278 V270 M110 278 V270 M120 278 V270 M130 278 V270 M140 278 V270 M150 278 V270 M250 278 V270 M260 278 V270 M270 278 V270 M280 278 V270 M290 278 V270 M300 278 V270 M310 278 V270 M320 278 V270 M330 278 V270 M340 278 V270 M350 278 V270 M360 278 V270 M370 278 V270"/></g>
</g>
</svg>
<figcaption class="label">Straßenansicht, Zeichnung</figcaption>
</figure>
</div>
</div>
<section class="werte" aria-label="Verwendete Werte">
<div>
<h2>Headline jetzt</h2>
<dl data-werte></dl>
</div>
<div>
<h2>Größenleiter „Folgt der Größe“</h2>
<table class="leiter">
<thead><tr><th>font-size</th><th>letter-spacing</th><th>line-height</th><th>Einsatz</th></tr></thead>
<tbody data-leiter></tbody>
</table>
</div>
</section>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, w = document) => w.querySelector(s);
const $$ = (s, w = document) => [...w.querySelectorAll(s)];
const blatt = $('[data-blatt]'), titel = $('[data-titel]'), ebene = $('[data-ebene]'), aufmacher = $('[data-aufmacher]');
const regler = $('[data-groesse]'), reglerWert = $('[data-groesse-wert]'), messKnopf = $('[data-mess]');
const zeilen = $$('.z', titel);
const NAMEN = { standard: 'Standard 0 / 1.2', fest: 'Fest −.04em / .9', gleitend: 'Folgt der Größe' };
const MIN = 2.25, MAX = 12;
let art = 'gleitend', g = 9.5, mess = true, handGroesse = false;
/* Formeln des Musters (identisch mit --lw / --zh im CSS) */
const lwVon = (a, gg) => a === 'standard' ? 0 : a === 'fest' ? -0.04 : -0.005 - (gg - 2.25) * 0.004828;
const zhVon = (a, gg) => a === 'standard' ? 1.2 : a === 'fest' ? 0.9 : 1.08 - (gg - 2.25) * 0.02483;
/* Startgröße aus der Fensterbreite: clamp(2.75rem, 2.45rem + 8.84vw, 12rem) */
const ausFenster = () => Math.max(2.75, Math.min(12, 2.45 + 8.84 * innerWidth / 100 / 16));
const zahl = (v, n = 3) => (v < 0 ? '−' : '') + Math.abs(v).toFixed(n).replace(/^0(?=\.)/, '');
/* Marken für Grundlinie und Versalhöhe je Zeile */
zeilen.forEach(z => {
const m = document.createElement('i'); m.style.cssText = 'display:inline-block;width:0;height:0;vertical-align:baseline'; z.prepend(m);
const k = document.createElement('i'); k.style.cssText = 'position:absolute;visibility:hidden;height:0;width:10cap'; z.append(k);
});
const kannCap = !!(window.CSS && CSS.supports('width', '1cap'));
function linie(cls, top, left, width, height) {
const d = document.createElement('div'); d.className = cls;
d.style.top = top + 'px'; d.style.left = left + 'px';
if (width != null) d.style.width = width + 'px';
if (height != null) d.style.height = height + 'px';
ebene.append(d); return d;
}
function schild(html, top, left) { const d = linie('schild', top, left); d.innerHTML = html; return d; }
function zeichnen() {
ebene.innerHTML = '';
const a = aufmacher.getBoundingClientRect();
const fs = parseFloat(getComputedStyle(titel).fontSize);
const g1 = zeilen[0].firstChild.getBoundingClientRect().top - a.top;
const g2 = zeilen[1].firstChild.getBoundingClientRect().top - a.top;
const kap = kannCap ? zeilen[1].lastChild.getBoundingClientRect().width / 10 : fs * .66;
const r1 = zeilen[0].getBoundingClientRect(), r2 = zeilen[1].getBoundingClientRect();
const links = r1.left - a.left, rechts = a.width;
const lsPx = parseFloat(getComputedStyle(titel).letterSpacing) || 0;
const wortBreite = r1.width - lsPx; /* ohne die Laufweite nach dem letzten Buchstaben */
linie('ml', g1, links, rechts - links);
linie('ml', g2, links, rechts - links);
linie('ml kap', g2 - kap, links, rechts - links);
/* Zeilenabstand (Grundlinie zu Grundlinie) und Durchschuss (Grundlinie bis Versalhöhe) */
const x = Math.min(rechts - 14, Math.max(r1.right, r2.right) - a.left + 34);
const passt = rechts - x > 190;
const sx = passt ? x : rechts - 8;
linie('mass', g1, sx, null, g2 - g1);
if (passt) {
schild(`Zeilenabstand ${Math.round(g2 - g1)} px <i>${zahl(zhVon(art, g), 2)}</i>`, g1 + (g2 - g1) / 2 - 22, sx + 12);
schild(`Lücke bis Versalhöhe ${Math.round(g2 - kap - g1)} px`, g1 + (g2 - g1) / 2 + 2, sx + 12);
}
/* Wortbreite mit Laufweite */
const oben = r1.top - a.top + fs * .06;
linie('breite', oben, links, wortBreite);
schild(`„Stadtvilla“ ${Math.round(wortBreite)} px <i>· ${zahl(lwVon(art, g))}em = ${lsPx.toFixed(1).replace('-', '−')} px je Zeichen</i>`, oben - 24, links);
werte(fs, lsPx, g2 - g1);
}
let raf = 0;
function nachfuehren(ms) {
cancelAnimationFrame(raf);
const ende = performance.now() + (reduziert() ? 0 : ms);
const schritt = () => { zeichnen(); if (performance.now() < ende) raf = requestAnimationFrame(schritt); };
schritt();
}
/* Werte und Größenleiter */
const werteListe = $('[data-werte]'), leiter = $('[data-leiter]');
const STUFEN = [[2.25, 'h3, Teaser'], [3.5, 'h2'], [5.5, 'h1 Tablet'], [7.5, 'h1'], [9.5, 'Aufmacher'], [12, 'Aufmacher, sehr groß']];
function werte(fs, lsPx, zabst) {
const z = [
['Satzart', NAMEN[art]],
['font-size', `${g.toFixed(2)}rem (${Math.round(fs)} px)`],
['letter-spacing', art === 'gleitend' ? `${zahl(lwVon(art, g))}em (${lsPx.toFixed(2).replace('-', '−')} px)` : `${art === 'fest' ? '−.04em' : '0'}`],
['line-height', `${zahl(zhVon(art, g), 3)} (${Math.round(zabst)} px)`],
['Formel Laufweite', 'calc((-0.005 - (var(--g) - 2.25) * 0.004828) * 1em)'],
['Formel Zeilenhöhe', 'calc(1.08 - (var(--g) - 2.25) * 0.02483)'],
['Schrift', 'Libre Caslon Display 400, #02385C'],
['Übergang', '900 ms cubic-bezier(0.165, 0.84, 0.44, 1)'],
];
werteListe.innerHTML = z.map(([k, v]) => `<div><dt>${k}</dt><dd>${v}</dd></div>`).join('');
let nah = 0; STUFEN.forEach((s, i) => { if (Math.abs(s[0] - g) < Math.abs(STUFEN[nah][0] - g)) nah = i; });
leiter.innerHTML = STUFEN.map(([s, e], i) => `<tr class="${i === nah && art === 'gleitend' ? 'jetzt' : ''}"><td>${s.toFixed(2)}rem</td><td>${zahl(lwVon('gleitend', s))}em</td><td>${zhVon('gleitend', s).toFixed(2)}</td><td>${e}</td></tr>`).join('');
$('[data-zustand]').textContent = NAMEN[art];
$('[data-zustand-wert]').textContent = `${g.toFixed(2)}rem · ${art === 'standard' ? '0' : zahl(lwVon(art, g))}em · ${zahl(zhVon(art, g), 2)}`;
}
/* Zustände */
function setzeArt(a) {
art = a; blatt.dataset.art = a;
$$('[data-art]').forEach(k => { if (k.tagName === 'BUTTON') k.setAttribute('aria-pressed', String(k.dataset.art === a)); });
nachfuehren(1000);
}
function setzeGroesse(v, sofort) {
g = Math.max(MIN, Math.min(MAX, v));
blatt.style.setProperty('--g', g.toFixed(3));
regler.value = g; reglerWert.textContent = g.toFixed(2) + ' rem';
if (sofort) { blatt.classList.add('ziehen'); zeichnen(); requestAnimationFrame(() => blatt.classList.remove('ziehen')); }
else nachfuehren(1000);
}
function setzeMess(an) { mess = an; messKnopf.setAttribute('aria-pressed', String(an)); blatt.classList.toggle('aus', !an); }
$$('button[data-art]').forEach(k => k.addEventListener('click', () => { pause(); setzeArt(k.dataset.art); }));
regler.addEventListener('input', () => { pause(); handGroesse = true; setzeGroesse(parseFloat(regler.value), true); });
messKnopf.addEventListener('click', () => { pause(); setzeMess(!mess); });
$$('a[href^="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
addEventListener('resize', () => { if (!handGroesse) setzeGroesse(ausFenster(), true); else zeichnen(); });
/* Gleiten der Größe für die Vorführung (gleiche Kurve wie im CSS) */
const easeOutQuart = t => 1 - Math.pow(1 - t, 4);
let gleitRaf = 0;
function gleite(ziel, ms) {
cancelAnimationFrame(gleitRaf);
if (reduziert()) { setzeGroesse(ziel, true); return; }
const start = g, t0 = performance.now();
const schritt = now => {
const t = Math.min(1, (now - t0) / ms);
setzeGroesse(start + (ziel - start) * easeOutQuart(t), true);
if (t < 1) gleitRaf = requestAnimationFrame(schritt);
};
gleitRaf = requestAnimationFrame(schritt);
}
/* Vorführung */
let uhren = [];
function pause() { uhren.forEach(clearTimeout); uhren = []; cancelAnimationFrame(gleitRaf); }
function neustart() {
pause(); handGroesse = false;
setzeMess(true); setzeArt('gleitend'); setzeGroesse(ausFenster(), true);
}
function vorfuehren() {
pause();
const G = html.classList.contains('vorschau') ? 9 : ausFenster(), klein = Math.max(2.75, Math.min(4, G * .45));
const plan = [
[0, () => { setzeMess(true); setzeGroesse(G, true); setzeArt('standard'); }],
[1400, () => setzeArt('gleitend')],
[3600, () => gleite(klein, 1600)],
[5800, () => setzeArt('fest')],
[7600, () => setzeArt('gleitend')],
[9200, () => gleite(G, 1600)],
];
plan.forEach(([t, f]) => uhren.push(setTimeout(f, t)));
uhren.push(setTimeout(vorfuehren, 11600));
}
function weiter() { vorfuehren(); }
/* Start */
setzeGroesse(ausFenster(), true);
const bildZeigen = () => $('[data-ansicht]').classList.add('da');
(document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve()).then(() => {
document.fonts.load('400 40px "Libre Caslon Display"').catch(() => {}).then(() => { zeichnen(); requestAnimationFrame(bildZeigen); });
});
setTimeout(bildZeigen, 1200);
})();
</script>
</body>
</html>