Zum Inhalt
← Typo-Techniken

Kondensierte Versal-Headlines auf volle Breite (fit-to-width)

Mission eines Bochumer Schwimmvereins in Big Shoulders: jede Versalzeile füllt die volle Breite, über die Schriftgröße je Zeile oder über die Breitenachse von Archivo.

Headlines in Versalien und kondensierten Schnitten, Zeile für Zeile gestapelt. Jede Zeile füllt die volle Breite, als wäre sie im Blocksatz gesetzt. In Produktion entsteht das über eine Schriftgröße in vw, nicht über JavaScript.

Die Headline wird zum Plakat: eine geschlossene Fläche mit harten Kanten links und rechts. Schmale Versalien bringen viele Buchstaben in eine Zeile und bleiben trotzdem riesig. Das wirkt laut, selbstbewusst und sofort lesbar.

Wann einsetzen

Für eine kurze Botschaft mit Haltung: Mission eines Vereins, Kampagne, Festival, Sport, Marken mit lautem Auftritt, Seitentitel und Wortmarken im Footer.

Wann nicht

Bei langen oder oft wechselnden Texten, bei ruhigen, leisen Marken und bei mehrsprachigen Seiten, wenn die Zeilen in jeder Sprache neu gemessen werden müssten. Bei Fließtext nie.

Varianten

  • Größe je Zeile in vw: jede Zeile bekommt eine eigene Schriftgröße (Zentry: Root-Größe .625vw, Titel 10rem = 6.25vw)
  • Eine Zeile über die volle Breite mit white-space: nowrap (Instrument: font-size 27.69vw)
  • Breitenachse: alle Zeilen gleich groß, die wdth-Achse einer Variable Font füllt die Breite (Instrument Sans mit 'wdth' 0 als schmalste Breite)
  • Fester kondensierter Schnitt mit enger Laufweite (Fantasy: Suisse Intl Condensed Bold, line-height .85, letter-spacing -.02em)

Umsetzung

Schrift laden, jede Zeile als eigenes Element mit white-space: nowrap setzen. Die Breite der Zeile einmal in em messen (Breite bei 100 px geteilt durch 100) und als Faktor eintragen: font-size: calc(100vw / Faktor), bei Polstern calc((100vw - 2 * Rand) / Faktor). In Komponenten 100cqi statt 100vw. Zeilenhöhe .8 bis .85, Laufweite leicht negativ. Für Text aus dem CMS oder die Breitenachse braucht es JavaScript: einmal messen, bei Größenänderung per ResizeObserver neu rechnen.

Robust umsetzen

Telefon. Die Zeilen bleiben bündig, werden aber klein. Für schmale Bildschirme die Zeilen neu aufteilen (z. B. fünf statt drei kürzere Zeilen per Container- oder Media-Query), damit die Headline groß bleibt.

Ohne Bewegung. Nicht relevant, die Technik hat keine Bewegung.

Barrierefrei. Den ganzen Satz einmal als Text im h1 lassen und die gestapelten Zeilen für Screenreader ausblenden, sonst werden sie als Bruchstücke vorgelesen. Versalien per CSS setzen, nicht im Quelltext. Prüfen, dass Browser-Zoom und eigene Schriftgrößen weiter wirken: reine vw-Größen wachsen beim Zoom nicht mit, ein rem-Anteil in clamp() oder calc() hilft.

Code

<!DOCTYPE html>
<!--
  Kondensierte Versal-Headlines auf volle Breite (fit-to-width)
  Fundus · Typo-Techniken · https://cecodesign.de/lab/fundus/typo-techniken/versal-kondensiert-fit-to-width/
  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>Kondensierte Versal-Headlines auf volle Breite (fit-to-width) · 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: 'Big Shoulders'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/big-shoulders: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; }
@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 {
  /* Rezept verein-nonprofit-wirkung */
  --weiss: #ffffff; --hell: #edf5fa; --navy: #002e6d; --navy-tief: #01214f;
  --tinte: #222520; --leise: #5b6470; --grau: #dfdfd7; --linie: rgba(0, 46, 109, .14);
  --spende: #ffca0a; --wasser: #008fc3; --mess: #007caa;   /* Messfarbe dunkler als Wasser: 4,7:1 auf Weiß */
  --kurve: cubic-bezier(0.645, 0.045, 0.355, 1);   /* easeInOutCubic */
  --ui: 180ms var(--kurve);
  --gross: 800ms var(--kurve);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--hell); color: var(--tinte); font-family: 'Archivo', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; padding: clamp(14px, 2.4vw, 32px) clamp(16px, 3.4vw, 48px) 40px; }
