Zum Inhalt
← Hero & Section-Layouts

Meta-Labels & HUD-Typografie (Mikrotypo als Layout-Ebene)

Festivalseite eines Linzer Kulturhauses mit zweiter Textebene: fixierte Mono-Labels in den Ecken mit Live-Uhr, Scroll-Prozent und rollendem Abschnittszähler.

Neben der großen Display-Schrift liegt eine zweite, winzige Textebene in Mono oder kleiner Grotesk: Indexnummern (01/), Seitenfortschritt (0 %), Uhrzeit und Ort, Koordinaten, Projektkategorie, Scrollhinweis, Sektionscodes. Die Labels sitzen in den Ecken des Viewports oder an Rasterlinien, oft fixiert. Viele aktualisieren sich live: Zähler, Prozent, Uhr, aktiver Abschnitt. Klammern und Schrägstriche wie [ Tickets ] oder (03) markieren interaktive Elemente.

Der Kontrast zwischen riesiger und winziger Schrift gibt der Seite Präzision und Interface-Charakter. Die Labels ordnen den Raum, ohne Fläche zu verbrauchen, und zeigen, dass die Seite lebt (Uhr, Fortschritt). Viele Award-Seiten nutzen das als Markenzeichen, und es kostet fast nichts.

Wann einsetzen

Portfolios, Studios, Tech, Architektur, Kulturhäuser und Festivals: überall, wo ein systematischer Look gewünscht ist. Ergänzt fast jedes andere Muster, besonders sichtbare Raster und viewportbreite Schrift.

Wann nicht

Wenn die Labels nur Deko sind und Inhalte verdrängen. Bei warmen, emotionalen Marken, die keinen Interface-Look wollen. Nie für Kerninformationen, die in 11px zu klein wären.

Varianten

  • Fixierte Ecken-Labels (Ort, Uhrzeit, Fortschritt)
  • Indexzähler 01/04 für Abschnitte oder Projekte, hochgestellte Zähler wie Programm(31)
  • Labels an Rasterlinien: sichtbare Spalten, Lineal, Koordinaten, Bildlegenden (Fig. 01)
  • Eckige Klammern als Button-Sprache
  • Mono-Metadatenzeilen in Listen (Datum, Uhrzeit, Raum)

Umsetzung

Labels per position: fixed in die vier Ecken, Abstand zur Kante gleich dem Seitenrand des Rasters, dazu env(safe-area-inset-*). Fortschritt ohne JS: @property --p als <integer>, auf :root animiert mit animation-timeline: scroll(root), ausgegeben über counter-reset: p var(--p) und content: counter(p). Ohne scroll()-Unterstützung setzt JS --p. Uhrzeit per Intl.DateTimeFormat mit der timeZone des Hauses. Aktiven Abschnitt per IntersectionObserver an der Mittellinie (rootMargin -50% 0px -50% 0px) erkennen. Schrift 10 bis 12px Mono, versal, letter-spacing 0.02 bis 0.08em, tabular-nums. Mit mix-blend-mode: difference bleiben weiße Labels auf hellen und dunklen Abschnitten lesbar.

Robust umsetzen

Telefon. Auf ein bis zwei Labels reduzieren, zum Beispiel Marke und Ticket oder Menü, dazu höchstens den Index. Mindestgröße 12px, Tippfläche der Label-Links 44px.

Ohne Bewegung. Live-Zähler, Uhr und Prozent dürfen bleiben. Einlauf- und Roll-Animationen entfallen, keine flackernden Scramble-Effekte.

Barrierefrei. Rein dekorative Labels mit aria-hidden. Live-Uhr und Prozent nicht als aria-live auszeichnen. Navigation in Klammern bleibt echter Link mit Fokusrahmen. Mindestkontrast auch bei kleiner Schrift; bei mix-blend-mode über Bildern und Farbflächen prüfen.

Code

<!DOCTYPE html>
<!--
  Meta-Labels & HUD-Typografie (Mikrotypo als Layout-Ebene)
  Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/meta-labels-hud-typografie/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-hud="raster">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Meta-Labels &amp; HUD-Typografie · 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: '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-style: normal; 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-400-normal.woff2') format('woff2'); font-weight: 400; 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; }

/* Welt: Rezept „kultur-programm-haltung“. Werft Urfahr, Haus für Klang und Bild in Linz, Festival „Pegel 26“. */
@property --p { syntax: '<integer>'; inherits: true; initial-value: 0; }

:root {
  --grund: #efede9; --text: #0b0b0b; --leise: #5c5b58; --linie: rgba(11, 11, 11, .16); --band: rgba(11, 11, 11, .022);
  --musik: #c4421a; --theater: #3d5f9a; --kunst: #2f7d4a; --film: #b0186b;
  --kurve: cubic-bezier(0.215, 0.61, 0.355, 1);   /* Rezept: kurze Out-Kurve ohne Überschwingen */
  --ui: 150ms; --rolle: 250ms; --wechsel: 300ms; --staffel: 60ms;
  --rand: 20px;                 /* HUD-Abstand zur Kante = Seitenrand des Rasters */
  --spalte-abstand: 20px;
  --hud-groesse: 11px; --hud-laufweite: .06em;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
@supports (animation-timeline: scroll()) {
  html { animation: fortschritt linear both; animation-timeline: scroll(root); }
}
@keyframes fortschritt { to { --p: 100; } }
body { font-size: 1.125rem; line-height: 1.6; overflow-x: clip; }
a { color: inherit; text-decoration: none; }
.mono { font: 400 .8125rem/1.45 'IBM Plex Mono', ui-monospace, monospace; letter-spacing: .02em; font-variant-numeric: tabular-nums; }
.versal { text-transform: uppercase; letter-spacing: var(--hud-laufweite); }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Raster: 12 Spalten, Rand und Abstand wie die HUD-Labels */
.spalten { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--spalte-abstand); padding-inline: var(--rand); }
.raster { position: fixed; inset: 0; z-index: 0; pointer-events: none; }
.raster i { background: var(--band); border-inline: 1px solid rgba(11, 11, 11, .055); }

