Zum Inhalt
← Galerien & Slider

Tiefenflug-Galerie

Ausstellungsarchiv einer Iserlohner Kunsthalle als Gang durch die Tiefe: Beim Scrollen kommt eine Ausstellung nach der anderen nach vorn, eine Leiste nennt Sparte, Datum und Zähler.

Ausstellungs- oder Projektarchiv mit Querformaten, Kultur und Kreativstudio.

Die Bilder stehen hintereinander im Raum. Beim Scrollen fährt der Blick hindurch: Das vordere Bild wird groß und blendet aus, das nächste kommt aus der Tiefe nach vorn. Eine Bühne bleibt dabei stehen (position: sticky), nur die Tiefe ändert sich. Eine Leiste am Rand nennt Titel, Datum und Zähler des Bildes, das gerade vorn ist.

Ein Archiv wird zum Weg, den man abgeht. Die Tiefe ordnet die Bilder zeitlich (vorn das Neueste, hinten das Ältere), ohne dass man ein Raster lesen muss. Weil nur die Tiefe wechselt und der Rahmen stehen bleibt, bleibt die Orientierung erhalten.

Wann einsetzen

Ausstellungs- oder Projektarchiv, Lookbook, Werkschau mit 6 bis 20 Bildern, die man nacheinander erleben soll. Gut als ein besonderes Kapitel auf einer sonst ruhigen Seite.

Wann nicht

Shops, Immobilien, Hotels: Fotos sind im Anflug klein und perspektivisch verkürzt. Inhalte, die man vergleichen muss (Preise, Zimmer, Produkte). Mehr als eine solche Scroll-Bühne pro Seite. Fotografen-Seiten, bei denen das Werk nicht skaliert werden darf. Lange Programmlisten, die man schnell überfliegen will.

Varianten

  • Flug: Querformate wechseln links und rechts der Mitte, der Blick fliegt zwischen ihnen hindurch (Archiv)
  • Achse: Hochformate auf der Mittelachse, ein Bild nach dem anderen, Titel groß dahinter (Lookbook, Werkschau)
  • Wolke: mehrere kleine Bilder je Tiefenstufe verstreut (dichtes Studio-Archiv, ab etwa 30 Bildern eher three.js)
  • Ansicht umschalten: Flug oder ruhiges Raster (für Tastatur, Vergleich und reduzierte Bewegung)

Umsetzung

Ein hoher Abschnitt (Bildzahl × 85 svh) mit einer Bühne in position: sticky, darauf perspective: 1200px und transform-style: preserve-3d. Eine registrierte Zahl @property --p wird per animation-timeline: view() über den Abschnitt von 0 auf Bildzahl − 1 animiert. Jedes Bild rechnet daraus seinen Abstand d = p − i und setzt translate3d(x, y, d × 850px); die Deckkraft ergibt sich mit clamp() aus d (aus der Tiefe einblenden, kurz vor der Kamera ausblenden). Boden und Decke der Halle sind zwei um 90° gekippte Flächen mit einem Fugenraster aus Verläufen; ihre Querfugen liegen im Bildabstand und wandern mit --p, so liest man die Fahrt als Raum. Ohne Scroll-Timeline setzt ein kleines Skript --p beim Scrollen (rAF). Dasselbe Skript schreibt Titel, Datum und Zähler des vorderen Bildes in die Leiste und nimmt vorbeigeflogene Bilder aus dem Zeiger (pointer-events: none). Die Deckkraft liegt nur am Bild, nicht am Link, damit der versteckte Linktext lesbar bleibt. Jedes Bild ist ein echter Link; bekommt es den Fokus, scrollt die Seite an seine Stelle. Ein Umschalter Raum/Raster zeigt dieselben Links als ruhiges Raster. three.js erst ab etwa 30 Bildern.

Robust umsetzen

Telefon. Gleiche Bühne, Bilder schmaler (72 vw), seitlicher Versatz kleiner, 70 svh Scrollweg je Bild. Natives Wischen, kein Scroll-Kapern.

Ohne Bewegung. Kein Flug: Die Bilder stehen als ruhiges Raster mit Bildunterschrift untereinander. Der Umschalter bietet dieselbe Ansicht für alle an.

Barrierefrei. Jedes Bild ist ein Link mit Titel, Sparte und Datum im Linktext; bekommt ein Link den Fokus, scrollt die Seite an seine Stelle und das Bild steht vorn. Pfeiltasten springen zum nächsten oder vorigen Bild. Ein Klick auf ein hinteres Bild holt es erst nach vorn, ein Klick auf das vordere öffnet es. Vorbeigeflogene Bilder bekommen pointer-events: none. Die Leiste ist aria-hidden, weil die Links dieselben Angaben tragen. Ansicht Raster als gleichwertiger Weg ohne Bewegung (WCAG 2.3.3). Spartenfarbe nie allein, immer mit Text. Kein Scroll-Kapern, kein Autoplay; die Vorführung läuft nur in der Galerie-Vorschau.

Code

<!DOCTYPE html>
<!--
  Tiefenflug-Galerie
  Fundus · Galerien & Slider · https://cecodesign.de/lab/fundus/galerien-slider/tiefenflug-galerie/
  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>Tiefenflug-Galerie · 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-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }

/* Welt: Kunsthalle Gießhaus, Iserlohn · Rezept kultur-programm-haltung
   Schwarz auf warmem Off-White, Spartenfarben immer mit Text-Label */