.mono { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
button, input { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--navy); outline-offset: 3px; }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ——— Demo-Leiste im Fluss ——— */
.leiste { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 24px; padding-bottom: 18px; color: var(--leise); }
.umschalter { display: flex; gap: 2px; border: 1px solid var(--linie); border-radius: 999px; padding: 3px; background: #fff; }
.umschalter button { letter-spacing: inherit; text-transform: inherit; font-size: inherit; color: var(--leise); background: none; border: 0; border-radius: 999px; padding: 7px 12px; cursor: pointer; transition: color var(--ui), background-color var(--ui); }
.umschalter button:hover { color: var(--navy); }
.umschalter button[aria-pressed="true"] { background: var(--navy); color: #fff; }
.regler { display: flex; align-items: center; gap: 12px; }
.regler input { width: clamp(120px, 16vw, 200px); accent-color: var(--navy); }
.regler output { min-width: 4.5em; text-align: right; color: var(--navy); font-variant-numeric: tabular-nums; }

/* ——— Bühne: Rahmen mit veränderbarer Breite ——— */
.buehne { position: relative; }
.halter { position: relative; width: 100%; }
.halter::before { content: ""; position: absolute; inset: 0; border: 1px dashed rgba(0, 46, 109, .22); border-radius: 6px; pointer-events: none; }
.rahmen { position: relative; width: 100%; min-width: 300px; background: var(--weiss); border-radius: 6px; overflow: hidden; box-shadow: 0 1px 0 rgba(0, 0, 0, .03), 0 26px 60px -36px rgba(0, 33, 79, .45); }
.griff { position: absolute; top: 140px; right: -15px; z-index: 3; width: 10px; height: 64px; border-radius: 6px; background: var(--navy); cursor: ew-resize; touch-action: none; box-shadow: 0 0 0 4px var(--hell); transition: scale var(--ui), background-color var(--ui); }
.griff::before { content: ""; position: absolute; inset: 22px 3px; border-block: 1px solid rgba(255, 255, 255, .6); }
.griff::after { content: ""; position: absolute; inset: -12px -14px; }
@media (max-width: 860px) { .griff::after { right: -4px; } }   /* Greiffläche ragt sonst über den Rand (Querscrollen bei 200 %) */
.griff:hover, .griff.zieht, .vorfuehrung .griff { background: var(--wasser); scale: 1.15; }

/* ——— Seite: Kopfsprung e.V. ——— */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 16px clamp(16px, 2.8vw, 40px); }
.logo { display: flex; align-items: center; gap: 10px; color: var(--navy); text-decoration: none; }
.logo svg { width: 30px; height: 30px; flex: none; }
.logo b { font: 800 1.625rem/1 'Big Shoulders', 'Archivo', sans-serif; letter-spacing: .01em; text-transform: uppercase; }
.logo small { font-size: .8125rem; font-weight: 600; margin-left: 5px; }
.nav { display: flex; gap: clamp(14px, 2vw, 28px); font-weight: 500; }
.nav a { color: var(--navy); text-decoration: none; padding: 4px 0; border-bottom: 2px solid transparent; transition: border-color var(--ui); }
.nav a:hover { border-color: var(--navy); }
.spenden { display: inline-flex; align-items: center; gap: 8px; height: 46px; padding: 0 20px; border-radius: 3px; background: var(--spende); color: var(--tinte); font-weight: 650; text-decoration: none; white-space: nowrap; transition: background-color var(--ui); }
.spenden:hover { background: #f2bc00; }
.spenden svg { width: 16px; height: 16px; }

.held { padding: clamp(14px, 2vw, 26px) clamp(16px, 2.8vw, 40px) clamp(22px, 3vw, 40px); }
.oben { display: flex; justify-content: space-between; align-items: baseline; gap: 6px 20px; flex-wrap: wrap; color: var(--navy); font-weight: 600; font-size: .9375rem; }
.oben .mono { color: var(--leise); font-weight: 500; }

/* Lineal über der Headline: zeigt, was 100cqi ist */
.lineal { position: relative; height: 22px; margin-top: 16px; color: var(--mess); }
.lineal::before { content: ""; position: absolute; left: 0; right: 0; top: 50%; border-top: 1px solid currentColor; opacity: .45; }
.lineal::after { content: ""; position: absolute; left: 0; right: 0; top: calc(50% - 5px); height: 10px; border-inline: 1px solid currentColor; }
.lineal span { position: absolute; left: 50%; top: 50%; translate: -50% -50%; background: var(--weiss); padding: 0 10px; white-space: nowrap; font-variant-numeric: tabular-nums; letter-spacing: .08em; }

/* ——— Die Technik ——— */
.fit { container: fit / inline-size; position: relative; margin-top: 10px; color: var(--navy); font-weight: 800; text-transform: uppercase; transition: opacity 400ms var(--kurve); }
.fit::before, .fit::after { content: ""; position: absolute; top: -4px; bottom: -4px; border-left: 1px dashed var(--mess); opacity: .55; pointer-events: none; }
.fit::before { left: 0; } .fit::after { right: 0; }
.fit.wechsel { opacity: 0; transition-duration: 200ms; }
.satz { display: block; }
.satz-schmal { display: none; }
@container fit (max-width: 480px) {
  .satz-breit { display: none; }
  .satz-schmal { display: block; }
}
.zeile { display: block; white-space: nowrap; }
.txt { display: inline-block; letter-spacing: -.01em; }

/* 1 · Größe je Zeile: Schriftgröße = Breite / Zeilenlänge in em (Zentry, Instrument) */
.v-groesse .txt { font-family: 'Big Shoulders', 'Archivo', sans-serif; font-size: calc(100cqi / var(--k)); line-height: .85; }
/* 2 · Breitenachse: eine Größe für alle Zeilen, wdth füllt die Breite (Instrument Sans 'wdth') */
.v-breite .txt { font-family: 'Archivo', sans-serif; line-height: .85; }
/* 3 · Gegenprobe ohne Fitting: Rezeptgröße, Zeilen flattern */
.v-ohne .txt { font-family: 'Big Shoulders', 'Archivo', sans-serif; font-size: clamp(2.75rem, 1.6rem + 5cqi, 6rem); line-height: .95; letter-spacing: .005em; }
.fit.uebergang .txt { transition: font-size var(--gross), line-height var(--gross); }

.messwerte { display: flex; flex-wrap: wrap; gap: 4px 22px; margin-top: 14px; color: var(--mess); letter-spacing: .06em; text-transform: none; font-variant-numeric: tabular-nums; min-height: 1.3em; }
.messwerte b { font-weight: 500; color: var(--navy); }

/* Unter der Headline */
.unter { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); gap: clamp(20px, 3vw, 48px); margin-top: clamp(22px, 3vw, 40px); align-items: start; }
@container seite (max-width: 760px) { .unter { grid-template-columns: 1fr; } }
.rahmen { container: seite / inline-size; }
.becken { position: relative; border-radius: 3px; overflow: hidden; background: #0b86b8; }
.becken svg { display: block; width: 100%; height: auto; }
.becken::after { content: ""; position: absolute; inset: auto 0 0; height: 42%; background: linear-gradient(rgba(1, 33, 79, 0), rgba(1, 33, 79, .5)); pointer-events: none; }   /* Lesegrund für die Bildunterschrift */
.becken figcaption { z-index: 1; position: absolute; left: 12px; bottom: 12px; right: 12px; display: flex; justify-content: space-between; gap: 10px; color: #fff; font-size: .8125rem; font-weight: 500; text-shadow: 0 1px 8px rgba(0, 33, 79, .5); }
.text p.lead { font-size: 1.0625rem; line-height: 1.6; max-width: 52ch; text-wrap: pretty; }
.knoepfe { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
.zweit { display: inline-flex; align-items: center; height: 46px; padding: 0 18px; border-radius: 3px; border: 1.5px solid var(--navy); color: var(--navy); font-weight: 600; text-decoration: none; transition: background-color var(--ui), color var(--ui); }
.zweit:hover { background: var(--navy); color: #fff; }
.zahlen { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 26px; padding-top: 16px; border-top: 2px solid var(--navy); }
.zahlen b { display: block; font: 800 clamp(2.4rem, 1.6rem + 2.6cqi, 3.6rem)/.9 'Big Shoulders', sans-serif; color: var(--navy); }
.zahlen span { display: block; margin-top: 6px; font-size: .875rem; line-height: 1.4; color: var(--leise); }
.stand { margin-top: 10px; font-size: .8125rem; color: var(--leise); }

/* Werte */
.werte { display: grid; grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); gap: 6px 32px; margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--linie); color: var(--leise); font: 400 11.5px/1.6 'Geist Mono', ui-monospace, monospace; }
.werte div { display: flex; gap: 12px; min-width: 0; }
.werte dt { color: var(--navy); flex: none; min-width: 8.5em; }
.werte dd { overflow-wrap: anywhere; }

.kopf .kurz { display: none; }
@container seite (max-width: 980px) { .nav { display: none; } }
@container seite (max-width: 440px) { .kopf .lang, .logo small, .becken figcaption span + span { display: none; } .kopf .kurz { display: inline; } .kopf .spenden { height: 40px; padding: 0 14px; } .logo b { font-size: 1.375rem; } }
@media (max-width: 600px) {
  body { padding: 12px 10px 28px; }
  .umschalter { width: 100%; }
  .umschalter button { flex: 1; padding: 8px 2px; letter-spacing: .04em; }
  .regler { width: 100%; }
  .regler input { flex: 1; width: auto; }
  .spenden { height: 40px; padding: 0 14px; }
  .zahlen { grid-template-columns: 1fr 1fr; }
  .zahlen div:last-child { grid-column: 1 / -1; }
  .werte { grid-template-columns: 1fr; }
  .werte dt { min-width: 7em; }
  .griff { display: none; }
}

/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }

/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; padding: 18px 22px; }
.vorschau .leiste, .vorschau .werte, .vorschau .nav, .vorschau .text p.lead, .vorschau .stand { display: none; }

/* Reduzierte Bewegung */
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }

