Ein-/Ausblenden mit @starting-style
Menü, Hovercard und Dialog kommen per @starting-style an und gehen dank allow-discrete sichtbar wieder, ganz ohne Animations-Skript.
Elemente, die per display, Popover oder Dialog erscheinen, bekommen ihren Startzustand in @starting-style, zum Beispiel opacity 0 und ein kleiner Versatz. Der Browser animiert dann vom Startzustand in den normalen Stil. Mit transition-behavior: allow-discrete laufen auch display und overlay mit. So bleibt das Element beim Schließen sichtbar, bis die Ausblendung fertig ist. Dropdowns, Tooltips, Dialoge und Toasts animieren so ohne JavaScript.
Ein Popover, das ankommt statt aufzuploppen, zeigt, woher es kommt und wohin es gehört. Das Auge folgt der Bewegung, der Zustandswechsel wird verständlich. Weil alles in CSS liegt, ist es leicht, schnell und überall gleich.
Wann einsetzen
Popover, Dialoge, Dropdowns, Tooltips, Hovercards, Toasts, Inline-Meldungen und Elemente mit [hidden]. Auch für Einstiegsanimationen beim ersten Rendern einer Seite.
Wann nicht
Bei Elementen, die per JavaScript sofort gemessen werden müssen: Während der Startphase haben sie noch Versatz, Scale oder Clip. Nicht für große Seitenübergänge, dafür gibt es View Transitions.
Varianten
- Popover oder Dialog mit display und overlay allow-discrete (PostHog: 240 ms, cubic-bezier(.4, 0, .2, 1))
- Hovercard: translateY(-10px) scale(.95) auf normal in 0,4 s (Chrome-Leitfaden)
- Dialog mit clip-path-Reveal inset(0 0 100%) in 0,4 s, ::backdrop ebenfalls per @starting-style (Cuberto)
- Einstieg beim ersten Rendern ohne Auslöser: translateY(8rem) und opacity 0, gestaffelt um 70 bis 100 ms (Collins)
Umsetzung
[popover] { opacity: 0; translate: 0 -8px; transition: opacity .24s var(--kurve), translate .24s var(--kurve), display .24s allow-discrete, overlay .24s allow-discrete; } [popover]:popover-open { opacity: 1; translate: 0 0; } @starting-style { [popover]:popover-open { opacity: 0; translate: 0 -8px; } }. Der geschlossene Grundzustand ist zugleich das Ziel der Ausblendung. @starting-style muss nach der Open-Regel stehen, sonst gewinnt die Open-Regel bei gleicher Spezifität. Beim Dialog: Lage fest setzen (position: fixed; inset: 0; margin: auto), denn :modal fällt beim Schließen sofort weg und der Dialog würde sonst während der Ausblendung springen. Ein globales margin: 0 löscht das margin: auto des Browsers. Für das ::backdrop gelten dieselben Regeln.
Robust umsetzen
Telefon. Identisch. Hover-Auslöser wie Hovercards auf Touch per Tippen öffnen.
Ohne Bewegung. Nur die Deckkraft kurz blenden (etwa 150 ms) oder sofort schalten. Regeln pro Komponente in @media (prefers-reduced-motion: reduce) verschachteln, kein globaler 0.01ms-Trick.
Barrierefrei. Popover und Dialog bringen Fokusverwaltung, Escape und Top-Layer mit. Die Animation darf den Fokus nicht verzögern: Er springt sofort, nur die Optik läuft nach. Hovercards zusätzlich per Fokus öffnen und mit Escape schließen.
Code
<!DOCTYPE html>
<!--
Ein-/Ausblenden mit @starting-style
Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/einblenden-starting-style/
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>Ein-/Ausblenden mit @starting-style · 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: 'Bricolage Grotesque'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bricolage-grotesque:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Onest'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/onest:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Shantell Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/shantell-sans:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
:root {
/* Rezept kurs-coaching-lernweg: kühler heller Grund, fast schwarze Tinte, ein Markenton, ein Erfolgston */
--grund: #eef1f8; --karte: #ffffff; --tinte: #10142b; --leise: rgba(16, 20, 43, .6); --linie: rgba(16, 20, 43, .13);
--marke: #3a4bd6; --marke-tief: #2a37a8; --erfolg: #12a66a; --hinweis: #f6c33b;
/* gemessen: Material Standard (PostHog), eine Kurve für alles */
--kurve: cubic-bezier(0.4, 0, 0.2, 1);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Onest', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; padding: clamp(20px, 3vw, 40px) clamp(20px, 4vw, 56px); display: grid; align-content: center; background: var(--grund); }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.notiz { font: 500 15px/1.3 'Shantell Sans', 'Onest', sans-serif; font-variation-settings: 'INFM' 40, 'BNCE' 0; color: var(--marke); }
button { font: inherit; color: inherit; }
/* ── Seite ─────────────────────────────────────────────── */
.buehne { width: 100%; max-width: 1180px; margin: 0 auto; }
.kopfzeile { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 16px 28px; padding-bottom: 18px; border-bottom: 1px solid var(--linie); margin-bottom: clamp(28px, 3.6vw, 48px); }
.marke { display: flex; align-items: center; gap: 10px; white-space: nowrap; font: 500 17px/1 'Bricolage Grotesque', 'Onest', sans-serif; font-variation-settings: 'opsz' 24; letter-spacing: -.02em; }
.marke b { font-weight: 800; }
.marke svg { width: 26px; height: 26px; flex: none; }
.kopf-rechts { display: flex; align-items: center; gap: clamp(16px, 3vw, 40px); }
.kopfzeile nav { display: flex; gap: clamp(14px, 2.4vw, 28px); }
.kopfzeile nav a { color: inherit; text-decoration: none; }
.kopfzeile nav a[aria-current] { color: var(--tinte); }
.stand { display: flex; align-items: center; gap: 10px; white-space: nowrap; font-size: 13px; font-variant-numeric: tabular-nums; color: var(--leise); }
.stand b { color: var(--tinte); font-weight: 600; }
.stand .balken { width: 84px; height: 6px; border-radius: 3px; background: rgba(16, 20, 43, .1); overflow: hidden; }
.stand .balken i { display: block; width: 37.5%; height: 100%; border-radius: inherit; background: var(--erfolg); }
.fall { display: grid; grid-template-columns: minmax(0, 9fr) minmax(0, 11fr); gap: clamp(28px, 6vw, 96px); align-items: center; }
.bild { position: relative; }
/* Übungsbild: eigene SVG-Szene, offene Blende = Hintergrund weich, Blüte scharf */
.sucher { position: relative; aspect-ratio: 4 / 4.4; border-radius: 14px; overflow: hidden; background: #c9d8f0; box-shadow: 0 1px 0 rgba(255, 255, 255, .6) inset, 0 30px 60px -36px rgba(16, 20, 43, .45); }
.sucher > svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.drittel { position: absolute; inset: 0; pointer-events: none;
background:
linear-gradient(to right, transparent calc(33.333% - .5px), rgba(255, 255, 255, .5) calc(33.333% - .5px), rgba(255, 255, 255, .5) calc(33.333% + .5px), transparent calc(33.333% + .5px), transparent calc(66.666% - .5px), rgba(255, 255, 255, .5) calc(66.666% - .5px), rgba(255, 255, 255, .5) calc(66.666% + .5px), transparent calc(66.666% + .5px)),
linear-gradient(to bottom, transparent calc(33.333% - .5px), rgba(255, 255, 255, .5) calc(33.333% - .5px), rgba(255, 255, 255, .5) calc(33.333% + .5px), transparent calc(33.333% + .5px), transparent calc(66.666% - .5px), rgba(255, 255, 255, .5) calc(66.666% - .5px), rgba(255, 255, 255, .5) calc(66.666% + .5px), transparent calc(66.666% + .5px)); }
.etikett { position: absolute; left: 14px; top: 14px; padding: 7px 11px; border-radius: 999px; background: rgba(255, 255, 255, .86); backdrop-filter: blur(6px); font-size: 13px; font-weight: 500; }
.sticker { position: absolute; right: 16px; top: 16px; width: 96px; height: 96px; border-radius: 50%; background: var(--hinweis); color: var(--tinte); display: grid; place-content: center; text-align: center; rotate: -9deg; font: 700 14px/1.12 'Bricolage Grotesque', 'Onest', sans-serif; font-variation-settings: 'opsz' 18; letter-spacing: -.01em; box-shadow: 0 10px 22px -12px rgba(16, 20, 43, .5); }
.sticker small { display: block; font-size: 11px; font-weight: 500; margin-top: 3px; }
.leiste { position: absolute; left: 0; right: 0; bottom: 0; display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 12px 16px; background: linear-gradient(to top, rgba(10, 13, 32, .72), rgba(10, 13, 32, 0)); color: #fff; }
.leiste .mono { color: rgba(255, 255, 255, .92); letter-spacing: .06em; text-transform: none; }
.skala { display: flex; align-items: center; gap: 5px; }
.skala i { width: 2px; height: 6px; background: rgba(255, 255, 255, .55); border-radius: 1px; }
.skala i.null { height: 12px; background: #fff; }
.bild figcaption { display: flex; justify-content: space-between; margin-top: 12px; }
.text { display: grid; gap: clamp(18px, 2.2vw, 26px); justify-items: start; }
.text h1 { font: 700 clamp(2.25rem, 1.4rem + 3.4vw, 4.25rem)/1.05 'Bricolage Grotesque', 'Onest', sans-serif; font-variation-settings: 'opsz' 96; letter-spacing: -.02em; text-wrap: balance; }
.text h1 span { display: block; }
.text h1 em { font-style: normal; color: var(--marke); background: linear-gradient(transparent 58%, rgba(246, 195, 59, .5) 58%, rgba(246, 195, 59, .5) 86%, transparent 86%); padding: 0 .06em; margin: 0 -.06em; }
.text .lauf { font-size: clamp(16px, 1.3vw, 18px); line-height: 1.6; max-width: 46ch; text-wrap: pretty; }
.mitwirkende { display: flex; flex-wrap: wrap; gap: 6px 22px; font-size: 15px; color: var(--leise); }
.mitwirkende b { font-weight: 600; color: var(--tinte); font-variant-numeric: tabular-nums; }
.person { background: none; border: 0; padding: 0; cursor: pointer; font-weight: 600; color: var(--tinte); text-decoration: underline; text-decoration-color: var(--linie); text-decoration-thickness: 1.5px; text-underline-offset: .25em; transition: text-decoration-color .15s var(--kurve); }
.person:hover, .person:focus-visible, .person[aria-expanded="true"] { text-decoration-color: var(--marke); outline: none; }
.aktionen { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 6px; }
.knopf { display: inline-flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 22px; border-radius: 12px; border: 1.5px solid var(--tinte); background: none; cursor: pointer; font-size: 15px; font-weight: 600; transition: background-color .15s var(--kurve), color .15s var(--kurve), border-color .15s var(--kurve); }
.knopf.voll { background: var(--marke); border-color: var(--marke); color: #fff; }
@media (hover: hover) {
.knopf:hover { background: var(--tinte); color: var(--karte); }
.knopf.voll:hover { background: var(--marke-tief); border-color: var(--marke-tief); }
}
.knopf:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; }
.knopf svg { width: 10px; height: 10px; transition: rotate .24s var(--kurve); }
.knopf[aria-expanded="true"] svg { rotate: 180deg; }
/* ── Variante Erstauftritt (Collins): ohne Auslöser, beim ersten Rendern ─────
Gilt auch, wenn ein Eltern-Element von display: none wieder sichtbar wird. */
.auftritt { transition: opacity .8s var(--kurve), translate .8s var(--kurve); transition-delay: calc(var(--i) * 70ms); }
@starting-style { .auftritt { opacity: 0; translate: 0 8rem; } }
.text.weg { display: none; }
/* ── Variante Menü (PostHog-Werte): Popover mit display + overlay ─────────── */
.menue[popover] {
position: fixed; inset: auto; margin: 0; width: 340px; padding: 18px 20px 14px;
background: var(--karte); color: var(--tinte); border: 1px solid var(--linie); border-radius: 14px;
box-shadow: 0 18px 40px -18px rgba(16, 20, 43, .35);
transform-origin: top left;
/* geschlossen = Endzustand des Ausblendens */
opacity: 0; translate: 0 var(--weg, -8px);
transition: opacity .24s var(--kurve), translate .24s var(--kurve), display .24s allow-discrete, overlay .24s allow-discrete;
}
.menue[popover]:popover-open { opacity: 1; translate: 0 0; }
/* Startzustand NACH der Open-Regel, sonst gewinnt die Open-Regel */
@starting-style { .menue[popover]:popover-open { opacity: 0; translate: 0 var(--weg, -8px); } }
/* Weg zeigt immer aus dem Anker heraus: öffnet das Menü nach oben, kommt es von unten */
.menue[data-seite="oben"] { --weg: 8px; transform-origin: bottom left; }
.menue .kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.menue .kopf span:last-child { font-size: 13px; color: var(--leise); font-variant-numeric: tabular-nums; }
.menue ol { list-style: none; padding: 0; margin-top: 10px; }
.menue li { display: grid; grid-template-columns: 22px 1fr auto; gap: 12px; align-items: center; padding: 9px 0; border-top: 1px solid var(--linie); font-size: 14px; line-height: 1.35; }
.menue li:first-child { border-top: 0; }
.menue li .zeit { font-size: 13px; color: var(--leise); font-variant-numeric: tabular-nums; }
.menue .haken { width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; background: var(--erfolg); color: #fff; }
.menue .haken svg { width: 11px; height: 11px; }
.menue .jetzt .haken { background: none; box-shadow: inset 0 0 0 2px var(--marke); }
.menue .jetzt .haken::after { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--marke); }
.menue .jetzt { font-weight: 600; }
.menue .offen .haken { background: none; box-shadow: inset 0 0 0 1.5px var(--linie); }
.menue .offen { color: var(--leise); }
/* ── Variante Hovercard (Chrome-Leitfaden): translateY(−10px) scale(.95), 400 ms ── */
.karte[popover] {
position: fixed; inset: auto; margin: 0; width: 290px; padding: 18px;
background: var(--tinte); color: #fff; border: 0; border-radius: 16px;
box-shadow: 0 24px 48px -20px rgba(16, 20, 43, .6);
transform-origin: top left;
opacity: 0; translate: 0 var(--weg, -10px); scale: .95;
transition: opacity .4s var(--kurve), translate .4s var(--kurve), scale .4s var(--kurve), display .4s allow-discrete, overlay .4s allow-discrete;
}
.karte[popover]:popover-open { opacity: 1; translate: 0 0; scale: 1; }
@starting-style { .karte[popover]:popover-open { opacity: 0; translate: 0 var(--weg, -10px); scale: .95; } }
.karte[data-seite="oben"] { --weg: 10px; transform-origin: bottom left; }
.karte .kopf { display: flex; gap: 12px; align-items: center; }
.karte .kuerzel { flex: none; width: 46px; height: 46px; border-radius: 50%; overflow: hidden; background: var(--hinweis); }
.karte .kuerzel svg { display: block; width: 100%; height: 100%; }
.karte strong { display: block; font: 700 17px/1.2 'Bricolage Grotesque', 'Onest', sans-serif; font-variation-settings: 'opsz' 24; }
.karte .mono { color: rgba(255, 255, 255, .6); }
.karte p:not(.mono) { font-size: 14px; line-height: 1.5; margin-top: 14px; color: rgba(255, 255, 255, .86); }
.karte .fuss { display: flex; justify-content: space-between; margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, .16); }
/* ── Variante Dialog (Cuberto): clip-path-Reveal + Backdrop ──────────────── */
.anfrage {
/* Lage fest setzen: beim Schließen fällt :modal sofort weg, sonst springt der Dialog während des Ausblendens */
position: fixed; inset: 0; margin: auto; height: fit-content;
width: min(560px, calc(100vw - 32px)); max-width: none; max-height: calc(100svh - 32px);
padding: clamp(24px, 4vw, 40px); border: 0; border-radius: 16px; background: var(--karte); color: var(--tinte);
/* geschlossen: wischt nach unten weg */
opacity: 0; clip-path: inset(100% 0 0 0);
transition: opacity .4s var(--kurve), clip-path .4s var(--kurve), display .4s allow-discrete, overlay .4s allow-discrete;
}
.anfrage[open] { opacity: 1; clip-path: inset(0 0 0 0); }
/* Start: wächst von oben nach unten auf (gemessen bei Cuberto) */
@starting-style { .anfrage[open] { opacity: 0; clip-path: inset(0 0 100% 0); } }
.anfrage::backdrop {
background-color: rgb(16 20 43 / 0);
transition: background-color .4s var(--kurve), display .4s allow-discrete, overlay .4s allow-discrete;
}
.anfrage[open]::backdrop { background-color: rgb(16 20 43 / .5); }
@starting-style { .anfrage[open]::backdrop { background-color: rgb(16 20 43 / 0); } }
.anfrage h2 { font: 700 clamp(28px, 3.4vw, 38px)/1.06 'Bricolage Grotesque', 'Onest', sans-serif; font-variation-settings: 'opsz' 60; letter-spacing: -.02em; margin: 10px 0 24px; padding-right: 44px; }
.anfrage h2 em { font-style: normal; color: var(--marke); display: block; }
.anfrage form { display: grid; gap: 16px; }
.anfrage label { display: grid; gap: 8px; }
.anfrage input, .anfrage textarea { font: 400 16px/1.4 'Onest', system-ui, sans-serif; color: var(--tinte); background: var(--grund); border: 1.5px solid transparent; padding: 11px 14px; border-radius: 10px; resize: none; transition: border-color .15s var(--kurve), background-color .15s var(--kurve); }
.anfrage input:focus, .anfrage textarea:focus { outline: none; border-color: var(--marke); background: #fff; }
.anfrage .fuss { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 18px; margin-top: 8px; }
.anfrage .fuss .notiz { font-size: 14px; color: var(--leise); }
.zu { position: absolute; top: 18px; right: 18px; width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--linie); background: none; cursor: pointer; display: grid; place-items: center; transition: background-color .15s var(--kurve); }
.zu:hover { background: rgba(16, 20, 43, .06); }
.zu:focus-visible { outline: 2px solid var(--marke); outline-offset: 2px; }
.zu svg { width: 12px; height: 12px; }
/* ── Bedienung ─────────────────────────────────────────── */
.steuerung { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 22px; margin-top: clamp(28px, 3vw, 40px); padding-top: 16px; border-top: 1px solid var(--linie); }
.schalter { display: flex; flex-wrap: wrap; gap: 4px; }
.schalter button { font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); background: none; border: 1px solid transparent; border-radius: 999px; padding: 8px 12px; cursor: pointer; transition: color .15s var(--kurve), border-color .15s var(--kurve), background-color .15s var(--kurve); }
.schalter button[aria-pressed="true"] { color: var(--tinte); border-color: var(--linie); background: var(--karte); }
.schalter button:hover { color: var(--tinte); }
.schalter button:focus-visible { outline: 2px solid var(--marke); outline-offset: 2px; }
.werte { display: flex; flex-wrap: wrap; gap: 4px 18px; margin-top: 12px; text-transform: none; letter-spacing: .02em; }
.werte b { font-weight: 500; color: var(--tinte); }
/* Vorführung: gezeichneter Zeiger im Top-Layer, damit er über Dialog und Popover liegt */
.zeiger[popover] { position: fixed; inset: auto; left: 0; top: 0; margin: 0; padding: 0; border: 0; background: none; overflow: visible; pointer-events: none; width: 20px; height: 20px; transition: transform .7s var(--kurve); }
.zeiger svg { display: block; width: 20px; height: 20px; }
.zeiger i { position: absolute; left: -15px; top: -15px; width: 34px; height: 34px; border-radius: 50%; border: 1.5px solid var(--marke); opacity: 0; }
/* Reduzierte Bewegung: nur Deckkraft, 150 ms, kein Weg, keine Maske */
.reduziert .auftritt { translate: none; transition: opacity .15s linear; transition-delay: 0s; }
@starting-style { .reduziert .auftritt { translate: none; } }
.reduziert .menue[popover], .reduziert .karte[popover], .reduziert .menue[popover]:popover-open, .reduziert .karte[popover]:popover-open { translate: none; scale: none; transition: opacity .15s linear, display .15s allow-discrete, overlay .15s allow-discrete; }
@starting-style { .reduziert .menue[popover]:popover-open, .reduziert .karte[popover]:popover-open { translate: none; scale: none; } }
.reduziert .anfrage, .reduziert .anfrage[open] { clip-path: none; transition: opacity .15s linear, display .15s allow-discrete, overlay .15s allow-discrete; }
@starting-style { .reduziert .anfrage[open] { clip-path: none; } }
.reduziert .anfrage::backdrop { transition: background-color .15s linear, display .15s allow-discrete, overlay .15s allow-discrete; }
.reduziert .zeiger[popover] { transition: none; }
.reduziert .knopf svg { transition: none; }
/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; padding: 36px 52px; }
.vorschau .steuerung, .vorschau .werte, .vorschau .kopfzeile nav { display: none; }
.vorschau .kopfzeile { margin-bottom: 36px; }
.vorschau .fall { gap: 56px; }
.vorschau .text h1 { font-size: 58px; }
.vorschau .sucher { aspect-ratio: 4 / 4.8; }
/* Tablet / Telefon */
@media (max-width: 820px) {
.fall { grid-template-columns: minmax(0, 1fr); gap: 28px; }
.sucher { aspect-ratio: 3 / 2; }
.bild figcaption { display: none; }
}
@media (max-width: 560px) {
.kopfzeile nav { display: none; }
.stand .balken { width: 56px; }
.sticker { width: 74px; height: 74px; font-size: 12px; right: 12px; top: 12px; }
.sticker small { font-size: 10px; }
.etikett { left: 10px; top: 10px; font-size: 12px; }
.leiste { padding: 10px 12px; }
.aktionen { width: 100%; }
.aktionen .knopf { flex: 1 1 auto; justify-content: center; }
.menue[popover], .karte[popover] { width: min(320px, calc(100vw - 32px)); }
}
@media (max-width: 440px) { .stand > span:first-child { display: none; } }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
@media (hover: none) {
.person { padding: 10px 0; margin: -10px 0; } /* größere Trefferfläche, Tippen statt Hover */
}
</style>
</head>
<body>
<div class="buehne">
<header class="kopfzeile">
<p class="marke"><svg viewBox="0 0 26 26" aria-hidden="true"><circle cx="13" cy="13" r="12" fill="#10142b"/><g fill="none" stroke="#ffffff" stroke-width="1.4" stroke-linejoin="round"><path d="M13 5.5 19.5 9.2v7.6L13 20.5l-6.5-3.7V9.2Z"/><path d="M13 5.5 9.6 14.2M19.5 9.2 10.4 10.5M19.5 16.8l-5.9-6.9M13 20.5l3.4-8.7M6.5 16.8l9.1-1.3M6.5 9.2l5.9 6.9"/></g></svg><span>Lernpfad <b>Fotografie</b></span></p>
<div class="kopf-rechts">
<nav class="mono" aria-label="Kursnavigation"><a href="#kurs">Kursübersicht</a><a href="#lektionen" aria-current="page">Lektionen</a><a href="#fragen">Fragen</a></nav>
<p class="stand"><span>Lektion</span> <b>9 / 24</b><span class="balken" role="progressbar" aria-label="Kursfortschritt" aria-valuemin="0" aria-valuemax="24" aria-valuenow="9"><i></i></span></p>
</div>
</header>
<article class="fall">
<figure class="bild">
<div class="sucher">
<svg viewBox="0 0 400 440" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Übungsbild: eine Blüte im Vordergrund ist scharf, Hügel, Sonne und Lichtpunkte im Hintergrund sind weich verschwommen">
<defs>
<linearGradient id="himmel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#a9c2ec"/><stop offset=".55" stop-color="#d7e2f6"/><stop offset="1" stop-color="#f3dccb"/></linearGradient>
<radialGradient id="sonne"><stop offset="0" stop-color="#fff4d6"/><stop offset=".45" stop-color="#ffd98a"/><stop offset="1" stop-color="#ffd98a" stop-opacity="0"/></radialGradient>
<filter id="weit" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="9"/></filter>
<filter id="mitte" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="4.5"/></filter>
<radialGradient id="kern"><stop offset="0" stop-color="#ffd24d"/><stop offset="1" stop-color="#e79b1c"/></radialGradient>
</defs>
<rect width="400" height="440" fill="url(#himmel)"/>
<g filter="url(#weit)">
<circle cx="300" cy="118" r="92" fill="url(#sonne)"/>
<path d="M-20 250 C 60 200 120 214 190 232 S 330 196 420 222 V 440 H -20Z" fill="#8aa6d8"/>
<path d="M-20 286 C 70 250 160 262 240 276 S 360 254 420 262 V 440 H -20Z" fill="#6f93c4"/>
<g fill="#4f78ad"><ellipse cx="54" cy="262" rx="22" ry="40"/><ellipse cx="88" cy="270" rx="16" ry="30"/><ellipse cx="342" cy="252" rx="24" ry="44"/><ellipse cx="372" cy="264" rx="15" ry="28"/></g>
</g>
<g filter="url(#mitte)">
<path d="M-20 330 C 80 300 180 312 260 322 S 380 306 420 312 V 440 H -20Z" fill="#7fb488"/>
<g fill="#ffffff"><circle cx="262" cy="196" r="20" opacity=".42"/><circle cx="318" cy="240" r="13" opacity=".5"/><circle cx="232" cy="262" r="9" opacity=".55"/><circle cx="356" cy="186" r="16" opacity=".35"/><circle cx="292" cy="290" r="24" opacity=".3"/></g>
<g fill="#ffe2a8"><circle cx="208" cy="214" r="11" opacity=".55"/><circle cx="374" cy="300" r="10" opacity=".5"/></g>
</g>
<!-- Vordergrund scharf -->
<path d="M-10 440 C 40 392 80 376 120 382 S 220 400 260 380 S 360 360 410 372 V 440Z" fill="#3f8a5b"/>
<g fill="none" stroke="#2c6b45" stroke-width="2.4" stroke-linecap="round">
<path d="M24 440 C 26 410 22 392 14 376"/><path d="M40 440 C 44 404 52 388 62 374"/><path d="M300 440 C 302 410 296 392 288 380"/><path d="M318 440 C 324 412 334 398 344 388"/><path d="M372 440 C 370 418 376 402 386 392"/>
</g>
<path d="M168 440 C 168 360 150 280 140 186" fill="none" stroke="#2c6b45" stroke-width="3.2" stroke-linecap="round"/>
<path d="M158 330 C 130 312 116 290 112 270 C 136 280 152 300 158 330Z" fill="#3f8a5b"/>
<path d="M162 368 C 190 352 204 330 208 308 C 184 318 168 340 162 368Z" fill="#3f8a5b"/>
<g transform="translate(140 172)">
<g fill="#f27d8f" stroke="#d65a70" stroke-width="1">
<ellipse rx="11" ry="27" transform="rotate(0) translate(0 -24)"/>
<ellipse rx="11" ry="27" transform="rotate(45) translate(0 -24)"/>
<ellipse rx="11" ry="27" transform="rotate(90) translate(0 -24)"/>
<ellipse rx="11" ry="27" transform="rotate(135) translate(0 -24)"/>
<ellipse rx="11" ry="27" transform="rotate(180) translate(0 -24)"/>
<ellipse rx="11" ry="27" transform="rotate(225) translate(0 -24)"/>
<ellipse rx="11" ry="27" transform="rotate(270) translate(0 -24)"/>
<ellipse rx="11" ry="27" transform="rotate(315) translate(0 -24)"/>
</g>
<g fill="#fbb3be" opacity=".75"><ellipse rx="5" ry="14" transform="rotate(22) translate(0 -22)"/><ellipse rx="5" ry="14" transform="rotate(112) translate(0 -22)"/><ellipse rx="5" ry="14" transform="rotate(202) translate(0 -22)"/><ellipse rx="5" ry="14" transform="rotate(292) translate(0 -22)"/></g>
<circle r="12" fill="url(#kern)"/>
<g fill="#b06c10"><circle cx="-4" cy="-3" r="1.4"/><circle cx="3" cy="-5" r="1.4"/><circle cx="5" cy="2" r="1.4"/><circle cx="-2" cy="5" r="1.4"/><circle cx="0" cy="0" r="1.4"/></g>
</g>
<!-- Fokusfeld -->
<g fill="none" stroke="#2fe08f" stroke-width="3" stroke-linecap="round">
<path d="M92 138 v-16 h16"/><path d="M172 122 h16 v16"/><path d="M188 206 v16 h-16"/><path d="M108 222 h-16 v-16"/>
</g>
</svg>
<div class="drittel" aria-hidden="true"></div>
<span class="etikett" aria-hidden="true">Übungsbild 04</span>
<span class="sticker" aria-hidden="true">Live-Feedback<small>Do, 19 Uhr</small></span>
<div class="leiste" aria-hidden="true"><span class="mono">f/2,8 · 1/500 s · ISO 100</span><span class="skala"><i></i><i></i><i></i><i class="null"></i><i></i><i></i><i></i></span></div>
</div>
<figcaption class="mono"><span>Offene Blende, weicher Hintergrund</span><span>04 / 05</span></figcaption>
</figure>
<div class="text" data-text>
<p class="notiz auftritt" style="--i:0">Modul 2 · Lektion 9 · 18 min</p>
<h1><span class="auftritt" style="--i:1">Blende verstehen:</span><span class="auftritt" style="--i:2">Wie viel Schärfe</span><span class="auftritt" style="--i:3"><em>braucht dein Bild?</em></span></h1>
<p class="lauf auftritt" style="--i:4">Die Blende regelt, wie viel Licht durchs Objektiv fällt und wie weit die Schärfe reicht. Bei f/2,8 löst sich der Hintergrund in weiche Flächen auf, bei f/11 bleibt die ganze Wiese scharf. Heute probierst du beides an einem Motiv aus.</p>
<p class="mitwirkende auftritt" style="--i:5"><span>Mit <button type="button" class="person" data-person aria-expanded="false" aria-controls="karte">Lena Hartwig</button></span><span>Übung <b>1 Foto, 2 Blenden</b></span></p>
<div class="aktionen auftritt" style="--i:6">
<button type="button" class="knopf" data-menueknopf popovertarget="menue" aria-expanded="false">Lektionsinhalt <svg viewBox="0 0 10 10" aria-hidden="true"><path d="M1.5 3.5 5 7l3.5-3.5" fill="none" stroke="currentColor" stroke-width="1.4"/></svg></button>
<button type="button" class="knopf voll" data-dialogknopf>Übung einreichen</button>
</div>
</div>
</article>
<div class="steuerung">
<span class="mono">Variante</span>
<div class="schalter" role="group" aria-label="Variante abspielen">
<button type="button" data-variante="menue" aria-pressed="true">Menü</button>
<button type="button" data-variante="karte" aria-pressed="false">Hovercard</button>
<button type="button" data-variante="dialog" aria-pressed="false">Dialog</button>
<button type="button" data-variante="auftritt" aria-pressed="false">Erstauftritt</button>
</div>
</div>
<p class="mono werte" data-werte aria-live="polite"></p>
</div>
<div class="menue" id="menue" popover data-menue>
<p class="kopf"><span class="notiz">In dieser Lektion</span><span>3 von 5 erledigt</span></p>
<ol>
<li><span class="haken"><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 6.3 5 8.7l4.6-5.2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span>Was die Blende macht</span><span class="zeit">3 min</span></li>
<li><span class="haken"><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 6.3 5 8.7l4.6-5.2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span>f-Zahlen lesen, ohne zu rechnen</span><span class="zeit">4 min</span></li>
<li><span class="haken"><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2.5 6.3 5 8.7l4.6-5.2" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span>Schärfentiefe sehen lernen</span><span class="zeit">4 min</span></li>
<li class="jetzt" aria-current="step"><span class="haken"></span><span>Übung: eine Blüte, zwei Blenden</span><span class="zeit">5 min</span></li>
<li class="offen"><span class="haken"></span><span>Typische Fehler und Lösungen</span><span class="zeit">2 min</span></li>
</ol>
</div>
<div class="karte" id="karte" popover="manual" data-karte role="tooltip">
<div class="kopf"><span class="kuerzel" aria-hidden="true"><svg viewBox="0 0 46 46"><rect width="46" height="46" fill="#f6c33b"/><circle cx="23" cy="19" r="8" fill="#10142b"/><path d="M8 46c1.5-9.5 7.5-15 15-15s13.5 5.5 15 15Z" fill="#10142b"/><rect x="15.5" y="31.5" width="15" height="9" rx="2.2" fill="#3a4bd6"/><circle cx="23" cy="36" r="2.8" fill="none" stroke="#fff" stroke-width="1.4"/></svg></span><div><strong>Lena Hartwig</strong><span class="mono">Fotografin · Trainerin</span></div></div>
<p>Fotografiert seit 2009 Porträts und Reportagen. Erklärt Technik so, dass sie nach einer Übung sitzt, nicht nach drei Seiten Theorie.</p>
<p class="mono fuss"><span>24 Lektionen</span><span>seit 2016 im Kurs</span></p>
</div>
<dialog class="anfrage" data-dialog aria-labelledby="anfrage-titel">
<button type="button" class="zu" data-zu aria-label="Schließen"><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M1 1l10 10M11 1 1 11" stroke="currentColor" stroke-width="1.4"/></svg></button>
<p class="notiz">Übung zu Lektion 9</p>
<h2 id="anfrage-titel">Zeig uns dein Bild <em>mit offener Blende</em></h2>
<form method="dialog">
<label><span class="mono">Bildtitel</span><input name="titel" placeholder="Zum Beispiel: Mohn am Feldrand"></label>
<label><span class="mono">Blende · Zeit · ISO</span><input name="werte" inputmode="text" placeholder="f/2,8 · 1/500 s · ISO 100"></label>
<label><span class="mono">Was sollte scharf sein?</span><textarea name="text" rows="2" placeholder="Ein Satz reicht. Lena geht im Live-Feedback darauf ein."></textarea></label>
<div class="fuss"><button class="knopf voll">Zur Besprechung einreichen</button><span class="notiz">Besprechung jeden Donnerstag</span></div>
</form>
</dialog>
<div class="zeiger" popover="manual" data-zeiger aria-hidden="true"><svg viewBox="0 0 20 20"><path d="M2.5 1.8 17 10.2l-6.5 1.4-3.1 6.1Z" fill="#10142b" stroke="#ffffff" stroke-width="1.3" stroke-linejoin="round"/></svg><i></i></div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const text = $('[data-text]'), menue = $('[data-menue]'), menueKnopf = $('[data-menueknopf]');
const karte = $('[data-karte]'), person = $('[data-person]');
const dialog = $('[data-dialog]'), dialogKnopf = $('[data-dialogknopf]');
const zeiger = $('[data-zeiger]'), werteZeile = $('[data-werte]');
const knoepfe = [...document.querySelectorAll('[data-variante]')];
const kannPopover = typeof HTMLElement.prototype.showPopover === 'function';
const WERTE = {
menue: ['<b>@starting-style</b> opacity 0 · translate 0 ±8px (aus dem Anker)', '<b>240 ms</b> cubic-bezier(.4, 0, .2, 1)', 'display, overlay <b>allow-discrete</b>', 'Aus = Startwerte'],
karte: ['<b>@starting-style</b> opacity 0 · translate 0 ±10px · scale .95', '<b>400 ms</b> cubic-bezier(.4, 0, .2, 1)', 'öffnet nach <b>300 ms</b>, schließt nach <b>200 ms</b>'],
dialog: ['<b>@starting-style</b> clip-path inset(0 0 100%) · opacity 0', 'aus: inset(100% 0 0)', '<b>400 ms</b> cubic-bezier(.4, 0, .2, 1)', '::backdrop rgb(16 20 43 / .5)'],
auftritt: ['<b>@starting-style</b> opacity 0 · translate 0 8rem', '<b>800 ms</b> cubic-bezier(.4, 0, .2, 1)', 'Stagger <b>70 ms</b>', 'ohne Auslöser, beim ersten Rendern'],
};
const REDUZIERT = ['nur <b>opacity</b>', '<b>150 ms</b> linear', 'kein Weg, kein Scale, kein clip-path'];
function markiere(v) {
knoepfe.forEach(k => k.setAttribute('aria-pressed', String(k.dataset.variante === v)));
werteZeile.innerHTML = (reduziert() ? REDUZIERT : WERTE[v]).map(t => `<span>${t}</span>`).join('');
}
/* Platzieren: Popover liegen im Top-Layer (position: fixed), Lage per JS.
Mit CSS Anchor Positioning ginge das ohne Skript. */
function platziere(el, anker, oben) {
const r = anker.getBoundingClientRect(), w = el.offsetWidth, h = el.offsetHeight, rand = 16;
let x = Math.max(rand, Math.min(innerWidth - w - rand, r.left));
let y = oben && r.top - h - 12 > rand ? r.top - h - 12 : r.bottom + 12;
if (!oben && y + h > innerHeight - rand && r.top - h - 12 > rand) y = r.top - h - 12;
el.style.left = x + 'px'; el.style.top = y + 'px';
el.dataset.seite = y < r.top ? 'oben' : 'unten';
}
function zeigerNachOben() { if (html.classList.contains('vorfuehrung') && kannPopover) { try { zeiger.hidePopover(); zeiger.showPopover(); } catch (e) {} } }
// Menü: natives popovertarget, nur Lage und aria-expanded per Skript
menue.addEventListener('beforetoggle', e => {
if (e.newState === 'open') { menue.style.visibility = 'hidden'; menue.style.display = 'block'; platziere(menue, menueKnopf, false); menue.style.display = ''; menue.style.visibility = ''; }
menueKnopf.setAttribute('aria-expanded', String(e.newState === 'open'));
});
menue.addEventListener('toggle', e => { if (e.newState === 'open') zeigerNachOben(); });
// Hovercard: manuelles Popover, verzögert öffnen und schließen
let kartenUhr = null;
function karteAuf() {
if (!kannPopover || karte.matches(':popover-open')) return;
karte.style.visibility = 'hidden'; karte.style.display = 'block'; platziere(karte, person, true); karte.style.display = ''; karte.style.visibility = '';
karte.showPopover(); person.setAttribute('aria-expanded', 'true'); zeigerNachOben();
}
function karteZu() { if (kannPopover && karte.matches(':popover-open')) karte.hidePopover(); person.setAttribute('aria-expanded', 'false'); }
const spaeter = (f, ms) => { clearTimeout(kartenUhr); kartenUhr = setTimeout(f, ms); };
[person, karte].forEach(el => {
el.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') spaeter(karteAuf, 300); });
el.addEventListener('pointerleave', e => { if (e.pointerType === 'mouse') spaeter(karteZu, 200); });
});
person.addEventListener('focus', () => spaeter(karteAuf, 300));
person.addEventListener('blur', () => spaeter(karteZu, 200));
person.addEventListener('click', e => { clearTimeout(kartenUhr); karte.matches(':popover-open') ? karteZu() : karteAuf(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') karteZu(); });
document.addEventListener('pointerdown', e => { if (!e.target.closest('[data-karte], [data-person], [data-variante]')) karteZu(); });
// Dialog
function dialogAuf() { if (!dialog.open) { dialog.showModal(); zeigerNachOben(); } }
dialogKnopf.addEventListener('click', () => { markiere('dialog'); dialogAuf(); });
$('[data-zu]').addEventListener('click', () => dialog.close());
dialog.addEventListener('click', e => { if (e.target === dialog) { const r = dialog.getBoundingClientRect(); if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) dialog.close(); } });
menueKnopf.addEventListener('click', () => markiere('menue'));
person.addEventListener('pointerenter', () => markiere('karte'));
// Erstauftritt erneut: Textspalte kurz aus dem Rendering nehmen, dann greift @starting-style wieder
function auftritt() {
text.classList.add('weg'); getComputedStyle(text).display;
text.classList.remove('weg');
}
function allesZu() {
clearTimeout(kartenUhr); karteZu();
if (kannPopover && menue.matches(':popover-open')) menue.hidePopover();
if (dialog.open) dialog.close();
}
knoepfe.forEach(k => k.addEventListener('click', () => {
const v = k.dataset.variante;
stopp(); allesZu(); markiere(v);
if (v === 'menue' && kannPopover) setTimeout(() => menue.showPopover(), 260);
if (v === 'karte') setTimeout(karteAuf, 260);
if (v === 'dialog') setTimeout(dialogAuf, 260);
if (v === 'auftritt') auftritt();
}));
/* ── Vorführung: gedachter Zeiger spielt alle vier Varianten nacheinander ── */
let lauf = 0;
const warte = ms => new Promise(r => setTimeout(r, ms));
function zeigerZu(el, fx = .5, fy = .55) {
const r = el.getBoundingClientRect();
zeiger.style.transform = `translate(${r.left + r.width * fx - 3}px, ${r.top + r.height * fy - 2}px)`;
}
function tippen() {
const ring = zeiger.querySelector('i');
if (ring.animate) ring.animate([{ opacity: .9, scale: .4 }, { opacity: 0, scale: 1.3 }], { duration: 500, easing: 'cubic-bezier(0.4, 0, 0.2, 1)' });
}
async function runde(mein) {
const ok = () => mein === lauf;
while (ok()) {
allesZu();
// 1 · Erstauftritt
markiere('auftritt'); auftritt();
zeiger.style.transform = `translate(${innerWidth * .78}px, ${innerHeight * .9}px)`;
await warte(1300); if (!ok()) return;
// 2 · Menü
markiere('menue'); zeigerZu(menueKnopf); await warte(750); if (!ok()) return;
tippen(); if (kannPopover) menue.showPopover();
await warte(2000); if (!ok()) return;
zeiger.style.transform = `translate(${innerWidth * .8}px, ${innerHeight * .3}px)`; await warte(450); if (!ok()) return;
tippen(); if (kannPopover && menue.matches(':popover-open')) menue.hidePopover();
await warte(700); if (!ok()) return;
// 3 · Hovercard
markiere('karte'); zeigerZu(person, .45, .7); await warte(750 + 300); if (!ok()) return;
karteAuf(); await warte(2000); if (!ok()) return;
zeiger.style.transform = `translate(${innerWidth * .85}px, ${innerHeight * .72}px)`; await warte(200); if (!ok()) return;
karteZu(); await warte(800); if (!ok()) return;
// 4 · Dialog
markiere('dialog'); zeigerZu(dialogKnopf); await warte(750); if (!ok()) return;
tippen(); dialogAuf(); dialog.querySelector('input').blur();
await warte(1100); if (!ok()) return;
zeigerZu(dialog.querySelector('[data-zu]')); await warte(1100); if (!ok()) return;
tippen(); dialog.close();
await warte(1100);
}
}
function stopp() { lauf++; html.classList.remove('vorfuehrung'); if (kannPopover) { try { zeiger.hidePopover(); } catch (e) {} } }
function vorfuehren() {
stopp();
html.classList.add('vorfuehrung');
if (kannPopover) { zeiger.style.transition = 'none'; zeiger.style.transform = `translate(${innerWidth * .78}px, ${innerHeight * .9}px)`; zeiger.showPopover(); zeiger.offsetWidth; zeiger.style.transition = ''; }
runde(++lauf);
}
function pause() { lauf++; }
function weiter() { if (html.classList.contains('vorfuehrung')) runde(++lauf); }
function neustart() { stopp(); allesZu(); markiere('auftritt'); auftritt(); }
addEventListener('load', () => markiere('menue'));
})();
</script>
</body>
</html>