@property --p { syntax: '<number>'; inherits: true; initial-value: 0; }

:root {
  --grund: #efede9; --text: #0b0b0b; --leise: rgba(11, 11, 11, .72); --linie: #0b0b0b;
  --foto: #c4421a; --arch: #3d5f9a; --kunst: #2f7d4a; --design: #b0186b;
  --fokus: #3d5f9a;
  --kurve: cubic-bezier(0.25, 1, 0.5, 1);       /* token-swiss-knackig, Eintritt */
  --kurve-aus: cubic-bezier(0.5, 0, 0.75, 0);   /* Austritt */
  --ui: 250ms;
  --kopf: 64px;
  /* Raum */
  --perspektive: 1200px;
  --tiefe: 850px;           /* Abstand je Bild in der Tiefe */
  --weg: 85svh;             /* Scrollweg je Bild */
  --n: 8;
  --schub-max: 24vw;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; font-size: 100%; -webkit-font-smoothing: antialiased; }
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; }
a { color: inherit; }
:focus-visible { outline: 3px solid var(--fokus); outline-offset: 3px; }
.mono { font: 500 .8125rem/1.35 'IBM Plex Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; letter-spacing: .04em; }
.versal { text-transform: uppercase; letter-spacing: .12em; font-size: .75rem; }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ---------- Kopf (sticky, klare Unterkante wie im Rezept) ---------- */
.kopf { position: sticky; top: 0; z-index: 20; height: var(--kopf); display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 clamp(16px, 3vw, 40px); background: var(--grund); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: baseline; gap: 10px; text-decoration: none; }
.marke b { font: 800 1.375rem/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.02em; }
.marke span { font-size: .875rem; color: var(--leise); }
.kopf nav ul { display: flex; gap: clamp(14px, 2vw, 28px); list-style: none; align-items: center; }
.kopf nav a { font: 600 .9375rem/1 'Schibsted Grotesk', sans-serif; text-decoration: none; padding: 12px 0; text-underline-offset: 6px; }
.kopf nav a:hover, .kopf nav a[aria-current] { text-decoration: underline; text-decoration-thickness: 2px; }
.kopf nav a.ticket { background: var(--text); color: var(--grund); padding: 12px 16px; border-radius: 999px; text-decoration: none; transition: background-color var(--ui) var(--kurve); }
.kopf nav a.ticket:hover { background: #2a2a2a; }

/* ---------- Strecke: hoher Abschnitt, Bühne klebt ---------- */
.strecke { position: relative; height: calc(100svh - var(--kopf) + (var(--n) - 1) * var(--weg)); }
.buehne { position: sticky; top: var(--kopf); height: calc(100svh - var(--kopf)); overflow: hidden; perspective: var(--perspektive); perspective-origin: 50% 46%; --p: 0; }
@supports (animation-timeline: view()) {
  .strecke { view-timeline: --strecke block; }
  body:not([data-ansicht=raster]) .buehne { animation: flug linear both; animation-timeline: --strecke; animation-range: contain 0% contain 100%; }
}
@keyframes flug { from { --p: 0; } to { --p: 7; } }

.raum { position: absolute; inset: 0; list-style: none; transform-style: preserve-3d; }
.karte {
  --d: calc(var(--p) - var(--i));                 /* Abstand zur Kamera in Bildern: < 0 = noch in der Tiefe */
  position: absolute; left: 50%; top: 50%; width: min(40vw, 640px);
  --schub: calc(var(--schub-max) * clamp(0, 1 - var(--p) * 1.25, 1) * min(1, var(--i)));   /* hält den Einstieg frei, bis man losscrollt */
  transform: translate(-50%, -50%) translate3d(calc(var(--x) + var(--schub)), var(--y), calc(min(var(--d), .6) * var(--tiefe)));
  --sicht: calc(clamp(0, var(--d) + 4.5, 1) * clamp(0, (.5 - var(--d)) / .35, 1));   /* Deckkraft nur am Bild, nicht am (versteckten) Linktext */
  will-change: transform;
}
.karte[data-vorbei] { pointer-events: none; }
.karte__link { display: block; text-decoration: none; outline-offset: 6px; }
.karte__bild { opacity: var(--sicht, 1); display: block; aspect-ratio: 3 / 2; background: #dcd9d3; overflow: hidden; box-shadow: 0 30px 60px -20px rgba(11, 11, 11, .35); }
/* Hallenboden: Fugenraster in der Tiefe, Querfugen genau auf den Bildabständen, wandern mit --p */
.halle { position: absolute; left: -150%; right: -150%; top: 50%; height: calc(var(--tiefe) * 7); pointer-events: none;
  transform-origin: 50% 0; transform: translateY(var(--boden, 34vh)) rotateX(-90deg);
  background:
    repeating-linear-gradient(90deg, transparent 0 79px, rgba(11, 11, 11, .2) 79px 81px, transparent 81px 160px),
    repeating-linear-gradient(180deg, rgba(11, 11, 11, .5) 0 3px, transparent 3px var(--tiefe));
  background-position: 50% 0, 0 calc(var(--p) * var(--tiefe) * -1 + var(--tiefe) * .5);
  -webkit-mask: linear-gradient(to bottom, transparent 0, #000 6%, #000 22%, transparent 70%); mask: linear-gradient(to bottom, transparent 0, #000 6%, #000 22%, transparent 70%); }
.halle--decke { transform: translateY(calc(var(--boden, 34vh) * -1.3)) rotateX(-90deg); opacity: .5; }
.karte__bild img { display: block; width: 100%; height: 100%; object-fit: cover; }
.karte__text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Einstieg liegt als Ebene auf der Bühne und blendet mit dem ersten Schritt aus */
.einstieg { position: absolute; z-index: 2; pointer-events: none; left: clamp(16px, 3vw, 40px); top: clamp(24px, 6vh, 64px); max-width: min(30rem, 36vw); opacity: clamp(0, 1 - var(--p) * 2.2, 1); translate: 0 calc(var(--p) * -40px); }
.einstieg .versal { display: block; margin-bottom: 18px; }
.einstieg h1 { font: 800 clamp(2.5rem, 4.9vw, 5rem)/.98 'Schibsted Grotesk', sans-serif; letter-spacing: -.03em; text-wrap: balance; }
.einstieg p { margin-top: 20px; font-size: 1.125rem; line-height: 1.55; max-width: 34ch; }
.einstieg .hinweis { margin-top: 22px; display: inline-flex; gap: 10px; align-items: center; color: var(--leise); }
.einstieg .hinweis::after { content: ''; width: 1px; height: 28px; background: currentColor; }

/* Leiste: Titel, Sparte, Datum, Zähler des vorderen Bildes */
.leiste { position: absolute; z-index: 3; left: clamp(16px, 3vw, 40px); right: clamp(16px, 3vw, 40px); bottom: clamp(16px, 3.4vh, 32px); display: flex; justify-content: space-between; align-items: end; gap: 24px; pointer-events: none; }
.leiste__links { max-width: 34rem; transition: opacity var(--ui) var(--kurve), translate var(--ui) var(--kurve); }
.leiste__links.wechsel { opacity: 0; translate: 0 6px; transition-timing-function: var(--kurve-aus); transition-duration: 150ms; }
.leiste__titel { display: block; font: 700 clamp(1.375rem, 2.2vw, 2rem)/1.08 'Schibsted Grotesk', sans-serif; letter-spacing: -.015em; margin-top: 10px; text-wrap: balance; }
.leiste__meta { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.leiste__rechts { display: grid; justify-items: end; gap: 10px; }
.zaehler { font: 500 .9375rem/1 'IBM Plex Mono', monospace; font-variant-numeric: tabular-nums; }
.ticks { display: flex; gap: 4px; }
.ticks i { width: 18px; height: 3px; background: rgba(11, 11, 11, .22); transition: background-color var(--ui) var(--kurve); }
.ticks i.an { background: var(--text); }

.sparte { display: inline-flex; align-items: center; gap: 8px; font: 500 .75rem/1 'IBM Plex Mono', monospace; letter-spacing: .1em; text-transform: uppercase; }
.sparte::before { content: ''; width: 10px; height: 10px; background: var(--farbe, var(--text)); }
.sparte--foto { --farbe: var(--foto); } .sparte--arch { --farbe: var(--arch); } .sparte--kunst { --farbe: var(--kunst); } .sparte--design { --farbe: var(--design); }

/* Ansicht-Umschalter: Raum oder Raster (gleichwertiger Weg ohne Flug) */
.ansicht { position: absolute; z-index: 4; right: clamp(16px, 3vw, 40px); top: clamp(16px, 3vh, 28px); display: flex; border: 1px solid var(--linie); border-radius: 999px; padding: 3px; background: var(--grund); }
.ansicht button { font: 500 .75rem/1 'IBM Plex Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: var(--text); background: none; border: 0; border-radius: 999px; min-height: 36px; padding: 0 14px; cursor: pointer; transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
.ansicht button[aria-pressed=true] { background: var(--text); color: var(--grund); }
.ansicht button:hover:not([aria-pressed=true]) { background: rgba(11, 11, 11, .08); }

/* Achse-Titel (Variante achse): groß hinter den Bildern */
.achsentitel { display: none; }

/* ---------- Variante achse: Hochformate auf der Mittelachse ---------- */
body[data-v=achse] .buehne { perspective-origin: 50% -12%; }
body[data-v=achse] .karte { width: min(25vw, 400px); --x: 0px !important; --y: 4vh !important; --schub-max: 0px; }
body[data-v=achse] .karte__bild { aspect-ratio: 2 / 3; }
body[data-v=achse] .achsentitel { display: grid; place-items: center; position: absolute; inset: 0; padding: 0 3vw; pointer-events: none; }
body[data-v=achse] .achsentitel span { font: 900 clamp(3.5rem, 10.5vw, 11rem)/.9 'Schibsted Grotesk', sans-serif; letter-spacing: -.04em; text-transform: uppercase; text-align: center; text-wrap: balance; max-width: 14ch; translate: 0 4vh; transition: opacity var(--ui) var(--kurve); }
body[data-v=achse] .achsentitel span.wechsel { opacity: 0; transition-duration: 150ms; transition-timing-function: var(--kurve-aus); }
body[data-v=achse] .einstieg { max-width: min(22rem, 26vw); }
body[data-v=achse] .einstieg h1 { font-size: clamp(2.25rem, 3.6vw, 3.75rem); }

/* ---------- Ansicht Raster (auch bei reduzierter Bewegung) ---------- */
body[data-ansicht=raster] .strecke { height: auto; }
body[data-ansicht=raster] .buehne { position: relative; top: 0; height: auto; overflow: visible; perspective: none; --p: 0 !important; padding: clamp(24px, 6vh, 64px) clamp(16px, 3vw, 40px) 56px; display: grid; gap: 40px; }
body[data-ansicht=raster] .einstieg { position: static; opacity: 1; translate: none; max-width: 40rem; }
body[data-ansicht=raster] .einstieg .hinweis { display: none; }
body[data-ansicht=raster] .ansicht { top: clamp(24px, 6vh, 64px); }
body[data-ansicht=raster] .raum { position: static; transform-style: flat; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); gap: 40px 24px; }
body[data-ansicht=raster] .karte { position: static; width: auto; transform: none; --sicht: 1; will-change: auto; pointer-events: auto; }
body[data-ansicht=raster] .karte__text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; white-space: normal; display: grid; gap: 8px; margin-top: 14px; }
body[data-ansicht=raster] .karte__titel { font: 700 1.25rem/1.15 'Schibsted Grotesk', sans-serif; letter-spacing: -.01em; }
body[data-ansicht=raster] .karte__meta { display: flex; gap: 14px; flex-wrap: wrap; align-items: center; font: 500 .8125rem/1 'IBM Plex Mono', monospace; }
body[data-ansicht=raster] .karte__link:hover .karte__titel { text-decoration: underline; text-underline-offset: 4px; }
body[data-ansicht=raster] .leiste, body[data-ansicht=raster] .achsentitel, body[data-ansicht=raster] .halle { display: none; }
body[data-ansicht=raster][data-v=achse] .karte__bild { aspect-ratio: 3 / 2; }
body[data-ansicht=raster] .karte, body[data-ansicht=raster] .einstieg { animation: auftauchen 300ms var(--kurve) both; }
@keyframes auftauchen { from { opacity: 0; } }

/* ---------- Danach: vollständige Liste + Fuß ---------- */
.nach { padding: clamp(56px, 10vh, 120px) clamp(16px, 3vw, 40px) 40px; border-top: 1px solid var(--linie); display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 32px; }
.nach h2 { font: 800 clamp(1.75rem, 3vw, 2.75rem)/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.02em; }
.nach > div > p { margin-top: 14px; font-size: 1.125rem; line-height: 1.55; max-width: 34ch; }
.liste { list-style: none; border-top: 1px solid var(--linie); }
.liste li { display: grid; grid-template-columns: 3ch minmax(0, 1fr) 13ch 22ch; gap: 16px; align-items: baseline; padding: 14px 8px; border-bottom: 1px solid rgba(11, 11, 11, .24); scroll-margin-top: calc(var(--kopf) + 24px); transition: background-color 600ms var(--kurve); }
.liste li:target { background: #e2ded6; }
.liste__nr, .liste time { font: 500 .8125rem/1.3 'IBM Plex Mono', monospace; font-variant-numeric: tabular-nums; }
.liste__titel { font: 700 1.125rem/1.2 'Schibsted Grotesk', sans-serif; }
.fuss { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px 40px; padding: 28px clamp(16px, 3vw, 40px) 40px; font-size: 1rem; line-height: 1.5; }
.fuss b { font-family: 'Schibsted Grotesk', sans-serif; }

/* ---------- Bedienpult der Demo (Varianten) ---------- */
.pult { position: fixed; z-index: 30; right: 16px; top: calc(var(--kopf) + 84px); display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; max-width: calc(100vw - 32px); padding: 8px 10px; background: rgba(11, 11, 11, .9); color: #efede9; border-radius: 12px; font: 500 .6875rem/1.2 'IBM Plex Mono', monospace; letter-spacing: .12em; text-transform: uppercase; }
.pult button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: #efede9; background: none; border: 1px solid rgba(239, 237, 233, .3); border-radius: 999px; min-height: 30px; padding: 0 12px; cursor: pointer; }
.pult button[aria-pressed=true] { background: #efede9; color: #0b0b0b; border-color: #efede9; }
.pult__werte { color: rgba(239, 237, 233, .78); letter-spacing: .06em; text-transform: none; }

/* ---------- Telefon ---------- */
@media (max-width: 760px) {
  :root { --perspektive: 900px; --tiefe: 800px; --weg: 70svh; --kopf: 56px; --schub-max: 0px; }
  .halle { --boden: 24vh; }
  .kopf nav li:not(:last-child) { display: none; }
  .marke span { display: none; }
  .karte { width: 72vw; --x: var(--xm) !important; --y: 9vh !important; }
  .einstieg { max-width: none; right: 16px; top: 18px; }
  .einstieg h1 { font-size: clamp(1.875rem, 8.4vw, 2.5rem); max-width: 12ch; }
  .einstieg p { font-size: 1rem; margin-top: 10px; }
  .einstieg .hinweis { display: none; }
  .ansicht { top: auto; bottom: 108px; right: 16px; }
  .leiste { flex-direction: row; align-items: end; }
  .leiste__titel { font-size: 1.25rem; }
  .ticks i { width: 10px; }
  body[data-v=achse] .buehne { perspective-origin: 50% 30%; }
  body[data-v=achse] .karte { width: 54vw; --x: 0px !important; --y: 12vh !important; }
  body[data-v=achse] .einstieg { max-width: none; }
  body[data-v=achse] .einstieg h1 { font-size: clamp(1.75rem, 7.6vw, 2.25rem); }
  body[data-v=achse] .achsentitel span { font-size: clamp(2.5rem, 13vw, 4rem); }
  body[data-ansicht=raster] .ansicht { position: static; justify-self: start; }
  .nach { grid-template-columns: minmax(0, 1fr); }
  .liste li { grid-template-columns: 3ch minmax(0, 1fr); row-gap: 8px; }
  .liste li .sparte, .liste li time { grid-column: 2; }
}
@media (max-width: 360px) { .kopf nav a.ticket { padding: 12px 12px; } .leiste__rechts .ticks { display: none; } }

/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .pult { display: none; }

/* Reduzierte Bewegung: Raster (per Skript), keine Übergänge */
:root.reduziert *, :root.reduziert *::before { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (hover: none) { .kopf nav a:hover { text-decoration: none; } }
@media (forced-colors: active) {
  button, [role=button], [role=tab] { border: 1px solid ButtonText !important; }
  .ansicht button[aria-pressed=true] { outline: 2px solid Highlight; }
  .sparte::before { forced-color-adjust: none; }
}
</style>
</head>
<body data-v="flug" data-ansicht="raum">
<header class="kopf">
  <a class="marke" href="#start"><b>Gießhaus</b><span>Kunsthalle Iserlohn</span></a>
  <nav aria-label="Hauptnavigation">
    <ul>
      <li><a href="#start">Programm</a></li>
      <li><a href="#start">Besuch</a></li>
      <li><a href="#start" aria-current="page">Archiv</a></li>
      <li><a href="#start">Barrierefrei</a></li>
      <li><a class="ticket" href="#start">Tickets</a></li>
    </ul>
  </nav>
</header>

<main id="start">
  <section class="strecke" aria-labelledby="titel">
    <div class="buehne">
      <div class="achsentitel" aria-hidden="true"><span data-achsentitel>Licht und was es übrig lässt</span></div>

      <div class="einstieg">
        <span class="mono versal">Archiv · Halle 4 · 2023–2026</span>
        <h1 id="titel">Acht Ausstellungen. Ein Gang durch die Halle.</h1>
        <p>Die jüngste steht vorn, die älteste ganz hinten. Scrollen Sie hindurch oder wählen Sie das Raster.</p>
        <span class="hinweis mono">Scrollen</span>
      </div>

      <div class="ansicht" role="group" aria-label="Ansicht des Archivs">
        <button type="button" data-ansicht="raum" aria-pressed="true">Raum</button>
        <button type="button" data-ansicht="raster" aria-pressed="false">Raster</button>
      </div>

      <div class="halle" aria-hidden="true"></div>
      <div class="halle halle--decke" aria-hidden="true"></div>
      <ol class="raum" aria-label="Ausstellungen, neueste zuerst">
      <li class="karte" style="--i:0; --x:22vw; --y:-7vh; --xm:10vw" data-titel="Licht und was es übrig lässt" data-sparte="Fotografie & Raum" data-sk="foto" data-datum="14.05.–02.08.2026">
        <a class="karte__link" href="#a-licht">
          <span class="karte__bild"><img src="bilder/typo-plakat.jpg" data-quer="bilder/typo-plakat.jpg" data-hoch="bilder/typo-plakat-hoch.jpg" alt="Plakat mit dem Wort LICHT in riesigen schwarzen Versalien, einer orangeroten Kreisscheibe und der kursiven Zeile „und was es übrig lässt“ auf limettengrünem Grund" width="1200" height="800" decoding="async"></span>
          <span class="karte__text"><span class="karte__titel">Licht und was es übrig lässt</span> <span class="karte__meta"><span class="sparte sparte--foto">Fotografie & Raum</span> <time datetime="2026-05-14">14.05.–02.08.2026</time></span></span>
        </a>
      </li>
      <li class="karte" style="--i:1; --x:-24vw; --y:6vh; --xm:-12vw" data-titel="Kragstufen. Bauen ohne Stütze" data-sparte="Architektur" data-sk="arch" data-datum="21.11.2025–22.02.2026">
        <a class="karte__link" href="#a-kragstufen">
          <span class="karte__bild"><img src="bilder/beton-treppe.jpg" data-quer="bilder/beton-treppe.jpg" data-hoch="bilder/beton-treppe-hoch.jpg" alt="Freitragende Betonstufen an einer hellen Sichtbetonwand, tiefes Abendlicht wirft treppenförmige Schatten und weiches Blattlicht auf die Wand" width="1200" height="800" decoding="async"></span>
          <span class="karte__text"><span class="karte__titel">Kragstufen. Bauen ohne Stütze</span> <span class="karte__meta"><span class="sparte sparte--arch">Architektur</span> <time datetime="2025-11-21">21.11.2025–22.02.2026</time></span></span>
        </a>
      </li>
      <li class="karte" style="--i:2; --x:25vw; --y:-9vh; --xm:12vw" data-titel="Stille Zimmer" data-sparte="Kunst" data-sk="kunst" data-datum="05.06.–14.09.2025">
        <a class="karte__link" href="#a-zimmer">
          <span class="karte__bild"><img src="bilder/fenster-licht.jpg" data-quer="bilder/fenster-licht.jpg" data-hoch="bilder/fenster-licht-hoch.jpg" alt="Ruhiger Raum mit salbeigrüner Wand, auf die Abendsonne ein Fensterbild wirft; davor eine Eichenbank mit dunkler Vase, trockenem Zweig und Büchern" width="1200" height="800" decoding="async"></span>
          <span class="karte__text"><span class="karte__titel">Stille Zimmer</span> <span class="karte__meta"><span class="sparte sparte--kunst">Kunst</span> <time datetime="2025-06-05">05.06.–14.09.2025</time></span></span>
        </a>
      </li>
      <li class="karte" style="--i:3; --x:-23vw; --y:8vh; --xm:-11vw" data-titel="Gedreht und gebrannt. Keramik aus dem Sauerland" data-sparte="Design" data-sk="design" data-datum="08.02.–18.05.2025">
        <a class="karte__link" href="#a-gedreht">
          <span class="karte__bild"><img src="bilder/keramik-stillleben.jpg" data-quer="bilder/keramik-stillleben.jpg" data-hoch="bilder/keramik-stillleben-hoch.jpg" alt="Stillleben aus heller Flaschenvase, schwarzem Kugelgefäß, ockerfarbenem Becher und flachen Schalen auf weißen Sockeln vor terrakottafarbenem Hintergrund" width="1200" height="800" decoding="async"></span>
          <span class="karte__text"><span class="karte__titel">Gedreht und gebrannt. Keramik aus dem Sauerland</span> <span class="karte__meta"><span class="sparte sparte--design">Design</span> <time datetime="2025-02-08">08.02.–18.05.2025</time></span></span>
        </a>
      </li>
      <li class="karte" style="--i:4; --x:24vw; --y:-6vh; --xm:12vw" data-titel="Mittelgebirge" data-sparte="Fotografie & Raum" data-sk="foto" data-datum="12.09.–08.12.2024">
        <a class="karte__link" href="#a-mittelgebirge">
          <span class="karte__bild"><img src="bilder/huegel-nebel.jpg" data-quer="bilder/huegel-nebel.jpg" data-hoch="bilder/huegel-nebel-hoch.jpg" alt="Hintereinander gestaffelte bewaldete Hügelketten, die im rosafarbenen Morgendunst immer heller werden, darüber eine blasse tiefe Sonne" width="1200" height="800" loading="lazy" decoding="async"></span>
          <span class="karte__text"><span class="karte__titel">Mittelgebirge</span> <span class="karte__meta"><span class="sparte sparte--foto">Fotografie & Raum</span> <time datetime="2024-09-12">12.09.–08.12.2024</time></span></span>
        </a>
      </li>
      <li class="karte" style="--i:5; --x:-25vw; --y:9vh; --xm:-12vw" data-titel="Faden und Falte" data-sparte="Design" data-sk="design" data-datum="03.05.–25.08.2024">
        <a class="karte__link" href="#a-faden">
          <span class="karte__bild"><img src="bilder/stoff-stapel.jpg" data-quer="bilder/stoff-stapel.jpg" data-hoch="bilder/stoff-stapel-hoch.jpg" alt="Ordentlich gestapelte, gefaltete Leinentücher in Pflaume, Altrosa, Ocker und Creme auf einem Holztisch im weichen Seitenlicht" width="1200" height="800" loading="lazy" decoding="async"></span>
          <span class="karte__text"><span class="karte__titel">Faden und Falte</span> <span class="karte__meta"><span class="sparte sparte--design">Design</span> <time datetime="2024-05-03">03.05.–25.08.2024</time></span></span>
        </a>
      </li>
      <li class="karte" style="--i:6; --x:22vw; --y:-8vh; --xm:11vw" data-titel="Die Ordnung der Früchte. Stillleben heute" data-sparte="Kunst" data-sk="kunst" data-datum="10.11.2023–18.02.2024">
        <a class="karte__link" href="#a-fruechte">
          <span class="karte__bild"><img src="bilder/zitronen-tisch.jpg" data-quer="bilder/zitronen-tisch.jpg" data-hoch="bilder/zitronen-tisch-hoch.jpg" alt="Heller Teller mit einer halbierten und einer ganzen Zitrone auf dunklem Schiefertisch, daneben weitere Zitronen und Blätter im Gegenlicht mit langen Schatten" width="1200" height="800" loading="lazy" decoding="async"></span>
          <span class="karte__text"><span class="karte__titel">Die Ordnung der Früchte. Stillleben heute</span> <span class="karte__meta"><span class="sparte sparte--kunst">Kunst</span> <time datetime="2023-11-10">10.11.2023–18.02.2024</time></span></span>
        </a>
      </li>
      <li class="karte" style="--i:7; --x:-24vw; --y:5vh; --xm:-12vw" data-titel="Hülle. Glas zwischen Labor und Flakon" data-sparte="Design" data-sk="design" data-datum="15.06.–24.09.2023">
        <a class="karte__link" href="#a-huelle">
          <span class="karte__bild"><img src="bilder/flakon.jpg" data-quer="bilder/flakon.jpg" data-hoch="bilder/flakon-hoch.jpg" alt="Rechteckiger Glasflakon mit bernsteinfarbener Füllung und schwarzer Kappe auf einem runden Steinsockel, daneben eine weiße Kugel, vor kräftigem Kobaltblau" width="1200" height="800" loading="lazy" decoding="async"></span>
          <span class="karte__text"><span class="karte__titel">Hülle. Glas zwischen Labor und Flakon</span> <span class="karte__meta"><span class="sparte sparte--design">Design</span> <time datetime="2023-06-15">15.06.–24.09.2023</time></span></span>
        </a>
      </li>
      </ol>

      <div class="leiste" aria-hidden="true">
        <div class="leiste__links" data-leiste>
          <span class="leiste__meta"><span class="sparte sparte--foto" data-l-sparte>Fotografie &amp; Raum</span><span class="mono" data-l-datum>14.05.–02.08.2026</span></span>
          <span class="leiste__titel" data-l-titel>Licht und was es übrig lässt</span>
        </div>
        <div class="leiste__rechts">
          <span class="zaehler" data-l-zaehler>01 / 08</span>
          <span class="ticks" data-ticks></span>
        </div>
      </div>
    </div>
  </section>

  <section class="nach" aria-labelledby="alle">
    <div>
      <h2 id="alle">Alle Ausstellungen</h2>
      <p>Kataloge, Raumpläne und Pressebilder zu jeder Ausstellung. Ältere Jahrgänge seit 2004 im Archivlesesaal, Di–Fr nach Anmeldung.</p>
    </div>
    <ol class="liste">
      <li id="a-licht"><span class="liste__nr">01</span><span class="liste__titel">Licht und was es übrig lässt</span><span class="sparte sparte--foto">Fotografie & Raum</span><time datetime="2026-05-14">14.05.–02.08.2026</time></li>
      <li id="a-kragstufen"><span class="liste__nr">02</span><span class="liste__titel">Kragstufen. Bauen ohne Stütze</span><span class="sparte sparte--arch">Architektur</span><time datetime="2025-11-21">21.11.2025–22.02.2026</time></li>
      <li id="a-zimmer"><span class="liste__nr">03</span><span class="liste__titel">Stille Zimmer</span><span class="sparte sparte--kunst">Kunst</span><time datetime="2025-06-05">05.06.–14.09.2025</time></li>
      <li id="a-gedreht"><span class="liste__nr">04</span><span class="liste__titel">Gedreht und gebrannt. Keramik aus dem Sauerland</span><span class="sparte sparte--design">Design</span><time datetime="2025-02-08">08.02.–18.05.2025</time></li>
      <li id="a-mittelgebirge"><span class="liste__nr">05</span><span class="liste__titel">Mittelgebirge</span><span class="sparte sparte--foto">Fotografie & Raum</span><time datetime="2024-09-12">12.09.–08.12.2024</time></li>
      <li id="a-faden"><span class="liste__nr">06</span><span class="liste__titel">Faden und Falte</span><span class="sparte sparte--design">Design</span><time datetime="2024-05-03">03.05.–25.08.2024</time></li>
      <li id="a-fruechte"><span class="liste__nr">07</span><span class="liste__titel">Die Ordnung der Früchte. Stillleben heute</span><span class="sparte sparte--kunst">Kunst</span><time datetime="2023-11-10">10.11.2023–18.02.2024</time></li>
      <li id="a-huelle"><span class="liste__nr">08</span><span class="liste__titel">Hülle. Glas zwischen Labor und Flakon</span><span class="sparte sparte--design">Design</span><time datetime="2023-06-15">15.06.–24.09.2023</time></li>
    </ol>
  </section>
  <footer class="fuss">
    <p><b>Gießhaus · Kunsthalle Iserlohn</b><br>Am Gießhaus 4, 58636 Iserlohn</p>
    <p>Di–So 11–18 Uhr, Do bis 21 Uhr<br>Eintritt 8 €, ermäßigt 5 €, unter 18 frei</p>
  </footer>
</main>

<div class="pult" role="group" aria-label="Variante der Demo">
  <span>Variante</span>
  <button type="button" data-variante="flug" aria-pressed="true">Flug</button>
  <button type="button" data-variante="achse" aria-pressed="false">Achse</button>
  <span class="pult__werte" data-werte>850 px je Bild · perspective 1200 px · Hallenraster</span>
</div>

<script>
(() => {
  const html = document.documentElement, body = document.body;
  const reduziert = () => html.classList.contains('reduziert');
  const strecke = document.querySelector('.strecke');
  const buehne = document.querySelector('.buehne');
  const karten = [...document.querySelectorAll('.karte')];
  const N = karten.length;
  const kannTimeline = CSS.supports('animation-timeline: view()');
  const $ = s => document.querySelector(s);
  const leiste = $('[data-leiste]'), lTitel = $('[data-l-titel]'), lSparte = $('[data-l-sparte]'), lDatum = $('[data-l-datum]'), lZaehler = $('[data-l-zaehler]'), achsentitel = $('[data-achsentitel]');
  const ticks = $('[data-ticks]');
  for (let i = 0; i < N; i++) ticks.appendChild(document.createElement('i'));

  /* ---------- Ansicht Raum / Raster ---------- */
  let ansicht = reduziert() ? 'raster' : 'raum';
  function setzeAnsicht(neu, halten) {
    ansicht = neu;
    body.dataset.ansicht = neu;
    document.querySelectorAll('[data-ansicht] button, .ansicht button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.ansicht === neu)));
    if (!halten) { const y = strecke.getBoundingClientRect().top + scrollY - kopfHoehe(); if (scrollY > y) scrollTo({ top: y, behavior: 'auto' }); }
    aktualisieren(true);
  }
  document.querySelectorAll('.ansicht button').forEach(b => b.addEventListener('click', () => setzeAnsicht(b.dataset.ansicht)));

  /* ---------- Varianten flug / achse (?v=) ---------- */
  const WERTE = { flug: '850 px je Bild · perspective 1200 px · Hallenraster', achse: 'Hochformat · Fluchtpunkt über dem Bild' };
  function setzeVariante(v) {
    if (!WERTE[v]) v = 'flug';
    body.dataset.v = v;
    karten.forEach(k => { const img = k.querySelector('img'); const s = v === 'achse' ? img.dataset.hoch : img.dataset.quer; if (img.getAttribute('src') !== s) { img.src = s; img.width = v === 'achse' ? 800 : 1200; img.height = v === 'achse' ? 1200 : 800; } });
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
    $('[data-werte]').textContent = WERTE[v];
    const q = new URLSearchParams(location.search); q.set('v', v);
    history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + location.hash);
    aktualisieren(true);
  }
  document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.variante)));

  /* ---------- Fortschritt: p = 0 … N−1 ---------- */
  const kopfHoehe = () => document.querySelector('.kopf').offsetHeight;
  function fortschritt() {
    const r = strecke.getBoundingClientRect();
    const weg = r.height - (innerHeight - kopfHoehe());
    return Math.min(1, Math.max(0, (kopfHoehe() - r.top) / Math.max(1, weg))) * (N - 1);
  }
  function scrollFuer(i) {   // Scrollposition, an der Bild i vorn steht
    const top = strecke.getBoundingClientRect().top + scrollY - kopfHoehe();
    const weg = strecke.offsetHeight - (innerHeight - kopfHoehe());
    return top + weg * (i / (N - 1));
  }

  let vorn = -1;
  function aktualisieren(erzwingen) {
    if (ansicht === 'raster') { karten.forEach(k => k.removeAttribute('data-vorbei')); return; }
    const p = fortschritt();
    if (!kannTimeline) buehne.style.setProperty('--p', p.toFixed(4));
    karten.forEach((k, i) => { const d = p - i; if (d > .3) k.setAttribute('data-vorbei', ''); else k.removeAttribute('data-vorbei'); });
    const neu = Math.min(N - 1, Math.max(0, Math.round(p)));
    if (neu !== vorn || erzwingen) zeige(neu, erzwingen);
  }
  let wechselUhr;
  function zeige(i, sofort) {
    vorn = i;
    const k = karten[i];
    const setzen = () => {
      lTitel.textContent = k.dataset.titel; lSparte.textContent = k.dataset.sparte;
      lSparte.className = 'sparte sparte--' + k.dataset.sk; lDatum.textContent = k.dataset.datum;
      lZaehler.textContent = String(i + 1).padStart(2, '0') + ' / ' + String(N).padStart(2, '0');
      achsentitel.textContent = k.dataset.titel.split('. ')[0];
      [...ticks.children].forEach((t, j) => t.classList.toggle('an', j <= i));
      leiste.classList.remove('wechsel'); achsentitel.classList.remove('wechsel');
    };
    clearTimeout(wechselUhr);
    if (sofort || reduziert()) { setzen(); return; }
    leiste.classList.add('wechsel'); achsentitel.classList.add('wechsel');
    wechselUhr = setTimeout(setzen, 150);
  }

  let rafAn = false;
  addEventListener('scroll', () => { if (!rafAn) { rafAn = true; requestAnimationFrame(() => { rafAn = false; aktualisieren(); }); } }, { passive: true });
  addEventListener('resize', () => aktualisieren(true));

  /* ---------- Tastatur und Klick: ein Bild nach vorn holen ---------- */
  function holeNachVorn(i) { scrollTo({ top: scrollFuer(i), behavior: reduziert() || html.classList.contains('vorschau') ? 'auto' : 'smooth' }); }
  karten.forEach((k, i) => {
    const a = k.querySelector('a');
    a.addEventListener('focus', () => { if (ansicht === 'raum') scrollTo({ top: scrollFuer(i), behavior: 'auto' }); });
    a.addEventListener('click', e => {
      if (ansicht !== 'raum') return;
      if (Math.abs(fortschritt() - i) > .3) { e.preventDefault(); holeNachVorn(i); }   // erst nach vorn holen, dann öffnen
    });
    a.addEventListener('keydown', e => {
      const weiterTaste = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[e.key];
      if (!weiterTaste) return;
      const z = karten[i + weiterTaste]; if (!z) return;
      e.preventDefault(); z.querySelector('a').focus();
    });
  });

  /* ---------- Start ---------- */
  setzeVariante(new URLSearchParams(location.search).get('v'));
  setzeAnsicht(ansicht, true);

  /* ---------- Vorführung: Seite fährt selbst durch die Strecke ---------- */
  let uhr = null, t0 = 0;
  const RUNDE = 15000;
  function vorfuehren() {
    pause();
    if (ansicht !== 'raum' && !reduziert()) setzeAnsicht('raum', true);
    if (ansicht !== 'raum') return;
    t0 = performance.now();
    const schritt = t => {
      const q = ((t - t0) % RUNDE) / RUNDE;
      const s = Math.min(1, Math.max(0, (q - .06) / .86));
      const ziel = scrollFuer(0) + (scrollFuer(N - 1) - scrollFuer(0)) * (.5 - .5 * Math.cos(s * Math.PI));
      scrollTo({ top: ziel, behavior: 'auto' });
      uhr = requestAnimationFrame(schritt);
    };
    uhr = requestAnimationFrame(schritt);
  }
  function pause() { cancelAnimationFrame(uhr); uhr = null; }
  function neustart() { pause(); scrollTo({ top: 0, behavior: 'auto' }); aktualisieren(true); }

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