@media (hover: none) {
  .griff { width: 14px; right: -17px; }
  .griff:hover { background: var(--navy); scale: 1; }
  .umschalter button { padding-block: 10px; }
}
</style>
</head>
<body>
<div class="leiste mono">
  <div class="umschalter" role="group" aria-label="Variante">
    <button type="button" data-variante="groesse" aria-pressed="true">Größe je Zeile</button>
    <button type="button" data-variante="breite" aria-pressed="false">Breitenachse</button>
    <button type="button" data-variante="ohne" aria-pressed="false">Ohne Fitting</button>
  </div>
  <label class="regler">Breite <input type="range" min="300" max="1400" step="1" value="1400" data-regler aria-label="Breite der Seite in Pixel"><output data-ausgabe>100 %</output></label>
</div>

<div class="buehne">
  <div class="halter" data-halter>
    <div class="rahmen" data-rahmen>
      <header class="kopf">
        <a class="logo" href="#" aria-label="Kopfsprung e.V., Startseite">
          <svg viewBox="0 0 30 30" aria-hidden="true"><circle cx="21.5" cy="6.5" r="3" fill="currentColor"/><path d="M4 14.5c5-6.5 11.5-7.5 17-3.5" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"/><path d="M3 21.5c2.2-1.6 4.4-1.6 6.6 0s4.4 1.6 6.6 0 4.4-1.6 6.6 0 3.3 1.2 4.2.8M3 26.5c2.2-1.6 4.4-1.6 6.6 0s4.4 1.6 6.6 0 4.4-1.6 6.6 0 3.3 1.2 4.2.8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
          <span><b>Kopfsprung</b><small>e.V.</small></span>
        </a>
        <nav class="nav" aria-label="Hauptnavigation">
          <a href="#">Kurse</a>
          <a href="#">Mitmachen</a>
          <a href="#">Transparenz</a>
          <a href="#">Über uns</a>
        </nav>
        <a class="spenden" href="#"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 14s-5.5-3.3-5.5-7.2A3 3 0 0 1 8 4.6a3 3 0 0 1 5.5 2.2C13.5 10.7 8 14 8 14Z" fill="currentColor"/></svg><span class="lang">Jetzt spenden</span><span class="kurz">Spenden</span></a>
      </header>

      <section class="held" aria-labelledby="mission">
        <p class="oben"><span>Schwimmkurse für Grundschulkinder in Bochum</span><span class="mono">Gemeinnützig · seit 2019</span></p>
        <div class="lineal mono" aria-hidden="true"><span data-lineal>100cqi</span></div>
        <h1 class="fit v-groesse" id="mission" data-fit>
          <span class="vh">Schwimmen lernen ist kein Luxus. Jedes Kind in Bochum.</span>
          <span class="satz satz-breit" aria-hidden="true">
            <span class="zeile" style="--k: 8.20"><span class="txt">Schwimmen lernen</span></span>
            <span class="zeile" style="--k: 5.81"><span class="txt">ist kein Luxus.</span></span>
            <span class="zeile" style="--k: 8.77"><span class="txt">Jedes Kind in Bochum</span></span>
          </span>
          <span class="satz satz-schmal" aria-hidden="true">
            <span class="zeile" style="--k: 5.12"><span class="txt">Schwimmen</span></span>
            <span class="zeile" style="--k: 4.21"><span class="txt">lernen ist</span></span>
            <span class="zeile" style="--k: 4.49"><span class="txt">kein Luxus.</span></span>
            <span class="zeile" style="--k: 4.28"><span class="txt">Jedes Kind</span></span>
            <span class="zeile" style="--k: 4.29"><span class="txt">in Bochum</span></span>
          </span>
        </h1>
        <p class="messwerte mono" data-messwerte aria-hidden="true"></p>

        <div class="unter">
          <figure class="becken">
            <svg viewBox="0 0 640 360" role="img" aria-label="Lehrschwimmbecken von oben: türkisfarbenes Wasser, Bahnenleinen und zwei Schwimmnudeln">
              <defs>
                <linearGradient id="wasser" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#1aa6d6"/><stop offset="1" stop-color="#0679ad"/></linearGradient>
                <filter id="licht" x="0" y="0" width="100%" height="100%">
                  <feTurbulence type="fractalNoise" baseFrequency=".018 .03" numOctaves="2" seed="7" result="n"/>
                  <feColorMatrix in="n" type="matrix" values="0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 -2.6 1.55"/>
                </filter>
                <pattern id="leine" width="24" height="10" patternUnits="userSpaceOnUse"><rect x="1" y="1" width="10" height="8" rx="4" fill="#ffffff"/><rect x="13" y="1" width="10" height="8" rx="4" fill="#002e6d"/></pattern>
                <pattern id="leine-ende" width="24" height="10" patternUnits="userSpaceOnUse"><rect x="1" y="1" width="10" height="8" rx="4" fill="#e8483b"/><rect x="13" y="1" width="10" height="8" rx="4" fill="#ffffff"/></pattern>
              </defs>
              <rect width="640" height="360" fill="url(#wasser)"/>
              <g fill="#0a4f86" opacity=".55">
                <path d="M70 52h500v8H70zM60 46h10v20H60zM570 46h10v20h-10z"/>
                <path d="M70 172h500v8H70zM60 166h10v20H60zM570 166h10v20h-10z"/>
                <path d="M70 292h500v8H70zM60 286h10v20H60zM570 286h10v20h-10z"/>
              </g>
              <rect width="640" height="360" filter="url(#licht)" opacity=".38"/>
              <rect y="111" width="640" height="10" fill="url(#leine)"/>
              <rect y="111" width="96" height="10" fill="url(#leine-ende)"/>
              <rect x="544" y="111" width="96" height="10" fill="url(#leine-ende)"/>
              <rect y="231" width="640" height="10" fill="url(#leine)"/>
              <rect y="231" width="96" height="10" fill="url(#leine-ende)"/>
              <rect x="544" y="231" width="96" height="10" fill="url(#leine-ende)"/>
              <rect x="0" y="0" width="640" height="14" fill="#e9eef0"/>
              <rect x="0" y="346" width="640" height="14" fill="#e9eef0"/>
              <g transform="rotate(-14 250 150)"><rect x="170" y="140" width="170" height="18" rx="9" fill="#ff7a59"/><rect x="170" y="140" width="170" height="6" rx="3" fill="#fff" opacity=".25"/></g>
              <g transform="rotate(9 430 280)"><rect x="360" y="270" width="150" height="18" rx="9" fill="#2fbf9b"/><rect x="360" y="270" width="150" height="6" rx="3" fill="#fff" opacity=".25"/></g>
              <g fill="#fff" opacity=".9"><circle cx="120" cy="80" r="2"/><circle cx="128" cy="86" r="1.4"/><circle cx="470" cy="200" r="2.2"/><circle cx="478" cy="194" r="1.3"/></g>
            </svg>
            <figcaption><span>Kurs Dienstag, 15:30 Uhr</span><span>zehn Kinder · zwei Trainerinnen · eine Bahn</span></figcaption>
          </figure>
          <div class="text">
            <p class="lead">Viele Kinder verlassen die Grundschule, ohne sicher schwimmen zu können. Wir bringen es ihnen bei: in kleinen Gruppen, mit ausgebildeten Trainerinnen und Trainern, für die Familien kostenlos.</p>
            <div class="knoepfe">
              <a class="spenden" href="#">60 € spenden</a>
              <a class="zweit" href="#">Als Trainer:in mitmachen</a>
            </div>
            <div class="zahlen">
              <div><b>1.284</b><span>Kinder mit Seepferdchen seit 2019</span></div>
              <div><b>46</b><span>Kurse im Jahr 2025</span></div>
              <div><b>60 €</b><span>ein ganzer Kurs für ein Kind</span></div>
            </div>
            <p class="stand">Stand: Jahresbericht 2025</p>
          </div>
        </div>
      </section>
    </div>
    <div class="griff" data-griff role="presentation" title="Ziehen, um die Breite zu ändern"></div>
  </div>