/* ---------- HUD: zweite Textebene, fixiert in den vier Ecken ---------- */
.hud { position: fixed; z-index: 20; color: #fff; mix-blend-mode: difference; font: 500 var(--hud-groesse)/1.4 'IBM Plex Mono', ui-monospace, monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; font-variant-numeric: tabular-nums; display: grid; gap: 2px; }
.hud--ol { top: max(var(--rand), env(safe-area-inset-top)); left: max(var(--rand), env(safe-area-inset-left)); }
.hud--or { top: max(var(--rand), env(safe-area-inset-top)); right: max(var(--rand), env(safe-area-inset-right)); display: flex; gap: 2px; }
.hud--ul { bottom: max(var(--rand), env(safe-area-inset-bottom)); left: max(var(--rand), env(safe-area-inset-left)); }
.hud--ur { bottom: max(var(--rand), env(safe-area-inset-bottom)); right: max(var(--rand), env(safe-area-inset-right)); text-align: right; justify-items: end; }
.hud .leise { opacity: .62; }
.hud b { font-weight: 500; }
/* Zeile, die beim Start von unten einläuft */
.z { display: block; overflow: hidden; }
.z > span { display: block; }
[data-start] .z > span { animation: einlauf var(--rolle) var(--kurve) both; animation-delay: calc(var(--i, 0) * var(--staffel)); }
@keyframes einlauf { from { translate: 0 105%; } }
.fenster { display: inline-block; position: relative; overflow: hidden; vertical-align: bottom; }
.fenster > span { display: inline-block; }
.fenster .alt { position: absolute; left: 0; top: 0; }
.prozent { counter-reset: p var(--p); }
.prozent::after { content: counter(p, decimal-leading-zero); }

/* Klammern als Button-Sprache */
.klammer { display: inline-block; cursor: pointer; }
.klammer > span { display: inline-flex; align-items: baseline; padding: 4px 6px; }
.klammer > span::before, .klammer > span::after { display: inline-block; transition: translate var(--ui) var(--kurve); }
.klammer > span::before { content: '['; margin-right: .5em; }
.klammer > span::after { content: ']'; margin-left: .5em; }
.klammer:hover > span::before, .klammer:focus-visible > span::before { translate: -3px 0; }
.klammer:hover > span::after, .klammer:focus-visible > span::after { translate: 3px 0; }
.klammer:focus-visible { outline: 1px solid currentColor; outline-offset: 2px; }
.klammer sup, .hoch { font-size: .8em; position: relative; top: -.45em; vertical-align: baseline; margin-left: .25em; line-height: 0; }

/* Lineal am rechten Rand (Ebene Raster) */
.lineal { position: fixed; z-index: 19; right: 6px; top: 128px; bottom: 112px; width: 8px; pointer-events: none; color: #fff; mix-blend-mode: difference;
  background: repeating-linear-gradient(to bottom, currentColor 0 1px, transparent 1px 2.5%) right / 4px 100% no-repeat,
              repeating-linear-gradient(to bottom, currentColor 0 1px, transparent 1px 25%) right / 8px calc(100% + 1px) no-repeat; opacity: .55; }
.lineal i { position: absolute; right: 0; width: 14px; height: 2px; background: currentColor; translate: 0 -1px; top: calc(var(--p) * 1%); }
@supports (animation-timeline: scroll()) {
  .lineal i { animation: lineal linear both; animation-timeline: scroll(root); }
}
@keyframes lineal { from { top: 0%; } to { top: 100%; } }

/* Varianten: Ecken (nur Ecken-Labels) · Index (+ Zähler, Sektionscodes) · Raster (+ Spalten, Lineal, Koordinaten) */
[data-ebene] { transition: opacity var(--wechsel) var(--kurve), visibility var(--wechsel); }
[data-hud="ecken"] [data-ebene="index"], [data-hud="ecken"] [data-ebene="raster"], [data-hud="index"] [data-ebene="raster"] { opacity: 0; visibility: hidden; }
[data-hud="ecken"] sup[data-ebene] { display: none; }

/* ---------- Bühne ---------- */
main, footer { position: relative; z-index: 1; }
.held { min-height: 100svh; padding-block: 76px 64px; grid-template-rows: auto 1fr auto; row-gap: 20px; align-content: stretch; }
.steuer { grid-column: 1 / -1; display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap; }
.wahl { display: flex; align-items: center; gap: 2px; font: 500 var(--hud-groesse)/1 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; }
.wahl > span { color: var(--leise); margin-right: 8px; }
.wahl button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); height: 26px; padding: 0 10px; cursor: pointer; transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
.wahl button[aria-pressed='true'] { background: var(--text); color: var(--grund); border-color: var(--text); }
.wahl button:focus-visible { outline: 1px solid var(--text); outline-offset: 2px; }
.werte { font: 400 var(--hud-groesse)/1.4 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; color: var(--leise); }
.werte b { color: var(--text); font-weight: 500; }

