Foto-Hero mit Wortmarke
Der Name des Hauses läuft randlos über die volle Breite und bildet mit dem Foto einen Block: als Band darüber, riesig an der Bildkante angeschnitten oder über einem Daumenkino, das die Maus blättert.
Pension, Ferienhaus, Café, Salon, Weingut mit kurzem Namen: Satz oben links, Buchen oben rechts, Name randlos, darunter das Foto als Band (Kereliott).
Name und Foto bilden einen Block. Der Name des Hauses läuft als Wortmarke über die volle Breite, randlos eingepasst wie beim Schrift-Hero. Das Foto hängt direkt darunter als breites Band (gestapelt) oder die Wortmarke steht riesig und weiß an der Unterkante eines dunklen Vollbilds und wird dort angeschnitten (im Bild). In der dritten Form blättert die Zeigerbewegung durch vier bis sechs gestapelte Vollbilder hinter der Wortmarke (Daumenkino). Dazu kommen nur ein Satz, ein Weg (Buchen, Tisch reservieren) und bei der lauten Form eine Leistungszeile aus drei Wörtern mit langen Haarlinien.
Kleine Häuser leben von ihrem Namen. Steht er über die ganze Breite, wird er zum Logo, zur Überschrift und zum Rahmen des Fotos zugleich. Das Foto zeigt in einer Sekunde, wo man ist; die Wortmarke sagt, wie das Haus heißt, ohne Kopfzeile und Slogan. Weil Name und Bild ein Block sind, wirkt der Hero gebaut und nicht aus Vorlagen zusammengesetzt.
Wann einsetzen
Pension, Ferienhaus, Café, Salon, Weingut und Gästehaus mit kurzem, einprägsamem Namen (gestapelt). Bar, Club, Musikbühne, Fitness oder Kreativstudio mit starkem, dunklem Foto (im Bild). Das Daumenkino nur für Bar, Club, Musik oder Kreativstudio und nur am Desktop.
Wann nicht
Bei langen Namen oder Namen aus mehreren Wörtern: auf dem Telefon wird die Wortmarke zu klein. Ohne brauchbares Foto (dann schrift-hero-viewportbreit). Im Bild nur bei dunklem, ruhigem Foto, sonst eine deckende Fläche hinter die Schrift. Das Daumenkino nie als einzige Möglichkeit, Bilder zu sehen, und nie auf Seiten, auf denen Gäste schnell Zeiten oder Preise suchen.
Varianten
- gestapelt: Satz oben links in Textgröße, Buchen oben rechts, darunter die Wortmarke über die volle Breite, direkt darunter das Foto als Band (Kereliott)
- im-bild: dunkles Vollbild, Wortmarke weiß an der Unterkante angeschnitten, Leistungszeile aus drei Wörtern mit langen Haarlinien in der Bildmitte, Tisch reservieren oben rechts (Maloot)
- daumenkino: wie im-bild, dahinter 4 bis 6 gestapelte Vollbilder; je 150 bis 250 px Zeigerweg wird das nächste aktiv, nur bei (hover: hover) and (pointer: fine) (Maloot)
Umsetzung
Wortmarke als echtes <h1> (aria-label mit dem Namen, Buchstaben aria-hidden, wenn sie einzeln einlaufen). Einpassen wie bei schrift-hero-viewportbreit: Breite bei 100px messen, Seitenlinien von erstem und letztem Zeichen per Canvas measureText (actualBoundingBox) abziehen, font-size = Platz / (Breite in em − Seitenlinien); bei document.fonts.ready und per ResizeObserver neu messen, CSS-Rückfall in vw. Gestapelt: Kopfzeile mit Satz und Weg, darunter Wortmarke mit line-height 0.8 und negativem margin-bottom (Unterlänge abziehen), direkt darunter <picture> mit object-fit: cover, Band füllt den Rest des Viewports (min. 38svh), mobil Hochformat. Im Bild: Hero 100svh mit overflow: clip, Wortmarke absolut unten mit translateY um etwa 18 % ihrer Höhe nach unten (angeschnitten), Leistungszeile als Liste in einem Flex-Container mit Haarlinien (flex: 1, 1 px) zwischen den Wörtern. Daumenkino: Bilder gestapelt (position: absolute, inset: 0), nur eines mit .aktiv; pointermove summiert Math.hypot(dx, dy) und schaltet nach einer Schwelle von etwa 200 px weiter. Nur unter matchMedia('(hover: hover) and (pointer: fine)') und ohne reduzierte Bewegung, sonst steht das erste Bild. Bilder ab dem zweiten mit loading="lazy" und decoding="async", das erste mit fetchpriority="high".
Robust umsetzen
Telefon. Gestapelt: Satz und Weg oben, Wortmarke randlos (bei 390 px etwa 80 bis 95 px Schriftgröße für sieben Buchstaben), Foto als Hochformat darunter. Im Bild: Wortmarke bleibt an der Unterkante, Leistungszeile bricht in drei Zeilen um, Knopf als volle Breite unten. Daumenkino: auf Touch aus, es steht das erste Bild.
Ohne Bewegung. Kein Einlaufen der Buchstaben, Foto steht. Daumenkino aus, es steht das erste Bild.
Barrierefrei. Ein echtes <h1> mit dem Namen, gesplittete Buchstaben aria-hidden. Weiße Schrift nur auf dunklem Foto (4,5:1 gegen die hellste Stelle der Bildzone prüfen), sonst Verlauf oder deckende Fläche unten. Alt-Text beschreibt das Foto, nicht den Namen. Das Daumenkino ist Schmuck: Bilder ab dem zweiten mit alt="" und die Inhalte dazu stehen weiter unten auf der Seite; es läuft nicht von selbst und braucht deshalb keinen Pause-Knopf. Bei 200 % Zoom und 320 px darf die Wortmarke nicht abgeschnitten werden (Fit per ResizeObserver).
Code
<!DOCTYPE html>
<!--
Foto-Hero mit Wortmarke
Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/foto-hero-wortmarke/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Foto-Hero mit Wortmarke · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Mona Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/mona-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Young Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/young-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Commissioner'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/commissioner:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; 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; }
/* ---------- Grundlagen ---------- */
* { box-sizing: border-box; margin: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100svh; overflow-x: clip; }
img { display: block; max-width: 100%; }
a { color: inherit; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.welt[hidden] { display: none; }
/* ---------- Welt 1: Pension Kiebitz (Rezept boutique-hotel-gastgeber, Ferienwohnung/Pension) ---------- */
.pension {
--grund: #F3EEE7; --text: #3E3A37; --leise: #6A6C67; --linie: #D5D1C8; --haus: #6E5A47; --creme: #FEF2E3;
--ease: cubic-bezier(0.37, 0, 0.63, 1); /* token-luxus-weich: Wortmarke und Foto */
--ui-ease: cubic-bezier(0.16, 1, 0.3, 1); /* token-toggle-asymmetrisch: Knöpfe */
--rand: clamp(18px, 3.6vw, 56px);
background: var(--grund); color: var(--text);
font: 400 1.0625rem/1.6 'Mona Sans', system-ui, sans-serif; font-stretch: 100%;
min-height: 100svh; display: flex; flex-direction: column;
}
.pension :focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
.p-kopf { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px 32px; align-items: start; padding: clamp(16px, 2.2vw, 28px) var(--rand) clamp(14px, 2vw, 26px); }
.p-satz { max-width: 36ch; font-size: 1.0625rem; line-height: 1.5; text-wrap: pretty; }
.p-satz em { font-family: 'Newsreader', Georgia, serif; font-style: italic; font-size: 1.12em; font-variation-settings: 'opsz' 24; }
.p-wege { display: flex; align-items: center; gap: 22px; flex-wrap: wrap; justify-content: flex-end; }
.label { font: 600 .75rem/1 'Mona Sans', system-ui, sans-serif; font-stretch: 118%; letter-spacing: .06em; text-transform: uppercase; }
.p-sprache { display: flex; gap: 10px; color: var(--leise); }
.p-sprache a { text-decoration: none; padding: 6px 0; }
.p-sprache a[aria-current] { color: var(--text); text-decoration: underline; text-underline-offset: 4px; }
.p-tel { text-decoration: none; color: var(--leise); }
.p-tel:hover { color: var(--text); }
.knopf { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 22px; border-radius: 999px; background: var(--haus); color: #fff; text-decoration: none; border: 0; cursor: pointer;
transition: background-color 220ms var(--ui-ease); }
.knopf:hover { background: #5a4939; }
.knopf svg { width: 14px; height: 14px; transition: transform 220ms var(--ui-ease); }
.knopf:hover svg { transform: translateX(3px); }
/* Wortmarke: randlos eingepasst, Tinte liegt bündig auf dem Foto */
.wm { --fs: 26vw; position: relative; padding: 0 var(--rand); font-weight: 400; line-height: 1; white-space: nowrap; }
.wm__zug { display: block; width: max-content; font-size: var(--fs); margin-left: var(--optik, 0); margin-top: var(--oben, 0); margin-bottom: var(--unten, 0); }
.wm .m { display: inline-block; clip-path: inset(-.4em -.2em 0 -.2em); vertical-align: top; }
.wm .b { display: inline-block; }
.pension .wm { font-family: 'Newsreader', Georgia, serif; font-variation-settings: 'opsz' 72; letter-spacing: 0; color: var(--text); }
/* Foto als Band direkt unter der Wortmarke */
.p-band { position: relative; flex: 1 0 auto; min-height: max(38svh, 260px); margin: 0 var(--rand); overflow: hidden; background: #5b5560; }
.p-band picture, .p-band img { position: absolute; inset: 0; width: 100%; height: 100%; }
.p-band img { object-fit: cover; object-position: 50% 55%; }
.p-unten { display: flex; justify-content: space-between; gap: 8px 24px; flex-wrap: wrap; padding: 14px var(--rand) 18px; color: var(--leise); }
.p-unten .label { font-weight: 500; }
/* Auftritt (nur mit JS, nicht bei reduzierter Bewegung) */
.js .pension.auftritt .wm .b { transform: translateY(105%); }
.js .pension.auftritt .p-band { clip-path: inset(0 0 100% 0); }
.js .pension.auftritt .p-band img { transform: scale(1.06); }
.js .pension.auftritt .p-kopf > *, .js .pension.auftritt .p-unten { opacity: 0; transform: translateY(16px); }
.pension .wm .b { transition: transform 1200ms var(--ease) calc(var(--i) * 120ms); }
.pension .p-band { transition: clip-path 1400ms var(--ease) 350ms; }
.pension .p-band img { transition: transform 1400ms var(--ease) 350ms; }
.pension .p-kopf > *, .pension .p-unten { transition: opacity 1200ms var(--ease) 500ms, transform 1200ms var(--ease) 500ms; }
/* ---------- Welt 2: Hifi-Bar Rille (Rezept gastro-warm-appetitlich, Bar) ---------- */
.bar {
--grund: #0e0c0a; --text: #F3EDE2; --leise: rgba(243, 237, 226, .78); --linie: rgba(243, 237, 226, .55); --papier: #F0ECE0; --dunkel: #293E3A; --senf: #EAC16E;
--ease: cubic-bezier(0.16, 1, 0.3, 1); /* token-editorial-langsam */
--rand: clamp(16px, 4vw, 48px);
background: var(--grund); color: var(--text);
font: 400 1.0625rem/1.55 'Commissioner', system-ui, sans-serif;
}
.bar :focus-visible { outline: 2px solid var(--senf); outline-offset: 3px; }
.b-hero { position: relative; height: 100svh; min-height: 520px; overflow: clip; isolation: isolate; }
.b-bilder { position: absolute; inset: 0; z-index: -2; }
.b-bilder img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 45%; opacity: 0; }
.b-bilder img.aktiv { opacity: 1; }
.b-bilder img:first-child { opacity: 1; }
.b-bilder img.weg { opacity: 0; }
.daumen .b-bilder img { transition: opacity .3s ease; } /* gemessen bei Maloot */
/* Lesbarkeit: oben leicht, unten kräftig abdunkeln (nur das Foto, nie den Text) */
.b-hero::before { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
background: linear-gradient(180deg, rgba(10, 8, 6, .62) 0%, rgba(10, 8, 6, 0) 22%, rgba(10, 8, 6, .05) 45%, rgba(10, 8, 6, .55) 62%, rgba(10, 8, 6, .9) 100%); }
.b-kopf { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: clamp(14px, 2vw, 24px) var(--rand); }
.b-nav { display: flex; gap: clamp(14px, 2.2vw, 30px); list-style: none; padding: 0; }
.b-nav a, .b-marke { font: 600 .75rem/1 'Commissioner', system-ui, sans-serif; font-variation-settings: 'FLAR' 80; letter-spacing: .15em; text-transform: uppercase; text-decoration: none; padding: 12px 0; display: inline-block; }
.b-marke { display: none; }
.b-reserv { margin-left: auto; }
.b-nav a:hover { text-decoration: underline; text-underline-offset: 5px; }
.b-reserv { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 20px; border-radius: 999px; background: var(--papier); color: #232625; text-decoration: none;
font: 600 .8125rem/1 'Commissioner', system-ui, sans-serif; letter-spacing: .04em; transition: background-color 200ms var(--ease); }
.b-reserv:hover { background: var(--senf); }
.b-reserv i { width: 8px; height: 8px; border-radius: 50%; background: #1f7a4d; box-shadow: 0 0 0 3px rgba(31, 122, 77, .2); }
.b-mitte { position: absolute; left: var(--rand); right: var(--rand); top: 46%; transform: translateY(-50%); text-align: center; }
.b-mitte::before { content: ""; position: absolute; left: 50%; top: 50%; width: min(1100px, 130%); height: 260%; transform: translate(-50%, -50%); z-index: -1; pointer-events: none;
background: radial-gradient(closest-side, rgba(10, 8, 6, .62), rgba(10, 8, 6, .38) 55%, rgba(10, 8, 6, 0)); }
.b-zeile { display: flex; align-items: center; gap: clamp(12px, 1.6vw, 22px); list-style: none; padding: 0; margin: 0 auto; max-width: 980px;
font: 600 .8125rem/1 'Commissioner', system-ui, sans-serif; font-variation-settings: 'FLAR' 100; letter-spacing: .2em; text-transform: uppercase; text-shadow: 0 1px 12px rgba(0, 0, 0, .5); }
.b-zeile li { flex: none; }
.b-zeile .haar { flex: 1 1 0; height: 1px; background: var(--linie); transform-origin: 0 50%; }
.b-info { margin-top: 18px; color: var(--leise); font-size: .9375rem; text-shadow: 0 1px 10px rgba(0, 0, 0, .6); }
.b-info b { color: var(--text); font-weight: 600; }
.bar .wm { position: absolute; left: 0; right: 0; bottom: 0; font-family: 'Young Serif', Georgia, serif; letter-spacing: -.01em; color: var(--text); }
.bar .wm__zug { transform: translateY(var(--anschnitt, 4vw)); }
/* Daumenkino-Zusätze */
.b-bild-info { position: absolute; left: var(--rand); right: var(--rand); bottom: calc(var(--wm-h, 22vw) * .82 + 18px); display: none; justify-content: space-between; align-items: end; gap: 16px; font: 500 .75rem/1.3 'Commissioner', system-ui, sans-serif; letter-spacing: .12em; text-transform: uppercase; text-shadow: 0 1px 10px rgba(0, 0, 0, .7); }
.daumen .b-bild-info { display: flex; }
.b-zaehler { font-variant-numeric: tabular-nums; }
.b-hinweis { display: inline-flex; align-items: center; gap: 8px; color: var(--leise); }
.b-hinweis svg { width: 14px; height: 14px; }
.zeiger { position: absolute; z-index: 5; width: 18px; height: 18px; margin: -9px 0 0 -9px; border: 1.5px solid var(--papier); border-radius: 50%; pointer-events: none; opacity: 0; transition: opacity 200ms var(--ease); }
.zeiger.an { opacity: 1; }
/* Auftritt Bar */
.js .bar.auftritt .wm .b { transform: translateY(70%); }
.js .bar.auftritt .b-bilder { filter: brightness(.15); }
.js .bar.auftritt .haar { transform: scaleX(0); }
.js .bar.auftritt .b-zeile li, .js .bar.auftritt .b-info, .js .bar.auftritt .b-kopf { opacity: 0; }
.bar .wm .b { transition: transform 1200ms var(--ease) calc(var(--i) * 100ms); }
.bar .b-bilder { transition: filter 1200ms var(--ease); }
.bar .haar { transition: transform 1000ms var(--ease) calc(400ms + var(--j, 0) * 100ms); }
.bar .b-zeile li, .bar .b-info, .bar .b-kopf { transition: opacity 1000ms var(--ease) 300ms; }
/* ---------- Pult (Demo-Bedienung) ---------- */
.pult { position: fixed; left: 12px; bottom: 12px; z-index: 50; display: grid; gap: 8px; max-width: min(440px, calc(100vw - 24px)); padding: 10px 12px; border-radius: 12px;
background: rgba(18, 18, 20, .86); color: #f3f2ee; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); border: 1px solid rgba(243, 242, 238, .16);
font: 500 10.5px/1.45 'Geist Mono', ui-monospace, monospace; letter-spacing: .06em; }
.pult__reihe { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.pult__titel { color: rgba(243, 242, 238, .7); text-transform: uppercase; letter-spacing: .14em; margin-right: 4px; }
.pult button { font: inherit; letter-spacing: .1em; text-transform: uppercase; color: inherit; background: transparent; border: 1px solid rgba(243, 242, 238, .28); border-radius: 999px; padding: 6px 10px; cursor: pointer; }
.pult button[aria-pressed="true"] { background: #f3f2ee; color: #121214; border-color: #f3f2ee; }
.pult button:focus-visible { outline: 2px solid #34a7b9; outline-offset: 2px; }
.pult__werte { display: flex; flex-wrap: wrap; gap: 2px 12px; color: rgba(243, 242, 238, .74); }
.pult__werte b { color: #f3f2ee; font-weight: 500; }
.pult__meter { height: 2px; background: rgba(243, 242, 238, .2); border-radius: 2px; overflow: hidden; }
.pult__meter i { display: block; height: 100%; width: 0; background: #f3f2ee; }
/* ---------- Telefon ---------- */
@media (max-width: 720px) {
.p-kopf { grid-template-columns: minmax(0, 1fr); }
.p-wege { justify-content: space-between; }
.p-tel { display: none; }
.p-band { min-height: 0; flex: none; aspect-ratio: 4 / 5; margin: 0 var(--rand); }
.b-nav { display: none; }
.b-marke { display: inline-block; }
.b-adr { display: block; }
.b-trenn { display: none; }
.b-zeile { flex-direction: column; gap: 10px; }
.b-zeile .haar { flex: none; width: 1px; height: 22px; transform-origin: 50% 0; }
.js .bar.auftritt .haar { transform: scaleY(0); }
.b-mitte { top: 50%; }
}
@media (max-width: 360px) {
.b-reserv { padding: 0 14px; }
.p-wege { gap: 12px; }
}
/* Touch: kein Daumenkino, kein Zeiger-Hinweis */
@media (hover: none), (pointer: coarse) {
.b-hinweis, .daumen .b-bild-info { display: none; }
}
.reduziert .b-hinweis, .reduziert .daumen .b-bild-info { display: none; }
/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .p-unten { display: none; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; transition-delay: 0s !important; }
@media (prefers-reduced-motion: reduce) { .zeiger { display: none; } }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea, .knopf, .b-reserv { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<!-- Variante gestapelt -->
<main class="welt pension" data-welt="gestapelt">
<header class="p-kopf">
<p class="p-satz">Sieben Zimmer und zwei Ferienwohnungen am Ostufer des <em>Krakower Sees</em>. Frühstück bis elf, der Steg gehört den Gästen.</p>
<div class="p-wege">
<nav class="p-sprache label" aria-label="Sprache"><a href="#" aria-current="true" lang="de">DE</a><a href="#" lang="en">EN</a></nav>
<a class="p-tel label" href="tel:+49384572190">038457 21 90</a>
<a class="knopf label" href="#buchen">Zimmer buchen <svg viewBox="0 0 14 14" aria-hidden="true"><path d="M1 7h11M8 3l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a>
</div>
</header>
<h1 class="wm" aria-label="Pension Kiebitz am Krakower See"><span class="wm__zug" aria-hidden="true" data-wort="Kiebitz">Kiebitz</span></h1>
<div class="p-band">
<picture>
<source media="(max-width: 720px)" srcset="bilder/hotel-sauna-hoch.webp">
<img src="bilder/hotel-sauna.webp" width="1200" height="800" fetchpriority="high" alt="Holzsauna auf dem Steg in der Dämmerung mit zwei erleuchteten Fenstern, dahinter das Backsteinhaus der Pension, Schilf und Spiegelung im See">
</picture>
</div>
<footer class="p-unten">
<span class="label">Am Ostufer 4 · 18292 Krakow am See</span>
<span class="label">Anreise ab 15 Uhr · Abreise bis 11 Uhr</span>
</footer>
</main>
<!-- Varianten im-bild und daumenkino -->
<main class="welt bar" data-welt="bar" hidden>
<section class="b-hero" aria-label="Rille, Hifi-Bar in Leipzig-Lindenau">
<div class="b-bilder" data-bilder>
<img src="bilder/foto-hero-wortmarke-tresen.webp" width="1600" height="1000" fetchpriority="high" alt="Dunkler Bartresen aus Holzlatten, dahinter drei Regalböden mit hinterleuchteten Flaschen und warm glimmende Glühlampen" data-titel="Der Tresen">
<img src="bilder/foto-hero-wortmarke-plattenspieler.webp" width="1600" height="1000" loading="lazy" decoding="async" alt="" data-titel="Plattenspieler, Seite A">
<img src="bilder/foto-hero-wortmarke-glaeser.webp" width="1600" height="1000" loading="lazy" decoding="async" alt="" data-titel="Zwei Coupes, ein Old Fashioned">
<img src="bilder/foto-hero-wortmarke-lautsprecher.webp" width="1600" height="1000" loading="lazy" decoding="async" alt="" data-titel="Röhren, Boxen, Plattenwand">
</div>
<header class="b-kopf">
<span class="b-marke">Rille · Leipzig</span>
<ul class="b-nav">
<li><a href="#karte">Karte</a></li>
<li><a href="#platten">Platten der Woche</a></li>
<li><a href="#abende">Abende</a></li>
<li><a href="#anfahrt">Anfahrt</a></li>
</ul>
<a class="b-reserv" href="#reservieren"><i aria-hidden="true"></i>Tisch reservieren</a>
</header>
<div class="b-mitte">
<ul class="b-zeile" aria-label="Was es hier gibt">
<li>Hifi-Bar</li><li class="haar" aria-hidden="true" style="--j:0"></li><li>Cocktails</li><li class="haar" aria-hidden="true" style="--j:1"></li><li>Vinyl</li>
</ul>
<p class="b-info"><b>Mittwoch bis Samstag ab 19 Uhr</b><span class="b-adr"><span class="b-trenn"> · </span>Karl-Heine-Straße 54, Leipzig</span></p>
</div>
<div class="b-bild-info" aria-hidden="true">
<span><span class="b-zaehler" data-zaehler>01 / 04</span> · <span data-titel>Der Tresen</span></span>
<span class="b-hinweis"><svg viewBox="0 0 14 14"><path d="M3 1l8 6-4 1-2 4z" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>Maus bewegen</span>
</div>
<h1 class="wm" aria-label="Rille, Hifi-Bar mit Cocktails und Vinyl in Leipzig"><span class="wm__zug" aria-hidden="true" data-wort="RILLE">RILLE</span></h1>
<div class="zeiger" aria-hidden="true"></div>
</section>
</main>
<div class="pult" role="group" aria-label="Steuerung der Demo">
<div class="pult__reihe">
<span class="pult__titel">Variante</span>
<button type="button" data-v="gestapelt" aria-pressed="true">Gestapelt</button>
<button type="button" data-v="im-bild" aria-pressed="false">Im Bild</button>
<button type="button" data-v="daumenkino" aria-pressed="false">Daumenkino</button>
<button type="button" data-nochmal>Auftritt ↻</button>
</div>
<div class="pult__werte" data-werte></div>
<div class="pult__meter" data-meter hidden><i></i></div>
</div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, w = document) => w.querySelector(s), $$ = (s, w = document) => [...w.querySelectorAll(s)];
const pension = $('.pension'), bar = $('.bar'), hero = $('.b-hero');
const werte = $('[data-werte]'), meter = $('[data-meter]');
const VARIANTEN = ['gestapelt', 'im-bild', 'daumenkino'];
let variante = VARIANTEN.includes(new URLSearchParams(location.search).get('v')) ? new URLSearchParams(location.search).get('v') : 'gestapelt';
/* ---------- Wortmarke einpassen (wie schrift-hero-viewportbreit) ---------- */
const ANSCHNITT = 0.16; // Anteil der Tintenhöhe, der unter der Bildkante verschwindet
const leinwand = document.createElement('canvas').getContext('2d');
const marken = $$('.wm').map(h1 => {
const zug = $('.wm__zug', h1), wort = zug.dataset.wort;
zug.innerHTML = [...wort].map((z, i) => `<span class="m"><span class="b" style="--i:${i}">${z}</span></span>`).join('');
return { h1, zug, wort, masken: $$('.m', zug), fs: 0 };
});
const schrift = h1 => { const cs = getComputedStyle(h1); return { familie: cs.fontFamily, gewicht: cs.fontWeight, sperrung: parseFloat(cs.letterSpacing) || 0 }; };
function tinte(text, s) {
leinwand.font = `${s.gewicht} 100px ${s.familie}`;
const m = leinwand.measureText(text);
return { breite: m.width, links: -m.actualBoundingBoxLeft, rechts: m.actualBoundingBoxRight, auf: m.actualBoundingBoxAscent, ab: m.actualBoundingBoxDescent, fa: m.fontBoundingBoxAscent, fd: m.fontBoundingBoxDescent };
}
function einpassen(w) {
if (!w.h1.clientWidth) return;
const s = schrift(w.h1), sp = s.sperrung / (parseFloat(getComputedStyle(w.zug).fontSize) || 100);
// Paar-Kerning zurückgeben, das durch das Teilen verloren geht
w.masken.forEach((m, i) => {
const a = w.wort[i], b = w.wort[i + 1];
const kern = b ? (tinte(a + b, s).breite - tinte(a, s).breite - tinte(b, s).breite) / 100 : 0;
m.style.marginRight = b ? `${(kern).toFixed(4)}em` : '0';
});
const erstes = tinte(w.wort[0], s), letztes = tinte(w.wort.at(-1), s), ganz = tinte(w.wort, s);
const lsb = erstes.links / 100, rsb = (letztes.breite - letztes.rechts) / 100;
w.zug.style.fontSize = '100px'; w.zug.style.setProperty('--optik', '0em');
const roh = w.zug.scrollWidth / 100;
const cs = getComputedStyle(w.h1), platz = w.h1.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
w.fs = platz / (roh - lsb - rsb - sp);
w.zug.style.fontSize = '';
w.h1.style.setProperty('--fs', `${w.fs.toFixed(2)}px`);
w.zug.style.setProperty('--optik', `${(-lsb).toFixed(4)}em`);
// Zeilenbox auf die Tinte zuschneiden: Leerraum über der Versalhöhe und unter der Grundlinie abziehen
const halb = (100 - (ganz.fa + ganz.fd)) / 2;
const oben = (halb + ganz.fa - ganz.auf) / 100, unten = (100 - halb - ganz.fa - ganz.ab) / 100;
w.zug.style.setProperty('--oben', `${(-oben).toFixed(4)}em`);
w.zug.style.setProperty('--unten', `${(-unten).toFixed(4)}em`);
w.tinteH = w.fs * (ganz.auf + ganz.ab) / 100;
if (w.h1.closest('.bar')) { hero.style.setProperty('--wm-h', `${w.tinteH.toFixed(1)}px`); w.zug.style.setProperty('--anschnitt', `${(ANSCHNITT * w.tinteH).toFixed(1)}px`); }
zeigeWerte();
}
const ro = new ResizeObserver(e => e.forEach(x => { const w = marken.find(m => m.h1 === x.target); if (w) einpassen(w); }));
marken.forEach(w => ro.observe(w.h1));
/* ---------- Daumenkino ---------- */
const bilder = $$('[data-bilder] img'), zaehler = $('[data-zaehler]'), titel = $('.b-bild-info [data-titel]'), zeiger = $('.zeiger');
const SCHWELLE = 200; // px Zeigerweg je Bild (Maloot: geschätzt 150–250)
const fein = matchMedia('(hover: hover) and (pointer: fine)');
let aktiv = 0, weg = 0, letzt = null;
const darf = () => variante === 'daumenkino' && fein.matches && !reduziert();
function zeige(i) {
aktiv = (i + bilder.length) % bilder.length;
bilder.forEach((b, k) => { b.classList.toggle('aktiv', k === aktiv); b.classList.toggle('weg', k !== aktiv); });
zaehler.textContent = `${String(aktiv + 1).padStart(2, '0')} / ${String(bilder.length).padStart(2, '0')}`;
titel.textContent = bilder[aktiv].dataset.titel;
}
function schritt(d) {
weg += Math.min(d, SCHWELLE);
if (weg >= SCHWELLE) { weg -= SCHWELLE; zeige(aktiv + 1); }
meter.firstElementChild.style.width = (weg / SCHWELLE * 100).toFixed(1) + '%';
zeigeWerte();
}
hero.addEventListener('pointermove', e => {
if (e.pointerType !== 'mouse' || !darf() || vorf) return;
if (letzt) schritt(Math.hypot(e.clientX - letzt.x, e.clientY - letzt.y));
letzt = { x: e.clientX, y: e.clientY };
});
hero.addEventListener('pointerleave', () => { letzt = null; });
/* ---------- Varianten ---------- */
function setzeVariante(v, url = true) {
variante = v;
const istBar = v !== 'gestapelt';
pension.hidden = istBar; bar.hidden = !istBar;
bar.classList.toggle('daumen', v === 'daumenkino');
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
meter.hidden = v !== 'daumenkino';
aktiv = 0; weg = 0; zeige(0);
if (v === 'im-bild') bilder.forEach((b, k) => { b.classList.toggle('aktiv', k === 0); b.classList.toggle('weg', k !== 0); });
if (url) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', '?' + q.toString() + location.hash); }
marken.forEach(einpassen);
auftritt();
}
$$('[data-v]').forEach(b => b.addEventListener('click', () => { stopp(); setzeVariante(b.dataset.v); }));
$('[data-nochmal]').addEventListener('click', () => auftritt());
function zeigeWerte() {
const w = marken[variante === 'gestapelt' ? 0 : 1]; if (!w) return;
const vw = (w.fs / innerWidth * 100).toFixed(1).replace('.', ',');
const basis = `<span>font-size <b>${vw}vw</b> gemessen</span>`;
const je = {
gestapelt: `${basis}<span>Newsreader <b>400 · opsz 72</b></span><span>Auftritt <b>1200 ms · 120 ms Versatz</b></span><span>Kurve <b>.37, 0, .63, 1</b></span>`,
'im-bild': `${basis}<span>Young Serif <b>400</b></span><span>Anschnitt <b>16 % der Tinte</b></span><span>Kurve <b>expo.out 1200 ms</b></span>`,
daumenkino: `<span>Zeigerweg <b>${Math.round(weg)} / ${SCHWELLE} px</b></span><span>Bild <b>${aktiv + 1} / ${bilder.length}</b></span><span>Wechsel <b>opacity .3s ease</b></span><span>${fein.matches && !reduziert() ? 'Zeiger <b>fein, aktiv</b>' : '<b>aus</b> (Touch oder reduziert)'}</span>`,
};
werte.innerHTML = je[variante];
}
/* ---------- Auftritt ---------- */
let auftrittT = 0;
function auftritt() {
const welt = variante === 'gestapelt' ? pension : bar;
clearTimeout(auftrittT);
if (reduziert()) { welt.classList.remove('auftritt'); return; }
welt.classList.add('auftritt');
void welt.offsetWidth;
requestAnimationFrame(() => requestAnimationFrame(() => welt.classList.remove('auftritt')));
}
/* ---------- Vorführung ---------- */
let schleife = 0, raf = 0, vorf = false, pausiert = false;
function stopp() { clearTimeout(schleife); cancelAnimationFrame(raf); vorf = false; zeiger.classList.remove('an'); }
function neustart() { stopp(); aktiv = 0; weg = 0; zeige(0); auftritt(); }
function daumenFahrt() {
// gedachter Zeiger: Lissajous-Bahn über das Bild, ca. 320 px/s
const r = hero.getBoundingClientRect(); let t0 = performance.now(), alt = null;
zeiger.classList.add('an');
const lauf = now => {
if (pausiert) return;
const t = (now - t0) / 1000;
const x = r.width * (0.5 + 0.32 * Math.sin(t * 0.9)), y = r.height * (0.42 + 0.18 * Math.sin(t * 1.7 + 1));
zeiger.style.left = x + 'px'; zeiger.style.top = y + 'px';
if (alt && !reduziert()) schritt(Math.hypot(x - alt.x, y - alt.y));
alt = { x, y };
raf = requestAnimationFrame(lauf);
};
raf = requestAnimationFrame(lauf);
}
function vorfuehren() {
stopp(); pausiert = false; vorf = true;
if (variante === 'daumenkino') {
aktiv = 0; weg = 0; zeige(0); auftritt();
if (!reduziert()) schleife = setTimeout(daumenFahrt, 1400); // reduziert: erstes Bild steht
return;
}
auftritt();
schleife = setTimeout(vorfuehren, variante === 'gestapelt' ? 5200 : 4600);
}
function pause() { pausiert = true; clearTimeout(schleife); cancelAnimationFrame(raf); }
function weiter() { if (vorf) { pausiert = false; if (variante === 'daumenkino') daumenFahrt(); else vorfuehren(); } }
const fertig = Promise.all([document.fonts.load('400 100px Newsreader'), document.fonts.load('400 100px "Young Serif"'), document.fonts.ready]);
setzeVariante(variante, false);
fertig.then(() => marken.forEach(einpassen));
fein.addEventListener?.('change', zeigeWerte);
})();
</script>
</body>
</html>