</div>

<dl class="werte" data-werte></dl>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);
  const fit = $('[data-fit]');
  const halter = $('[data-halter]');
  const rahmen = $('[data-rahmen]');
  const griff = $('[data-griff]');
  const regler = $('[data-regler]');
  const ausgabe = $('[data-ausgabe]');
  const lineal = $('[data-lineal]');
  const messwerte = $('[data-messwerte]');
  const werteListe = $('[data-werte]');
  const knoepfe = [...document.querySelectorAll('[data-variante]')];
  const zeilen = [...fit.querySelectorAll('.zeile')];
  const STUFEN = [62, 72.5, 83, 93.5, 104, 114.5, 125];   // Stützstellen der wdth-Achse

  let variante = 'groesse', breite = null, gemessen = false;

  const komma = (n, s = 2) => n.toFixed(s).replace('.', ',');

  /* Einmal messen: Breite jeder Zeile in em. Big Shoulders für --k, Archivo an sieben wdth-Stufen */
  const messer = document.createElement('span');
  messer.setAttribute('aria-hidden', 'true');
  messer.style.cssText = 'position:absolute;left:-9999px;top:0;visibility:hidden;white-space:nowrap;text-transform:uppercase;font-size:100px;letter-spacing:-.01em;font-weight:800';
  document.body.appendChild(messer);
  function miss(text, familie, wdth) {
    messer.style.fontFamily = familie;
    messer.style.fontStretch = (wdth || 100) + '%';
    messer.textContent = text;
    return messer.getBoundingClientRect().width / 100;
  }
  function messen() {
    zeilen.forEach(z => {
      const t = z.textContent.trim();
      z._k = miss(t, "'Big Shoulders'");
      z._w = STUFEN.map(s => miss(t, "'Archivo'", s));
      z._n = t.length;
      z.style.setProperty('--k', z._k.toFixed(3));
    });
    gemessen = true;
  }

  /* Variante 2: gemeinsame Größe aus der längsten Zeile bei wdth 62, dann wdth je Zeile per Interpolation */
  function sichtbareZeilen() { return zeilen.filter(z => z.offsetParent !== null || z.getClientRects().length); }
  function passeBreite() {
    const C = fit.clientWidth;
    const liste = sichtbareZeilen();
    if (variante !== 'breite' || !gemessen) {
      zeilen.forEach(z => { const t = z.firstElementChild; t.style.fontSize = t.style.fontStretch = t.style.letterSpacing = ''; z._wdth = null; z._extra = 0; });
      return;
    }
    const S = C / Math.max(...liste.map(z => z._w[0]));
    liste.forEach(z => {
      const ziel = C / S, w = z._w, t = z.firstElementChild;
      let wdth = 125, extra = 0;
      if (ziel <= w[0]) wdth = 62;
      else if (ziel >= w[w.length - 1]) extra = (ziel - w[w.length - 1]) / z._n;
      else for (let i = 1; i < w.length; i++) if (ziel <= w[i]) { wdth = STUFEN[i - 1] + (ziel - w[i - 1]) / (w[i] - w[i - 1]) * (STUFEN[i] - STUFEN[i - 1]); break; }
      t.style.fontSize = S.toFixed(2) + 'px';
      t.style.fontStretch = wdth.toFixed(1) + '%';
      t.style.letterSpacing = (-0.01 + extra).toFixed(4) + 'em';
      z._wdth = wdth; z._extra = extra; z._S = S;
    });
  }

  function zeigeMesswerte() {
    const C = fit.clientWidth;
    lineal.textContent = '100cqi = ' + Math.round(C) + ' px';
    const liste = sichtbareZeilen();
    messwerte.innerHTML = liste.map((z, i) => {
      const t = z.firstElementChild, px = parseFloat(getComputedStyle(t).fontSize);
      const anteil = Math.round(t.getBoundingClientRect().width / C * 100);
      if (variante === 'groesse') return `<span><b>Z${i + 1}</b> 100cqi / ${komma(z._k || +z.style.getPropertyValue('--k'))} = ${Math.round(px)} px</span>`;
      if (variante === 'breite') return `<span><b>Z${i + 1}</b> ${Math.round(px)} px · wdth ${Math.round(z._wdth || 100)}${z._extra ? ' + Laufweite' : ''}</span>`;
      return `<span><b>Z${i + 1}</b> ${Math.round(px)} px · füllt ${anteil} %</span>`;
    }).join('');
  }

  const WERTE = {
    groesse: [
      ['Schrift', 'Big Shoulders 800, versal, letter-spacing -.01em, line-height .85'],
      ['Größe je Zeile', 'font-size: calc(100cqi / var(--k)), --k = Zeilenbreite in em (einmal gemessen)'],
      ['Container', 'container: fit / inline-size auf der h1; im echten Einsatz 100vw statt 100cqi'],
      ['Schmal', '@container fit (max-width: 480px): 5 statt 3 Zeilen'],
      ['Wechsel', 'font-size 800ms cubic-bezier(0.645, 0.045, 0.355, 1)']
    ],
    breite: [
      ['Schrift', 'Archivo 800, versal, wdth 62–125, line-height .85'],
      ['Größe', 'eine Größe für alle Zeilen: Breite / längste Zeile bei wdth 62'],
      ['Breite je Zeile', 'font-stretch aus 7 gemessenen Stufen interpoliert; reicht 125 nicht, füllt Laufweite den Rest'],
      ['Neuberechnung', 'ResizeObserver auf der h1'],
      ['Wechsel', 'Einblenden 400ms cubic-bezier(0.645, 0.045, 0.355, 1)']
    ],
    ohne: [
      ['Schrift', 'Big Shoulders 800, versal, letter-spacing .005em, line-height .95'],
      ['Größe', 'clamp(2.75rem, 1.6rem + 5cqi, 6rem) für alle Zeilen (Rezeptwert)'],
      ['Folge', 'Zeilen flattern rechts, die Fläche bleibt unbesetzt']
    ]
  };
  function zeigeWerte() { werteListe.innerHTML = WERTE[variante].map(([a, b]) => `<div><dt>${a}</dt><dd>${b}</dd></div>`).join(''); }

  function aktualisieren() { passeBreite(); requestAnimationFrame(zeigeMesswerte); }

  let wechselUhr = null, uebergangUhr = null;
  function waehle(v, sofort) {
    // Schriftfamilie wechselt: kurz ausblenden statt springen (nur bei Breitenachse nötig)
    const familie = (v === 'breite') !== (variante === 'breite');
    const setzen = () => {
      // Größen gleiten nur innerhalb derselben Familie; beim Familienwechsel würde die alte Größe die Zeile sprengen
      if (!reduziert() && !sofort && !familie) { fit.classList.add('uebergang'); clearTimeout(uebergangUhr); uebergangUhr = setTimeout(() => fit.classList.remove('uebergang'), 850); }
      variante = v;
      fit.classList.remove('v-groesse', 'v-breite', 'v-ohne');
      fit.classList.add('v-' + v);
      knoepfe.forEach(k => k.setAttribute('aria-pressed', k.dataset.variante === v));
      zeigeWerte();
      aktualisieren();
    };
    clearTimeout(wechselUhr);
    if (sofort || reduziert() || !familie) { fit.classList.remove('wechsel'); setzen(); return; }
    fit.classList.add('wechsel');
    wechselUhr = setTimeout(() => { setzen(); fit.classList.remove('wechsel'); }, 200);
  }
  knoepfe.forEach(k => k.addEventListener('click', () => { stopp(); waehle(k.dataset.variante); }));

  /* Breite der Bühne */
  const platz = () => halter.clientWidth;
  function setzeBreite(w) {
    const p = platz();
    breite = Math.round(Math.max(300, Math.min(p, w)));
    rahmen.style.width = breite >= p ? '' : breite + 'px';
    griff.style.left = (breite + 5) + 'px'; griff.style.right = 'auto';
    regler.max = p; regler.value = breite;
    ausgabe.textContent = Math.round(breite / p * 100) + ' %';
  }
  regler.addEventListener('input', () => { stopp(); setzeBreite(+regler.value); });
  let zug = null;
  griff.addEventListener('pointerdown', e => { stopp(); zug = { x: e.clientX, w: breite }; griff.setPointerCapture(e.pointerId); griff.classList.add('zieht'); });
  griff.addEventListener('pointermove', e => { if (zug) setzeBreite(zug.w + e.clientX - zug.x); });
  const los = () => { zug = null; griff.classList.remove('zieht'); };
  griff.addEventListener('pointerup', los);
  griff.addEventListener('pointercancel', los);

  new ResizeObserver(aktualisieren).observe(fit);
  /* Eigene Textabstände (WCAG 1.4.12): läuft eine Zeile über die Breite, Zeilen neu messen */
  const ueberlauf = new ResizeObserver(() => {
    if (!gemessen || variante !== 'groesse') return;
    const C = fit.clientWidth;
    if (sichtbareZeilen().some(z => z.firstElementChild.getBoundingClientRect().width > C + 2)) { messen(); aktualisieren(); }
  });
  zeilen.forEach(z => ueberlauf.observe(z.firstElementChild));
  new ResizeObserver(() => { if (breite === null || breite >= regler.max - 1) setzeBreite(platz()); else setzeBreite(breite); }).observe(halter);

  /* Vorführung: Seite schmaler und breiter, die Zeilen bleiben bündig; danach nächste Variante */
  const bez = (x1, y1, x2, y2) => t => {   // cubic-bezier lösen (wie CSS)
    let a = t; for (let i = 0; i < 6; i++) { const x = 3 * (1 - a) * (1 - a) * a * x1 + 3 * (1 - a) * a * a * x2 + a * a * a - t; const d = 3 * (1 - a) * (1 - a) * x1 + 6 * (1 - a) * a * (x2 - x1) + 3 * a * a * (1 - x2); if (Math.abs(d) < 1e-6) break; a = Math.min(1, Math.max(0, a - x / d)); }
    return 3 * (1 - a) * (1 - a) * a * y1 + 3 * (1 - a) * a * a * y2 + a * a * a;
  };
  const inout = bez(0.645, 0.045, 0.355, 1);
  let uhren = [], raf = null;
  const spaeter = (fn, ms) => uhren.push(setTimeout(fn, ms));
  function fahre(von, bis, dauer) {
    cancelAnimationFrame(raf);
    if (reduziert()) { setzeBreite(bis); return; }
    const t0 = performance.now();
    const schritt = t => { const p = Math.min(1, (t - t0) / dauer); setzeBreite(von + (bis - von) * inout(p)); if (p < 1) raf = requestAnimationFrame(schritt); };
    raf = requestAnimationFrame(schritt);
  }
  const FOLGE = ['groesse', 'breite', 'ohne'];
  function runde(i) {
    const v = FOLGE[i % FOLGE.length], voll = platz(), schmal = Math.max(320, Math.round(voll * .62));
    waehle(v);
    setzeBreite(voll);
    spaeter(() => fahre(voll, schmal, 1000), 700);
    spaeter(() => fahre(schmal, voll, 1000), 2600);
    spaeter(() => runde(i + 1), 4300);
  }
  let rundeNr = 0;
  function stopp() { uhren.forEach(clearTimeout); uhren = []; cancelAnimationFrame(raf); html.classList.remove('vorfuehrung'); }
  function vorfuehren() { stopp(); html.classList.add('vorfuehrung'); rundeNr = 0; runde(0); }
  function pause() { uhren.forEach(clearTimeout); uhren = []; cancelAnimationFrame(raf); }
  function weiter() { if (html.classList.contains('vorfuehrung')) runde(FOLGE.indexOf(variante) + 1); }
  function neustart() { stopp(); setzeBreite(platz()); waehle('groesse', true); }

  setzeBreite(platz());
  zeigeWerte();
  aktualisieren();
  if (document.fonts) Promise.all([document.fonts.load("800 100px 'Big Shoulders'"), document.fonts.load("800 100px 'Archivo'")]).then(() => document.fonts.ready).then(() => { messen(); aktualisieren(); });

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