.intro { grid-column: 1 / 6; grid-row: 2; align-self: start; padding-top: 7vh; display: grid; gap: 22px; max-width: 34ch; }
.intro__satz { font-size: clamp(1.25rem, 1rem + .8vw, 1.625rem); line-height: 1.35; font-weight: 500; text-wrap: balance; }
.daten { display: grid; grid-template-columns: auto 1fr; gap: 4px 18px; padding-top: 14px; border-top: 1px solid var(--text); }
.daten dt { color: var(--leise); }
.knoepfe { display: flex; flex-wrap: wrap; gap: 6px; font: 500 .8125rem/1 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; margin-left: -6px; }
.knoepfe .voll > span { background: var(--text); color: var(--grund); }
.countdown { display: flex; justify-content: space-between; gap: 12px; padding-top: 12px; border-top: 1px solid var(--linie); font: 500 var(--hud-groesse)/1.4 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; font-variant-numeric: tabular-nums; }
.countdown span:first-child { color: var(--leise); }

.bild { grid-column: 6 / -1; grid-row: 2 / 4; align-self: start; position: relative; margin-top: 8px; }
.bild canvas { display: block; width: 100%; aspect-ratio: 16 / 10; }
.marke-q { position: absolute; translate: 12px -135%; background: var(--grund); padding: 2px 6px; font: 500 var(--hud-groesse)/1.2 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; white-space: nowrap; }
.marke-q--links { translate: calc(-100% - 12px) 35%; }
.bild__label { position: absolute; font: 500 var(--hud-groesse)/1.4 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; font-variant-numeric: tabular-nums; white-space: nowrap; }
.bild__label--o { left: 0; top: -22px; }
/* Pause für das endlose Interferenzbild (WCAG 2.2.2), als HUD-Label in Klammern */
.bild__ruhe { position: absolute; right: -6px; top: -28px; font: 500 var(--hud-groesse)/1.4 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; color: var(--text); background: none; border: 0; padding: 0; }
.bild__ruhe > span { padding: 5px 6px; }
.bild__ruhe[aria-pressed='true'] > span { background: var(--text); color: var(--grund); }
.reduziert .bild__ruhe, .vorschau .bild__ruhe { display: none; }
.bild__label--pegel { right: 0; top: calc(62.5% - 20px); color: var(--musik); background: var(--grund); padding: 0 0 0 8px; }
.kreuz { position: absolute; width: 13px; height: 13px; translate: -6px -6px; }
.kreuz::before, .kreuz::after { content: ''; position: absolute; background: var(--text); }
.kreuz::before { left: 6px; top: 0; width: 1px; height: 13px; }
.kreuz::after { top: 6px; left: 0; height: 1px; width: 13px; }

.titel { grid-column: 1 / -1; grid-row: 3; align-self: end; position: relative; z-index: 2; font: 900 clamp(4.25rem, 17.2vw, 16rem)/.8 'Schibsted Grotesk', sans-serif; letter-spacing: -.045em; margin-left: -.05em; display: flex; align-items: flex-start; pointer-events: none; }
.titel em { font-style: normal; color: var(--musik); margin-left: .12em; }
.titel .ausgabe { font: 500 var(--hud-groesse)/1.4 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; margin: 6.5em 0 0 .8em; white-space: nowrap; }

/* Abschnitte */
.abschnitt { padding-block: clamp(80px, 14vh, 140px); row-gap: 28px; }
.kopf { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; align-items: end; padding-bottom: 18px; border-bottom: 1px solid currentColor; }
.code { grid-column: 1 / 3; align-self: start; font: 500 var(--hud-groesse)/1.4 'IBM Plex Mono', monospace; letter-spacing: var(--hud-laufweite); text-transform: uppercase; padding-top: .9em; }
h2 { grid-column: 3 / 9; font: 800 clamp(2.75rem, 1.5rem + 5vw, 6rem)/.92 'Schibsted Grotesk', sans-serif; letter-spacing: -.035em; }
.legende { grid-column: 9 / -1; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px 16px; color: var(--leise); }
.sparte { display: inline-flex; align-items: center; gap: 8px; white-space: nowrap; }
.sparte::before { content: ''; width: 9px; height: 9px; background: var(--farbe); }
.s-musik { --farbe: var(--musik); } .s-film { --farbe: var(--film); } .s-kunst { --farbe: var(--kunst); } .s-theater { --farbe: var(--theater); }

.liste { grid-column: 1 / -1; list-style: none; display: grid; grid-template-columns: subgrid; }
.termin { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; align-items: baseline; padding-block: 18px 20px; border-bottom: 1px solid var(--linie); }
.termin .nr { grid-column: 1; color: var(--leise); }
.termin .wann { grid-column: 2 / 4; display: flex; gap: 14px; }
.termin .was { grid-column: 4 / 9; }
.termin h3 { font: 700 clamp(1.375rem, 1rem + 1.1vw, 2rem)/1.12 'Schibsted Grotesk', sans-serif; letter-spacing: -.015em; }
.termin p { font: 400 1.0625rem/1.5 'Atkinson Hyperlegible Next', sans-serif; letter-spacing: 0; color: var(--leise); margin-top: 6px; max-width: 46ch; }
.termin .wo { grid-column: 9 / 11; display: grid; gap: 2px; }
.termin .wo span:last-child { color: var(--leise); }
.termin .kauf { grid-column: 11 / -1; justify-self: end; display: flex; align-items: baseline; gap: 10px; font-weight: 500; text-transform: uppercase; letter-spacing: var(--hud-laufweite); }
.termin .kauf .preis { color: var(--leise); text-transform: none; letter-spacing: .02em; font-weight: 400; }

.dunkel { background: var(--text); color: var(--grund); }
.dunkel .leise, .dunkel p { color: #b9b7b2; }
.ausstellung .text { grid-column: 3 / 7; display: grid; gap: 22px; align-content: start; }
.ausstellung .text p { max-width: 52ch; }
.ausstellung .eck { display: grid; grid-template-columns: auto 1fr; gap: 4px 18px; padding-top: 14px; border-top: 1px solid rgba(239, 237, 233, .3); color: var(--grund); }
.ausstellung .eck dt { color: #b9b7b2; }
.spektrum { grid-column: 8 / -1; position: relative; align-self: start; padding-top: 6px; }
.spektrum svg { display: block; width: 100%; height: auto; }
.spektrum figcaption { display: flex; justify-content: space-between; margin-top: 10px; color: #b9b7b2; }

.besuch .spalte { grid-column: span 4; display: grid; gap: 12px; align-content: start; padding-top: 14px; border-top: 1px solid var(--text); }
.besuch .spalte:first-of-type { grid-column: 1 / 5; }
.besuch h3 { font: 700 1.5rem/1.15 'Schibsted Grotesk', sans-serif; }
.zeiten { display: grid; grid-template-columns: auto 1fr; gap: 6px 18px; }
.zeiten dd { text-align: right; }
.besuch p { max-width: 40ch; }
.fuss { padding-block: 40px 96px; border-top: 1px solid var(--text); row-gap: 12px; }
.fuss > * { grid-column: span 3; }
.fuss .gross { grid-column: 1 / 7; font: 800 clamp(2rem, 4vw, 3.25rem)/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.03em; }

/* ---------- Telefon: HUD auf zwei Labels reduzieren, Mindestgröße 12 px ---------- */
@media (max-width: 760px) {
  :root { --rand: 16px; --spalte-abstand: 12px; --hud-groesse: 12px; }
  .spalten { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .raster i:nth-child(n+5) { display: none; }
  .hud--ol .leise, .hud--ul, .lineal, .hud--or .klammer:not(.ticket) { display: none; }
  .hud--ur .z:first-child { display: none; }
  [data-hud="index"] .hud--ur .z:first-child, [data-hud="raster"] .hud--ur .z:first-child { display: block; }
  [data-hud="index"] .hud--ur .z:last-child, [data-hud="raster"] .hud--ur .z:last-child { display: none; }
  .held { padding-top: 64px; grid-template-rows: auto; row-gap: 22px; }
  .werte { display: none; }
  .bild { grid-column: 1 / -1; grid-row: auto; margin-top: 26px; }
  .titel { grid-row: auto; font-size: 25vw; margin-top: -2px; }
  @media (max-width: 360px) { .titel { font-size: 24vw; } }
  .bild { margin-top: 50px; }
  .bild__ruhe { top: -54px; }   /* eigene Zeile über „Fig. 01“ */
  .titel .ausgabe, .marke-q { display: none; }
  .intro { grid-column: 1 / -1; grid-row: auto; padding-top: 0; }
  .code { grid-column: 1 / -1; padding-top: 0; }
  h2 { grid-column: 1 / -1; }
  .legende { grid-column: 1 / -1; justify-content: flex-start; padding-top: 14px; }
  .termin { row-gap: 8px; }
  .termin .nr { grid-column: 1; grid-row: 1; }
  .termin .wann { grid-column: 2 / -1; grid-row: 1; justify-content: flex-end; }
  .termin .was { grid-column: 1 / -1; }
  .termin .wo { grid-column: 1 / 3; }
  .termin .kauf { grid-column: 3 / -1; }
  .ausstellung .text, .spektrum, .besuch .spalte, .besuch .spalte:first-of-type, .fuss > *, .fuss .gross { grid-column: 1 / -1; }
  .besuch .spalte + .spalte { margin-top: 20px; }
  .hud--or { top: max(calc(var(--rand) - 11px), env(safe-area-inset-top)); right: max(calc(var(--rand) - 8px), env(safe-area-inset-right)); }
  .hud--or .klammer > span { padding: 11px 8px; }   /* Tippfläche 44 px */
  .spektrum { margin-top: 12px; }
}
@media (min-width: 761px) and (max-width: 1100px) {
  .intro { grid-column: 1 / 6; padding-top: 3vh; }
  .termin .was { grid-column: 4 / 10; }
  .termin .wo { display: none; }
  .termin .kauf { grid-column: 10 / -1; flex-direction: column; align-items: flex-end; gap: 4px; }
  .legende { grid-column: 1 / -1; justify-content: flex-start; padding-top: 14px; }
  h2 { grid-column: 3 / -1; }
  .ausstellung .text { grid-column: 1 / 7; }
}

/* Touch: kein Hover, Antippen färbt die Klammer kurz */
@media (hover: none) {
  .klammer:hover > span::before, .klammer:hover > span::after { translate: none; }
  .klammer:active > span { background: currentColor; }
  .klammer:active > span > * { mix-blend-mode: difference; }
}

/* Reduzierte Bewegung: Labels stehen sofort, Zahlen zählen weiter, nichts rollt */
.reduziert [data-start] .z > span { animation: none; }
.reduziert .klammer > span::before, .reduziert .klammer > span::after, .reduziert [data-ebene], .reduziert .wahl button { transition: none; }

.vorschau body { overflow: hidden; }
.vorschau .steuer { display: none; }
.vorschau .held { padding-top: 64px; }
</style>
</head>
<body>
<div class="raster spalten" aria-hidden="true" data-ebene="raster"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div>

<!-- HUD -->
<div class="hud hud--ol">
  <a class="z" href="#festival"><span><b>Werft Urfahr</b></span></a>
  <span class="z leise" aria-hidden="true"><span>Haus für Klang und Bild · Linz</span></span>
</div>
<nav class="hud hud--or" aria-label="Hauptnavigation">
  <a class="klammer z" href="#programm"><span>Programm<sup data-ebene="index" aria-hidden="true">(31)</sup></span></a>
  <a class="klammer z" href="#besuch"><span>Besuch</span></a>
  <a class="klammer z ticket" href="#programm"><span>Tickets</span></a>
</nav>
<div class="hud hud--ul" aria-hidden="true">
  <span class="z" data-ebene="raster"><span>48.3110° N · 14.2870° E</span></span>
  <span class="z"><span>Linz <span class="uhr">--:--:--</span> <span class="zone leise">MEZ</span></span></span>
</div>
<div class="hud hud--ur" aria-hidden="true">
  <span class="z" data-ebene="index"><span><span class="fenster"><span class="idx">01</span></span>/04 · <span class="fenster"><span class="name">Festival</span></span></span></span>
  <span class="z"><span>Scroll <span class="prozent"></span>&#8239;%</span></span>
</div>
<div class="lineal" aria-hidden="true" data-ebene="raster"><i></i></div>

<main>
  <section class="held spalten" id="festival" data-abschnitt="Festival">
    <div class="steuer">
      <div class="wahl" role="group" aria-label="HUD-Ebenen">
        <span>HUD</span>
        <button type="button" data-wahl="ecken" aria-pressed="false">Ecken</button>
        <button type="button" data-wahl="index" aria-pressed="false">Index</button>
        <button type="button" data-wahl="raster" aria-pressed="true">Raster</button>
      </div>
      <p class="werte" aria-label="Verwendete Werte">Label <b>11 px</b> · Versal <b>+0,06 em</b> · Rand <b>20 px</b> · tabular-nums · Wechsel <b>250 ms</b> cubic-bezier(.215,.61,.355,1)</p>
    </div>

    <div class="intro">
      <p class="intro__satz">Elf Tage Festival für Klang, Bild und Maschine. In den alten Werfthallen am Donauufer.</p>
      <dl class="daten mono">
        <dt>Zeitraum</dt><dd>12.–22.11.2026</dd>
        <dt>Ort</dt><dd>Werft Urfahr, Linz</dd>
        <dt>Programm</dt><dd>31 Termine · 4 Sparten</dd>
      </dl>
      <div class="knoepfe">
        <a class="klammer" href="#programm"><span>Programm ansehen</span></a>
        <a class="klammer voll" href="#programm"><span>Tickets ab 9&nbsp;€</span></a>
      </div>
      <p class="countdown" aria-hidden="true"><span>Eröffnung · Do 12.11. 20:00</span><span class="bis">in 00 T 00:00:00</span></p>
    </div>

    <figure class="bild">
      <canvas width="960" height="640" aria-hidden="true"></canvas>
      <button type="button" class="klammer bild__ruhe" aria-pressed="false" data-ruhe><span>Bewegung anhalten</span></button>
      <span class="bild__label bild__label--o" data-ebene="raster" aria-hidden="true">Fig. 01 · Interferenz · 48 × 32 Punkte</span>
      <span class="kreuz" style="left:0;top:0" data-ebene="raster" aria-hidden="true"></span><span class="kreuz" style="left:100%;top:0" data-ebene="raster" aria-hidden="true"></span>
      <span class="kreuz" style="left:0;top:100%" data-ebene="raster" aria-hidden="true"></span><span class="kreuz" style="left:100%;top:100%" data-ebene="raster" aria-hidden="true"></span>
      <span class="marke-q" style="left:26%;top:34%" data-ebene="raster" aria-hidden="true">Q1 · 0,25 Hz</span>
      <span class="marke-q marke-q--links" style="left:74%;top:66%" data-ebene="raster" aria-hidden="true">Q2 · 0,40 Hz</span>
      <span class="bild__label bild__label--pegel" aria-hidden="true">Pegel <span class="db">−12,0</span> dBFS</span>
    </figure>

    <h1 class="titel"><span>Pegel</span><em>26</em><span class="ausgabe" data-ebene="index">(07)<br>Ausgabe</span></h1>
  </section>

  <section class="abschnitt spalten" id="programm" data-abschnitt="Programm">
    <div class="kopf">
      <p class="code" data-ebene="index">§ 02/04</p>
      <h2>Programm</h2>
      <p class="legende mono versal" aria-label="Sparten">
        <span class="sparte s-musik">Musik<span class="hoch" data-ebene="index">(12)</span></span>
        <span class="sparte s-film">Film<span class="hoch" data-ebene="index">(7)</span></span>
        <span class="sparte s-kunst">Kunst<span class="hoch" data-ebene="index">(5)</span></span>
        <span class="sparte s-theater">Performance<span class="hoch" data-ebene="index">(7)</span></span>
      </p>
    </div>
    <ol class="liste">
      <li class="termin mono">
        <span class="nr" data-ebene="index">(01)</span>
        <span class="wann"><time datetime="2026-11-12T20:00">Do 12.11.</time><span>20:00</span></span>
        <div class="was"><h3>Eröffnung: Donauwellen für 40 Lautsprecher</h3><p>Raumkomposition von Mirela Haslinger, live gesteuert vom Flusspegel.</p></div>
        <span class="wo"><span class="sparte s-musik versal">Musik</span><span>Kesselhalle</span></span>
        <span class="kauf"><span class="preis">ab 18&nbsp;€</span><a class="klammer" href="#programm"><span>Tickets</span></a></span>
      </li>
      <li class="termin mono">
        <span class="nr" data-ebene="index">(02)</span>
        <span class="wann"><time datetime="2026-11-13T19:30">Fr 13.11.</time><span>19:30</span></span>
        <div class="was"><h3>Kurzfilmnacht: Städte nach Mitternacht</h3><p>Neun Filme aus Linz, Łódź und Turin. Mit Gespräch.</p></div>
        <span class="wo"><span class="sparte s-film versal">Film</span><span>Turbinensaal</span></span>
        <span class="kauf"><span class="preis">9&nbsp;€</span><a class="klammer" href="#programm"><span>Tickets</span></a></span>
      </li>
      <li class="termin mono">
        <span class="nr" data-ebene="index">(03)</span>
        <span class="wann"><time datetime="2026-11-14T11:00">Sa 14.11.</time><span>11:00</span></span>
        <div class="was"><h3>Ausstellung: Rauschen als Material</h3><p>Zwölf Arbeiten zwischen Messgerät und Malerei. Bis 22.11.</p></div>
        <span class="wo"><span class="sparte s-kunst versal">Kunst</span><span>Galerie Ost</span></span>
        <span class="kauf"><span class="preis">frei</span><a class="klammer" href="#ausstellung"><span>Mehr</span></a></span>
      </li>
      <li class="termin mono">
        <span class="nr" data-ebene="index">(04)</span>
        <span class="wann"><time datetime="2026-11-14T21:00">Sa 14.11.</time><span>21:00</span></span>
        <div class="was"><h3>Körper unter Strom</h3><p>Tanz mit Sensoren: drei Tänzerinnen spielen die Orgel der Halle.</p></div>
        <span class="wo"><span class="sparte s-theater versal">Performance</span><span>Kesselhalle</span></span>
        <span class="kauf"><span class="preis">22&nbsp;€</span><a class="klammer" href="#programm"><span>Tickets</span></a></span>
      </li>
      <li class="termin mono">
        <span class="nr" data-ebene="index">(05)</span>
        <span class="wann"><time datetime="2026-11-15T16:00">So 15.11.</time><span>16:00</span></span>
        <div class="was"><h3>Modular am Sonntag</h3><p>Offene Session. Eigene Geräte mitbringen, Kabel liegen bereit.</p></div>
        <span class="wo"><span class="sparte s-musik versal">Musik</span><span>Hafenfoyer</span></span>
        <span class="kauf"><span class="preis">frei</span><a class="klammer" href="#programm"><span>Anmelden</span></a></span>
      </li>
    </ol>
  </section>

  <section class="abschnitt spalten dunkel ausstellung" id="ausstellung" data-abschnitt="Ausstellung">
    <div class="kopf">
      <p class="code" data-ebene="index">§ 03/04</p>
      <h2>Rauschen als Material</h2>
    </div>
    <div class="text">
      <p>Zwölf Künstlerinnen und Künstler arbeiten mit dem, was Technik sonst herausfiltert: Brummen, Störung, Zufall. Die Werke hängen in der früheren Schlosserei, jedes mit eigenem Messpunkt.</p>
      <dl class="eck mono">
        <dt>Raum</dt><dd>Galerie Ost, Halle 3</dd>
        <dt>Dauer</dt><dd>14.–22.11.2026</dd>
        <dt>Eintritt</dt><dd>frei</dd>
      </dl>
    </div>
    <figure class="spektrum" aria-hidden="true">
      <svg viewBox="0 0 640 300" data-spektrum></svg>
      <figcaption class="mono versal"><span>20 Hz</span><span data-ebene="raster">Fig. 02 · Spektrum, Halle 3</span><span>20 kHz</span></figcaption>
    </figure>
  </section>

  <section class="abschnitt spalten besuch" id="besuch" data-abschnitt="Besuch">
    <div class="kopf">
      <p class="code" data-ebene="index">§ 04/04</p>
      <h2>Besuch</h2>
    </div>
    <div class="spalte">
      <h3>Öffnungszeiten</h3>
      <dl class="zeiten mono">
        <dt>Di–Fr</dt><dd>14:00–22:00</dd>
        <dt>Sa–So</dt><dd>11:00–24:00</dd>
        <dt>Mo</dt><dd>geschlossen</dd>
      </dl>
    </div>
    <div class="spalte">
      <h3>Anfahrt</h3>
      <p>Straßenbahn bis Hinsenkampplatz, dann vier Minuten zu Fuß Richtung Donau. Radständer am Ufer.</p>
    </div>
    <div class="spalte">
      <h3>Barrierefrei</h3>
      <p>Alle Säle stufenlos. Induktionsschleife im Turbinensaal. Programm auch in Leichter Sprache.</p>
    </div>
  </section>
</main>
<footer class="fuss spalten mono">
  <p class="gross">Werft Urfahr</p>
  <p>Haus für Klang und Bild<br>Fährgasse 12, 4040 Linz</p>
  <p>Kasse +43 732 908 260<br>kasse@werft-urfahr.at</p>
</footer>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const KURVE = 'cubic-bezier(0.215, 0.61, 0.355, 1)';
  const ROLLE = 250;

  /* Varianten-Umschalter */
  const knoepfe = [...document.querySelectorAll('[data-wahl]')];
  function waehlen(v) {
    html.dataset.hud = v;
    knoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.wahl === v)));
  }
  knoepfe.forEach(b => b.addEventListener('click', () => waehlen(b.dataset.wahl)));

  /* Start: HUD-Zeilen laufen gestaffelt ein (60 ms Abstand) */
  const zeilen = [...document.querySelectorAll('.hud .z')];
  zeilen.forEach((z, i) => z.style.setProperty('--i', i));
  function einlaufen() {
    document.querySelectorAll('.hud').forEach(h => h.removeAttribute('data-start'));
    void html.offsetWidth;
    document.querySelectorAll('.hud').forEach(h => h.setAttribute('data-start', ''));
  }

  /* Live-Uhr in der Zeitzone des Hauses */
  const uhr = document.querySelector('.uhr'), zone = document.querySelector('.zone');
  const zeitFormat = new Intl.DateTimeFormat('de-AT', { timeZone: 'Europe/Vienna', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' });
  function ticken() {
    const teile = zeitFormat.formatToParts(new Date());
    const w = t => (teile.find(p => p.type === t) || {}).value || '';
    uhr.textContent = `${w('hour')}:${w('minute')}:${w('second')}`;
    zone.textContent = w('timeZoneName');
  }
  /* Countdown zur Eröffnung, 12.11.2026 20:00 Linzer Zeit */
  const bis = document.querySelector('.bis'), OEFFNUNG = Date.parse('2026-11-12T20:00:00+01:00');
  const zwei = n => String(n).padStart(2, '0');
  function zaehlen() {
    let r = Math.max(0, Math.floor((OEFFNUNG - Date.now()) / 1000));
    if (!r) { bis.textContent = 'läuft'; return; }
    const tg = Math.floor(r / 86400); r %= 86400;
    bis.textContent = `in ${zwei(tg)} T ${zwei(Math.floor(r / 3600))}:${zwei(Math.floor(r % 3600 / 60))}:${zwei(r % 60)}`;
  }
  ticken(); zaehlen(); setInterval(() => { ticken(); zaehlen(); }, 1000);

  /* Fortschritt: CSS (@property --p + scroll()) oder Ersatz per JS */
  if (!CSS.supports('animation-timeline: scroll()')) {
    const setzen = () => {
      const max = html.scrollHeight - innerHeight;
      html.style.setProperty('--p', Math.round(max > 0 ? scrollY / max * 100 : 0));
    };
    addEventListener('scroll', setzen, { passive: true }); addEventListener('resize', setzen); setzen();
  }

  /* Aktiver Abschnitt: IntersectionObserver an der Mittellinie, Wechsel rollt 250 ms */
  const abschnitte = [...document.querySelectorAll('[data-abschnitt]')];
  const idx = document.querySelector('.idx'), name = document.querySelector('.name');
  function rollen(el, text) {
    if (el.textContent === text) return;
    const box = el.parentElement;
    box.querySelectorAll('.alt').forEach(a => a.remove());
    if (reduziert()) { el.textContent = text; return; }
    const alt = el.cloneNode(true); alt.classList.add('alt'); box.appendChild(alt);
    el.textContent = text;
    alt.animate([{ translate: '0 0' }, { translate: '0 -105%' }], { duration: ROLLE, easing: KURVE, fill: 'forwards' }).onfinish = () => alt.remove();
    el.animate([{ translate: '0 105%' }, { translate: '0 0' }], { duration: ROLLE, easing: KURVE });
  }
  const beobachter = new IntersectionObserver(eintraege => eintraege.forEach(e => {
    if (!e.isIntersecting) return;
    const n = abschnitte.indexOf(e.target) + 1;
    rollen(idx, String(n).padStart(2, '0'));
    rollen(name, e.target.dataset.abschnitt);
  }), { rootMargin: '-50% 0px -50% 0px' });
  abschnitte.forEach(a => beobachter.observe(a));

  /* Bildwelt 1: Interferenz zweier Quellen als Punktraster, 48 × 32 */
  const leinwand = document.querySelector('.bild canvas'), ctx = leinwand.getContext('2d');
  const dbFeld = document.querySelector('.db');
  const SP = 48, ZE = 32, PEGEL_ZEILE = 20, Q1 = [.26, .34, .25], Q2 = [.74, .66, .40];
  let bw = 0, bh = 0, bildUhr = null, sichtbar = true, angehalten = false, letzteDb = 0;
  function groesse() {
    const r = leinwand.getBoundingClientRect(), dpr = Math.min(devicePixelRatio || 1, 2);
    bw = r.width; bh = r.height;
    leinwand.width = Math.round(bw * dpr); leinwand.height = Math.round(bh * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  function zeichnen(t) {
    const s = bw / SP, sz = bh / ZE, k = Math.PI * 2 / (bw / 7);
    ctx.clearRect(0, 0, bw, bh);
    let summe = 0, zeile = 0;
    const schwarz = new Path2D(), rot = new Path2D();
    for (let y = 0; y < ZE; y++) for (let x = 0; x < SP; x++) {
      const px = (x + .5) * s, py = (y + .5) * sz;
      const d1 = Math.hypot(px - Q1[0] * bw, py - Q1[1] * bh), d2 = Math.hypot(px - Q2[0] * bw, py - Q2[1] * bh);
      const v = .5 + .25 * (Math.cos(d1 * k - t * Q1[2] * Math.PI * 2) + Math.cos(d2 * k - t * Q2[2] * Math.PI * 2));
      summe += v; if (y === PEGEL_ZEILE) zeile += v;
      const r = Math.max(.4, Math.min(s, sz) * .44 * v);
      const pfad = y === PEGEL_ZEILE ? rot : schwarz;
      pfad.moveTo(px + r, py); pfad.arc(px, py, r, 0, Math.PI * 2);
    }
    ctx.fillStyle = '#0b0b0b'; ctx.fill(schwarz);
    ctx.fillStyle = '#c4421a'; ctx.fill(rot);
    ctx.fillRect(0, (PEGEL_ZEILE + .5) * sz - .5, bw, 1);
    // Kreuze an den beiden Quellen
    ctx.fillStyle = '#0b0b0b';
    [Q1, Q2].forEach(q => { ctx.clearRect(q[0] * bw - 9, q[1] * bh - 9, 18, 18); ctx.fillRect(q[0] * bw - 8, q[1] * bh - .5, 16, 1); ctx.fillRect(q[0] * bw - .5, q[1] * bh - 8, 1, 16); });
    const mittel = zeile / SP;   // Pegel = mittlere Aussteuerung der roten Zeile
    if (t - letzteDb > .25 || !letzteDb) {   // Pegelanzeige 4× pro Sekunde
      letzteDb = t;
      dbFeld.textContent = (20 * Math.log10(Math.max(.02, mittel * .8))).toFixed(1).replace('.', ',').replace('-', '−');
    }
  }
  function bildLauf(ms) {
    zeichnen(ms / 1000);
    bildUhr = (sichtbar && !angehalten && !reduziert()) ? requestAnimationFrame(bildLauf) : null;
  }
  function bildStart() { if (!bildUhr && !reduziert()) bildUhr = requestAnimationFrame(bildLauf); if (reduziert()) zeichnen(3.2); }
  new IntersectionObserver(e => { sichtbar = e[0].isIntersecting; if (sichtbar && !angehalten) bildStart(); }).observe(leinwand);
  new ResizeObserver(() => { groesse(); if (!bildUhr) zeichnen(reduziert() ? 3.2 : performance.now() / 1000); }).observe(leinwand);

  /* Bildwelt 2: Spektrum als SVG (fest, aus einer Formel) */
  const svg = document.querySelector('[data-spektrum]');
  let balken = '';
  for (let i = 0; i < 64; i++) {
    const f = i / 63, h = 70 + 230 * Math.abs(Math.sin(f * 9.3) * Math.cos(f * 3.1 + .6)) * (1 - f * .35) + 16 * Math.sin(i * 2.7);
    const hoch = Math.max(18, Math.min(288, h));
    balken += `<rect x="${i * 10 + 1}" y="${290 - hoch}" width="6" height="${hoch}" fill="${i % 16 === 9 ? '#efede9' : '#2f7d4a'}"/>`;
  }
  svg.innerHTML = `<line x1="0" y1="290.5" x2="640" y2="290.5" stroke="#efede9" stroke-opacity=".4"/>${balken}`;

  /* Vorführung: Selbst-Scroll durch die vier Abschnitte, Labels zählen mit */
  let schritt = null, zeitgeber = null;
  const weich = q => .5 - .5 * Math.cos(Math.PI * q);
  function fahren(nach, dauer) {
    return new Promise(fertig => {
      const von = scrollY;
      if (reduziert() || dauer === 0) { scrollTo(0, nach); return fertig(); }
      const t0 = performance.now();
      const lauf = t => {
        const q = Math.min(1, (t - t0) / dauer);
        scrollTo(0, von + (nach - von) * weich(q));
        if (q < 1) schritt = requestAnimationFrame(lauf); else fertig();
      };
      schritt = requestAnimationFrame(lauf);
    });
  }
  const warten = ms => new Promise(r => { zeitgeber = setTimeout(r, ms); });
  let runde = 0;
  async function vorfuehren() {
    pause(); weiterLaufen();
    const meine = ++runde;
    while (meine === runde) {
      scrollTo(0, 0); einlaufen();
      await warten(1800); if (meine !== runde) return;
      const max = html.scrollHeight - innerHeight;
      for (const a of abschnitte.slice(1)) {
        await fahren(Math.min(a.offsetTop, max), 1100); if (meine !== runde) return;
        await warten(1400); if (meine !== runde) return;
      }
      await fahren(max, 900); if (meine !== runde) return;
      await warten(900); if (meine !== runde) return;
      await fahren(0, 1300); if (meine !== runde) return;
      await warten(300);
    }
  }
  function pause() { runde++; cancelAnimationFrame(schritt); clearTimeout(zeitgeber); }
  function neustart() { pause(); scrollTo(0, 0); einlaufen(); weiterLaufen(); }
  const ruheKnopf = document.querySelector('[data-ruhe]');
  function anhalten() { pause(); angehalten = true; ruheKnopf.setAttribute('aria-pressed', 'true'); }
  function weiterLaufen() { angehalten = false; ruheKnopf.setAttribute('aria-pressed', 'false'); bildStart(); }
  ruheKnopf.addEventListener('click', () => angehalten ? weiterLaufen() : anhalten());

  einlaufen();
  document.fonts.ready.then(() => { groesse(); bildStart(); });

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