Player per Zwei-Klick (Spotify, Apple Music, Bandcamp, SoundCloud, YouTube, Vimeo)
Release, Musikvideo und DJ-Set hörbar machen, ohne vorher Daten an Spotify, YouTube & Co. zu senden: Platzhalter in Player-Höhe, Laden nach Klick, Fehler mit Link nach außen, Widerruf ohne Neuladen.
Album oder Single einer Band, Musikerin oder eines Chors mit Player von Spotify, Apple Music oder Bandcamp.
Musik und Videos hörbar machen, ohne vor der Einwilligung Daten an Plattformen zu senden. Sofort sichtbar sind Cover bzw. Poster vom eigenen Server, Titel, Länge, Trackliste als Text und alle Plattform-Links. Der eingebettete Player lädt erst nach Klick auf „Player von [Anbieter] laden“. Der Platzhalter hat mindestens die Höhe des Players, damit nichts springt. „[Anbieter] künftig direkt laden“ setzt die Kategorie „Medien“ der Einwilligung; ein Widerruf macht alle Player ohne Neuladen wieder zu Platzhaltern. Scheitert das Laden, bleibt der Link zur Plattform.
Fans wollen sofort hören, das Gesetz verlangt vor dem Laden fremder Player eine Einwilligung. Der Platzhalter löst beides: Er sieht aus wie Teil der Seite, nennt Anbieter und Datenfluss in einem Satz und lädt mit einem Klick. Trackliste und Plattform-Links stehen als Text da, also funktioniert die Seite auch ohne Einwilligung, für Suchmaschinen und für Screenreader.
Wann einsetzen
Bands, Musiker:innen, DJs, Labels, Chöre: Release-Seiten mit Spotify-, Apple-Music- oder Bandcamp-Player, Musikvideos über YouTube oder Vimeo, DJ-Sets über SoundCloud oder Mixcloud. Auch Podcasts und Hörproben auf Verlags- oder Kulturseiten.
Wann nicht
Selbst gehostete Videos brauchen keine Einwilligung: eigener-videoplayer. Karten und Social-Feeds deckt karte-zwei-klick ab, Buchungs-Widgets termin-buchung-zwei-klick. Wer nur auf Plattformen verweisen will, braucht keinen Player, eine Liste normaler Links reicht. Nie Player ohne Klick laden, nie Vorschaubilder vom Plattform-Server holen.
Varianten
- Release: Album oder Single mit Cover, Trackliste als Text, Plattform-Links und Player von Spotify, Apple Music oder Bandcamp
- Video: Musikvideo über youtube-nocookie.com bzw. Vimeo mit dnt=1, Poster selbst gehostet, Seitenverhältnis fest, Hinweis auf Untertitel
- Mix: DJ-Set über SoundCloud oder Mixcloud mit Tracklist und Zeitmarken als Text
- Mehrere Player auf einer Seite: eine Einwilligung lädt alle Platzhalter
Umsetzung
Je Player ein <figure> mit .flaeche (min-height: var(--h), Token je Anbieter) und <figcaption>. Die Embed-Adresse steht nur als data-src an der Figur; vor dem Klick lädt nichts von der Plattform, kein Skript, kein Vorschaubild.
Platzhalter: eigenes Cover bzw. Poster, Label „Album · 9 Titel · 38 Min.“, Titel, Satz zum Datenfluss mit Link zur Datenschutzerklärung (aria-describedby am Knopf), Knopf „Player von [Anbieter] laden“, Kästchen „[Anbieter] künftig direkt laden“, Link „Direkt bei [Anbieter] öffnen“ (neuer Tab). Beim Video steht auf dem Poster nur Titel und Knopf, der Rest in einer Zeile darunter.
Zustände über data-zustand: platzhalter → laedt (Platzhalter bleibt mit visibility: hidden im Fluss, Ladebalken, aria-live „Player wird geladen“) → geladen (iframe mit title, allow-Liste, loading=lazy; bei load Fokus ins iframe) oder fehler (Zeitüberschreitung, etwa 8 s: Text, „Direkt bei [Anbieter] öffnen“, „Nochmal versuchen“). „Player entfernen“ und der Widerruf führen zurück zum Platzhalter, ohne die Seite neu zu laden.
Einwilligung im selben Format wie consent-banner-gestaltet (Kategorie medien), dazu ein Ereignis, auf das alle Figuren hören. Liegt die Einwilligung beim Seitenaufruf vor, laden die Player direkt, aber ohne Fokus und ohne Abspielen.
YouTube über www.youtube-nocookie.com/embed/ID, Vimeo mit ?dnt=1; beides ersetzt die Einwilligung nicht. Kein autoplay. In der Demo ist der geladene Player eine Nachbildung per srcdoc (kein Ton); im Einsatz: iframe.src = figure.dataset.src.
Robust umsetzen
Telefon. Platzhalter in voller Breite, Cover klein neben dem Titel, Knopf in voller Breite. Ist der Platzhalter höher als der Player, bekommt das iframe dieselbe Höhe, damit nichts springt. Beim Video nur der Knopf auf dem Poster, Hinweis und Kästchen darunter. Plattform-Links als Raster mit 48 px hohen Zielen.
Ohne Bewegung. Platzhalter und Player tauschen ohne Überblendung oder Verschluss, der Ladebalken bewegt sich nicht. Nichts startet von selbst, auch nicht nach dem Laden.
Barrierefrei. Platzhalter als <figure> mit <figcaption>, Knopf mit Anbietername, Datenfluss per aria-describedby. Das iframe hat einen title mit Anbieter und Titel, nach dem Laden per Klick wandert der Fokus hinein, Tab führt durch den Player. Nichts startet mit Ton (WCAG 1.4.2). Ladezustand, „Geladen von …“ und Fehler in einer aria-live-Region der Bildunterschrift. Nach „Player entfernen“ springt der Fokus zurück auf den Ladeknopf, nach dem Widerruf auf den Einwilligungstext. Trackliste und Zeitmarken als <ol> mit <time>, Plattform-Links als normale Links mit „neuer Tab“. Unsichtbare Ebenen sind ausgeblendet bzw. visibility: hidden, damit der Fokus nicht ins Leere springt.
Code
<!DOCTYPE html>
<!--
Player per Zwei-Klick (Spotify, Apple Music, Bandcamp, SoundCloud, YouTube, Vimeo)
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/player-einbettung-zwei-klick/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-variante="release">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Player per Zwei-Klick (Spotify, Apple Music, Bandcamp, SoundCloud, YouTube, Vimeo) · 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: 'Science Gothic'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/science-gothic:vf@latest/latin-wght-normal.woff') format('woff'); font-weight: 100 900; font-stretch: 50% 200%; font-display: swap; }
@font-face { font-family: 'Redaction'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/redaction@latest/latin-400-normal.woff') format('woff'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Redaction'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/redaction@latest/latin-400-italic.woff') format('woff'); font-weight: 400; font-style: italic; font-display: swap; }
@font-face { font-family: 'Martian Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/martian-mono:vf@latest/latin-wght-normal.woff') format('woff'); font-weight: 100 800; font-stretch: 75% 112.5%; font-display: swap; }
/* Rezept musik-kuenstler-buehne: Tinte, Papier und ein Ära-Token aus dem Cover (hier Ultramarin) */
:root {
--tinte: #111113; --papier: #eeebe3; --aera: #2d3bff; /* Papier auf Ultramarin 5,5 : 1; auf Tinte nur als Fläche mit Papierkante */
--leise: #9a9893; --rot: #ff6b5e; --linie: rgba(238, 235, 227, .2); --flaeche: rgba(238, 235, 227, .06);
/* Höhe je Anbieter als Token: Platzhalter und Player gleich hoch, kein Layoutsprung */
--h-spotify: 352px; --h-apple: 450px; --h-bandcamp: 470px; --h-soundcloud: 300px; --h-mixcloud: 400px;
/* UI-Stil token-toggle-asymmetrisch, Takt aus token-brutal-hart */
--auf: cubic-bezier(0.16, 1, 0.3, 1); --zu: cubic-bezier(0.7, 0, 0.84, 0); --t-auf: 300ms; --t-zu: 200ms; --takt: 80ms;
--kante: clamp(16px, 4vw, 56px);
--mono: 'Martian Mono', ui-monospace, monospace; --kopf: 'Science Gothic', 'Arial Narrow', sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--tinte); color: var(--papier); font: 400 1.1875rem/1.6 'Redaction', Georgia, serif; -webkit-font-smoothing: antialiased; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; }
a { color: inherit; text-underline-offset: 3px; text-decoration-thickness: 1px; }
a:hover { text-decoration-thickness: 2px; }
button, input { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
ol, ul { list-style: none; }
img { display: block; max-width: 100%; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
:focus-visible { outline: 2px solid var(--papier); outline-offset: 3px; }
.label { font: 500 .75rem/1.35 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
/* ---------- Kopf der Künstlerseite ---------- */
.seite { display: flex; align-items: center; justify-content: space-between; gap: 20px; height: 64px; padding: 0 var(--kante); border-bottom: 1px solid var(--linie); }
.wortmarke { font: 800 1.25rem/1 var(--kopf); font-stretch: 200%; text-transform: uppercase; letter-spacing: -.02em; text-decoration: none; }
.seite ul { display: flex; gap: clamp(14px, 2.4vw, 32px); }
.seite nav a { font: 600 .8125rem/1 var(--mono); letter-spacing: .06em; text-transform: uppercase; text-decoration: none; padding: 8px 0; border-bottom: 2px solid transparent; }
.seite nav a[aria-current] { border-color: var(--aera); box-shadow: 0 2px 0 0 var(--aera); }
/* ---------- Sektion ---------- */
.musik { max-width: 1320px; margin: 0 auto; padding: clamp(32px, 5vw, 72px) var(--kante) 120px; }
.sek-kopf { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: end; gap: 12px 32px; padding-bottom: 20px; border-bottom: 2px solid var(--papier); margin-bottom: clamp(24px, 3vw, 40px); }
.sek-kopf .label { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; }
.sek-kopf .label i { width: 10px; height: 10px; background: var(--aera); box-shadow: 0 0 0 1px var(--papier); }
h2 { font: 700 clamp(2rem, 1.2rem + 3.5vw, 4rem)/.95 var(--kopf); font-stretch: 120%; text-transform: uppercase; letter-spacing: -.01em; }
.anbieter-wahl { border: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
.anbieter-wahl legend { float: left; margin-right: 6px; font: 500 .75rem/44px var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.anbieter-wahl label { position: relative; display: inline-flex; align-items: center; min-height: 44px; padding: 0 14px; border: 1px solid var(--linie); font: 600 .8125rem/1 var(--mono); letter-spacing: .04em; text-transform: uppercase; cursor: pointer; }
.anbieter-wahl input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.anbieter-wahl label:has(:checked) { background: var(--papier); color: var(--tinte); border-color: var(--papier); }
.anbieter-wahl label:has(:focus-visible) { outline: 2px solid var(--papier); outline-offset: 3px; }
.anbieter-wahl label:hover { border-color: var(--papier); }
.block[hidden] { display: none; }
.raster { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, .8fr); gap: clamp(28px, 4vw, 64px); align-items: start; }
/* ---------- Einbettung: Figur mit Platzhalter, Laden, Player, Fehler ---------- */
.einbettung { --h: var(--h-spotify); }
.flaeche { position: relative; min-height: var(--h); border: 2px solid var(--papier); background: var(--tinte); overflow: hidden; }
.einbettung[data-art="video"] .flaeche { min-height: 0; aspect-ratio: 16 / 9; }
.flaeche > .ebene { position: absolute; inset: 0; }
.flaeche > .platz { position: relative; inset: auto; min-height: calc(var(--h) - 4px); }
.einbettung[data-art="video"] .flaeche > .platz { position: absolute; inset: 0; min-height: 0; }
.flaeche:not([data-zustand="platzhalter"]) > .platz { visibility: hidden; }
.flaeche:not([data-zustand="laedt"]) > .laedt, .flaeche:not([data-zustand="fehler"]) > .fehler { display: none; }
/* Audio-Platzhalter: Cover wächst mit der Höhe des Anbieters */
.platz-audio { --cover: clamp(88px, calc(var(--h) - 220px), 260px); display: grid; grid-template-columns: var(--cover) minmax(0, 1fr); grid-template-rows: auto 1fr; gap: 14px 20px; padding: 18px 20px 20px; }
.platz-audio .cover { width: var(--cover); height: var(--cover); object-fit: cover; box-shadow: 0 0 0 1px var(--linie); }
.platz-audio .kopfzeile { display: grid; align-content: end; gap: 6px; min-width: 0; }
.ptitel { font: 400 clamp(1.5rem, 1rem + 1.6vw, 2.25rem)/1.05 'Redaction', Georgia, serif; text-wrap: balance; }
.ptitel small { display: block; font: 700 .875rem/1.2 var(--kopf); font-stretch: 140%; text-transform: uppercase; letter-spacing: .01em; margin-top: 8px; }
.platz-audio .unten { grid-column: 1 / -1; display: grid; gap: 12px; align-content: end; }
.datenfluss { font-size: .9375rem; line-height: 1.5; color: #c9c6be; max-width: 52ch; text-wrap: pretty; }
.zeile-knoepfe { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; }
.kaestchen { display: inline-flex; align-items: center; gap: 10px; min-height: 32px; font: 500 .75rem/1.3 var(--mono); letter-spacing: .02em; cursor: pointer; }
.kaestchen input { width: 18px; height: 18px; accent-color: var(--aera); flex: none; }
.extern { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; font: 500 .75rem/1.3 var(--mono); letter-spacing: .02em; }
.extern svg { width: 12px; height: 12px; flex: none; }
/* Video-Platzhalter: Poster vom eigenen Server, Knopf auf eigener Tinte-Fläche */
.platz-video img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.platz-video .box { position: absolute; left: clamp(12px, 3%, 28px); bottom: clamp(12px, 4%, 28px); display: grid; gap: 10px; justify-items: start; max-width: min(420px, calc(100% - 24px)); padding: 16px 18px 18px; background: var(--tinte); border: 2px solid var(--papier); }
.platz-video .box .label { color: #c9c6be; }
.platz-video .box .ptitel { font-size: clamp(1.25rem, 1rem + 1vw, 1.75rem); }
.video-zeile { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px 24px; align-items: center; padding: 14px 0 0; }
.video-zeile .datenfluss { font-size: .9375rem; }
.video-zeile .zeile-knoepfe { justify-content: flex-end; }
/* Harte Taste (tiefen-taste-3d, versatz-brutal) für die eine Haupthandlung: Anhören */
.taste { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 48px; padding: 0 20px; background: var(--aera); color: var(--papier); font: 600 .875rem/1.15 var(--mono); letter-spacing: .04em; text-transform: uppercase; text-align: left;
box-shadow: inset 0 0 0 2px var(--papier), 5px 5px 0 0 var(--papier); margin: 0 5px 5px 0; transition: transform 160ms steps(2, end), box-shadow 160ms steps(2, end); }
.taste svg { width: 14px; height: 14px; flex: none; }
@media (hover: hover) { .taste:hover { transform: translate(-2px, -2px); box-shadow: inset 0 0 0 2px var(--papier), 7px 7px 0 0 var(--papier); } }
.taste.ist-hover { transform: translate(-2px, -2px); box-shadow: inset 0 0 0 2px var(--papier), 7px 7px 0 0 var(--papier); }
.taste:active, .taste.drueckt { transform: translate(5px, 5px); box-shadow: inset 0 0 0 2px var(--papier), 0 0 0 0 var(--papier); transition-duration: 0ms; }
.taste:focus-visible { outline-offset: 6px; }
/* Kontur-Knopf (kontur-button-zeichnen, laufend) für Nebenaktionen */
.kontur { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 16px; font: 600 .8125rem/1.1 var(--mono); letter-spacing: .04em; text-transform: uppercase; text-decoration: none; }
.kontur svg.pfeil { width: 12px; height: 12px; }
.kontur__svg { position: absolute; inset: -1px; width: calc(100% + 2px); height: calc(100% + 2px); overflow: visible; pointer-events: none; }
.kontur__svg rect { x: .75px; y: .75px; width: calc(100% - 1.5px); height: calc(100% - 1.5px); fill: none; stroke: currentColor; stroke-width: 1.25; stroke-dasharray: 7 5; }
@media (hover: hover) { .kontur:hover .kontur__svg rect { animation: laufen 1600ms linear 3; } .kontur:hover { background: var(--flaeche); } }
.kontur:focus-visible .kontur__svg rect, .kontur.ist-hover .kontur__svg rect { animation: laufen 1600ms linear 3; }
@keyframes laufen { to { stroke-dashoffset: -48; } }
/* Laden: Höhe bleibt, Balken springt in Stufen (6 × 80 ms) */
.laedt { display: grid; place-content: center; justify-items: center; gap: 14px; background: var(--tinte); text-align: center; }
.laedt p { font: 600 .8125rem/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.balken { width: 192px; height: 8px; box-shadow: 0 0 0 1px var(--papier); background: linear-gradient(90deg, var(--aera) 0 0) 0 0 / 0 100% no-repeat; }
.flaeche[data-zustand="laedt"] .balken { animation: balken calc(var(--takt) * 6) steps(6, end) infinite; }
@keyframes balken { to { background-size: 100% 100%; } }
.laedt small { font: 400 .75rem/1.4 var(--mono); color: var(--leise); }
/* Player (iframe): öffnet als Verschluss von oben, 300 ms expo.out */
.flaeche iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; background: #232326; clip-path: inset(0 0 0 0); transition: clip-path var(--t-auf) var(--auf); }
@starting-style { .flaeche iframe { clip-path: inset(0 0 100% 0); } }
.flaeche.schliesst iframe { clip-path: inset(0 0 100% 0); transition: clip-path var(--t-zu) var(--zu); }
/* Fehler: Link nach außen bleibt */
.fehler { display: grid; align-content: center; gap: 12px; padding: 24px; background: repeating-linear-gradient(-45deg, var(--tinte) 0 10px, #17171a 10px 12px); }
.fehler b { font: 600 .8125rem/1.3 var(--mono); letter-spacing: .06em; text-transform: uppercase; color: var(--rot); display: flex; gap: 8px; align-items: center; }
.fehler p { max-width: 46ch; font-size: 1rem; line-height: 1.5; }
.fehler .zeile-knoepfe { gap: 10px; }
figcaption { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px 18px; padding-top: 12px; font: 400 .75rem/1.5 var(--mono); color: var(--leise); }
figcaption .status { color: var(--papier); }
figcaption button { font: 600 .75rem/1 var(--mono); text-decoration: underline; text-underline-offset: 3px; min-height: 24px; }
/* ---------- Release-Infos, Trackliste, Plattform-Links ---------- */
.info h3 { font: 400 clamp(1.75rem, 1rem + 2.5vw, 3rem)/1.02 'Redaction', Georgia, serif; text-wrap: balance; }
.meta { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 10px; font: 500 .8125rem/1.5 var(--mono); font-variant-numeric: tabular-nums; color: var(--leise); }
.meta b { color: var(--papier); font-weight: 500; }
.tracks { margin-top: 24px; border-top: 1px solid var(--papier); counter-reset: t; }
.tracks li { display: grid; grid-template-columns: 2.4rem minmax(0, 1fr) auto; gap: 12px; align-items: baseline; padding: 8px 0; border-bottom: 1px solid var(--linie); font-size: 1.0625rem; line-height: 1.35; }
.tracks .nr, .tracks time { font: 500 .8125rem/1 var(--mono); font-variant-numeric: tabular-nums; color: var(--leise); }
.tracks .nr { font-stretch: 87.5%; }
[data-block="mix"] .tracks li { grid-template-columns: 3.6rem minmax(0, 1fr); }
.tracks .wer { display: block; font: 500 .75rem/1.4 var(--mono); color: var(--leise); }
.summe { display: flex; justify-content: space-between; padding-top: 10px; font: 500 .75rem/1.4 var(--mono); color: var(--leise); text-transform: uppercase; letter-spacing: .06em; }
.formate { display: grid; gap: 0; margin-top: 22px; border-top: 1px solid var(--papier); }
.formate li { display: flex; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--linie); font: 500 .8125rem/1.4 var(--mono); font-variant-numeric: tabular-nums; }
.formate a { text-decoration: none; }
.formate a:hover span:first-child { text-decoration: underline; }
.plattformen { margin-top: 22px; }
.plattformen h4, .weitere h4 { font: 700 .9375rem/1 var(--kopf); font-stretch: 140%; text-transform: uppercase; margin-bottom: 10px; }
.plattformen ul { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(10.5rem, 100%), 1fr)); border-top: 1px solid var(--linie); border-left: 1px solid var(--linie); }
.plattformen a { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 48px; padding: 0 14px; border-right: 1px solid var(--linie); border-bottom: 1px solid var(--linie); font: 600 .8125rem/1.2 var(--mono); letter-spacing: .02em; text-decoration: none; }
.plattformen a:hover { background: var(--papier); color: var(--tinte); }
.plattformen svg, .weitere svg { width: 12px; height: 12px; flex: none; }
.platzhalter-recht { margin-top: 18px; padding: 8px 12px 10px; border: 1px dashed var(--leise); font-size: .875rem; line-height: 1.5; color: #c9c6be; }
.platzhalter-recht::before { content: 'Platzhalter · vom Kunden prüfen lassen'; display: block; font: 500 .625rem/1.7 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.weitere { margin-top: 28px; }
.weitere ul { border-top: 1px solid var(--linie); }
.weitere a { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 12px; min-height: 48px; padding: 6px 0; border-bottom: 1px solid var(--linie); text-decoration: none; font-size: 1.0625rem; line-height: 1.3; }
.weitere a:hover .t { text-decoration: underline; }
.weitere time { font: 500 .75rem/1 var(--mono); color: var(--leise); }
.video-info { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 22rem); gap: 24px 48px; margin-top: 28px; }
/* ---------- Einwilligung (Ausschnitt der Datenschutz-Einstellungen) ---------- */
.einwilligung { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 20px; margin-top: clamp(36px, 5vw, 56px); padding: 14px 0; border-top: 1px solid var(--linie); border-bottom: 1px solid var(--linie); font: 500 .8125rem/1.45 var(--mono); }
.einwilligung b { font-weight: 600; }
.einwilligung .an { color: var(--papier); }
.einwilligung p span { color: var(--leise); }
/* ---------- Meldung, Pult, Zeiger ---------- */
.toast { position: fixed; left: 50%; bottom: 24px; z-index: 50; translate: -50% 12px; opacity: 0; visibility: hidden; width: min(480px, calc(100vw - 24px)); display: grid; gap: 4px; padding: 14px 18px; background: var(--papier); color: var(--tinte); box-shadow: 6px 6px 0 0 var(--aera); transition: opacity 170ms var(--zu), translate 170ms var(--zu), visibility 0s linear 170ms; }
.toast.an { opacity: 1; translate: -50% 0; visibility: visible; transition: opacity 250ms var(--auf), translate 250ms var(--auf), visibility 0s; }
.toast b { font: 600 .875rem/1.3 var(--mono); }
.toast small { font: 400 .75rem/1.45 var(--mono); color: #4a4843; overflow-wrap: anywhere; }
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 40; display: grid; gap: 8px; padding: 12px 14px; max-width: 370px; background: rgba(17, 17, 19, .95); color: #eeebe3; border: 1px solid rgba(238, 235, 227, .35); font: 500 .6875rem/1.45 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.pult .zeile { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pult button { font: inherit; letter-spacing: inherit; text-transform: inherit; min-height: 28px; padding: 0 9px; border: 1px solid rgba(238, 235, 227, .45); color: #eeebe3; }
.pult button[aria-pressed="true"] { background: #eeebe3; color: #111113; }
.pult .werte { color: #a9a7a1; text-transform: none; letter-spacing: .02em; }
.pult .werte b { color: #eeebe3; font-weight: 500; }
.zeiger { position: fixed; left: 0; top: 0; z-index: 60; width: 24px; height: 24px; pointer-events: none; display: none; transition: transform 640ms var(--auf); }
.vorfuehrung .zeiger { display: block; }
.zeiger.klick { scale: .82; }
/* ---------- Reduzierte Bewegung: Platzhalter und Player tauschen ohne Übergang ---------- */
.reduziert .flaeche iframe, .reduziert .flaeche.schliesst iframe { transition: none; }
.reduziert .flaeche[data-zustand="laedt"] .balken { animation: none; background-size: 50% 100%; }
.reduziert .kontur__svg rect { animation: none !important; }
.reduziert .taste { transition: none; }
.reduziert .toast, .reduziert .toast.an { translate: -50% 0; transition: opacity 120ms linear, visibility 0s linear 120ms; }
.reduziert .toast.an { transition: opacity 120ms linear; }
.reduziert .zeiger { transition: none; }
/* ---------- Touch ---------- */
@media (hover: none) {
.anbieter-wahl label:hover { border-color: var(--linie); }
.plattformen a:hover { background: none; color: inherit; }
.kaestchen, .extern { min-height: 44px; }
}
/* ---------- Schmaler ---------- */
@media (max-width: 1000px) {
.raster { grid-template-columns: minmax(0, 1fr); }
.video-info { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 760px) {
.seite { height: 56px; }
.seite nav li:not(.jetzt) { display: none; }
.musik { padding-top: 28px; }
.anbieter-wahl legend { width: 100%; line-height: 1.4; margin-bottom: 4px; }
.platz-audio { --cover: 96px; padding: 16px; gap: 12px 14px; }
.datenfluss { font-size: .875rem; line-height: 1.45; }
.platz-audio .taste { width: calc(100% - 5px); }
.zeile-knoepfe { gap: 4px 16px; }
.video-zeile { grid-template-columns: minmax(0, 1fr); }
.video-zeile .zeile-knoepfe { justify-content: flex-start; }
.platz-video .box { inset: 0; max-width: none; place-content: center; justify-items: center; padding: 10px; background: none; border: 0; }
.platz-video .box .taste { white-space: nowrap; padding: 0 16px; }
.platz-video .box .label, .platz-video .box .ptitel { display: none; }
}
@media (max-width: 360px) {
.wortmarke { font-stretch: 150%; }
.platz-audio { --cover: 72px; }
.ptitel { font-size: 1.375rem; }
.platz-video .box .taste { white-space: normal; text-align: center; letter-spacing: .02em; padding: 6px 12px; max-width: calc(100% - 10px); }
}
/* ---------- Vorschau 960 × 720 ---------- */
.vorschau body { overflow: hidden; }
.vorschau .musik { padding-top: 22px; }
.vorschau .sek-kopf { padding-bottom: 14px; margin-bottom: 20px; }
.vorschau .platzhalter-recht, .vorschau .weitere { display: none; }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.taste, .kontur { border: 1px solid ButtonText !important; }
.anbieter-wahl label { border: 1px solid ButtonText; }
.anbieter-wahl label:has(:checked) { outline: 2px solid Highlight; }
}
</style>
</head>
<body>
<header class="seite">
<a class="wortmarke" href="#musik">Flutung</a>
<nav aria-label="Hauptnavigation"><ul id="nav"></ul></nav>
</header>
<main class="musik" id="musik">
<div class="sek-kopf">
<div>
<p class="label"><i aria-hidden="true"></i><span id="kopf-label">Musik · Neues Album</span></p>
<h2 id="kopf-titel">Anhören</h2>
</div>
<fieldset class="anbieter-wahl" id="anbieter-wahl">
<legend id="wahl-legende">Hören mit</legend>
</fieldset>
</div>
<!-- ===== Variante release: Album mit Spotify, Apple Music oder Bandcamp ===== -->
<section class="block" data-block="release" aria-labelledby="rel-titel">
<div class="raster">
<div>
<figure class="einbettung" id="e-release" data-art="audio" aria-labelledby="rel-cap">
<div class="flaeche" data-zustand="platzhalter">
<div class="ebene platz platz-audio">
<img class="cover" src="bilder/player-einbettung-zwei-klick-cover.webp" width="640" height="640" alt="Cover von „Ostsee ohne Meer“: ein gefluteter Tagebau von oben, ultramarinblaues Wasser zwischen sandfarbenen Terrassen und weißen Höhenlinien" fetchpriority="high">
<div class="kopfzeile">
<p class="label">Album · 9 Titel · 38 Min.</p>
<p class="ptitel">Ostsee ohne Meer<small>Flutung</small></p>
</div>
<div class="unten">
<p class="datenfluss" id="df-release">Beim Laden überträgt <span data-name>Spotify</span> Daten, u. a. deine IP-Adresse. Mehr in der <a href="#datenschutz">Datenschutzerklärung</a>.</p>
<button class="taste laden" type="button" aria-describedby="df-release"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M2 1v12l11-6z" fill="currentColor"/></svg><span>Player von <span data-name>Spotify</span> laden</span></button>
<div class="zeile-knoepfe">
<label class="kaestchen"><input type="checkbox" class="kuenftig"><span><span data-name>Spotify</span> künftig direkt laden</span></label>
<a class="extern" data-extern href="https://open.spotify.com/" target="_blank" rel="noopener"><span>Direkt bei <span data-name>Spotify</span> öffnen</span><span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a>
</div>
</div>
</div>
<div class="ebene laedt" aria-hidden="true"><div class="balken"></div><p>Player wird geladen</p><small data-ziel></small></div>
<div class="ebene fehler"></div>
</div>
<figcaption><span id="rel-cap">Flutung, „Ostsee ohne Meer“ (Album, 2026) · Player von <span data-name>Spotify</span></span><span class="status" aria-live="polite"></span></figcaption>
</figure>
<div class="plattformen">
<h4>Überall hören</h4>
<ul>
<li><a href="https://open.spotify.com/" target="_blank" rel="noopener">Spotify<span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
<li><a href="https://music.apple.com/" target="_blank" rel="noopener">Apple Music<span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
<li><a href="https://bandcamp.com/" target="_blank" rel="noopener">Bandcamp<span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
<li><a href="https://music.youtube.com/" target="_blank" rel="noopener">YouTube Music<span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
<li><a href="https://www.deezer.com/" target="_blank" rel="noopener">Deezer<span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
<li><a href="https://tidal.com/" target="_blank" rel="noopener">Tidal<span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
</ul>
</div>
</div>
<div class="info">
<h3 id="rel-titel">Ostsee ohne Meer</h3>
<p class="meta"><span>VÖ <b>Fr 23.10.2026</b></span><span>Sandow Schallplatten</span><span>Kat.-Nr. <b>SAN 014</b></span></p>
<ol class="tracks" aria-label="Trackliste">
<li><span class="nr">01</span><span>Restloch</span><time datetime="PT3M58S">3:58</time></li>
<li><span class="nr">02</span><span>Förderbrücke</span><time datetime="PT4M41S">4:41</time></li>
<li><span class="nr">03</span><span>Pegel</span><time datetime="PT3M52S">3:52</time></li>
<li><span class="nr">04</span><span>Böschung</span><time datetime="PT4M5S">4:05</time></li>
<li><span class="nr">05</span><span>Kippenland</span><time datetime="PT5M12S">5:12</time></li>
<li><span class="nr">06</span><span>Flutung (Teil 1)</span><time datetime="PT2M47S">2:47</time></li>
<li><span class="nr">07</span><span>Sandbank am Montag</span><time datetime="PT4M20S">4:20</time></li>
<li><span class="nr">08</span><span>Ufersicherung</span><time datetime="PT4M33S">4:33</time></li>
<li><span class="nr">09</span><span>Ostsee ohne Meer</span><time datetime="PT4M32S">4:32</time></li>
</ol>
<p class="summe"><span>9 Titel</span><span>38:00</span></p>
<ul class="formate" aria-label="Formate">
<li><a href="#shop"><span>Vinyl LP, 180 g</span></a><span>26,00 €</span></li>
<li><a href="#shop"><span>CD im Digipak</span></a><span>14,00 €</span></li>
<li><a href="https://bandcamp.com/" target="_blank" rel="noopener"><span>Download bei Bandcamp<span class="sr"> (neuer Tab)</span></span></a><span>ab 9,00 €</span></li>
</ul>
<p class="platzhalter-recht">Preise inkl. MwSt., zzgl. Versand. Eigene Songs auf der eigenen Seite: Rechtehinweis und Lizenz für die Eigenpräsentation klären.</p>
</div>
</div>
</section>
<!-- ===== Variante video: Musikvideo über YouTube (nocookie) oder Vimeo (dnt) ===== -->
<section class="block" data-block="video" aria-labelledby="vid-titel" hidden>
<figure class="einbettung" id="e-video" data-art="video" aria-labelledby="vid-cap">
<div class="flaeche" data-zustand="platzhalter">
<div class="ebene platz platz-video">
<img src="bilder/player-einbettung-zwei-klick-poster.webp" width="1280" height="720" alt="Standbild aus dem Video „Pegel“: eine Förderbrücke als Silhouette über einem See in der Dämmerung, vorn eine rot-weiße Pegellatte, am Ufer eine Person">
<div class="box">
<p class="label">Musikvideo · 3:52 · Untertitel DE, EN</p>
<p class="ptitel">Pegel</p>
<button class="taste laden" type="button" aria-describedby="df-video"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M2 1v12l11-6z" fill="currentColor"/></svg><span>Video von <span data-name>YouTube</span> laden</span></button>
</div>
</div>
<div class="ebene laedt" aria-hidden="true"><div class="balken"></div><p>Player wird geladen</p><small data-ziel></small></div>
<div class="ebene fehler"></div>
</div>
<div class="video-zeile">
<p class="datenfluss" id="df-video">Beim Laden überträgt <span data-name>YouTube</span> Daten, u. a. deine IP-Adresse. Mehr in der <a href="#datenschutz">Datenschutzerklärung</a>.</p>
<div class="zeile-knoepfe">
<label class="kaestchen"><input type="checkbox" class="kuenftig"><span><span data-name>YouTube</span> künftig direkt laden</span></label>
<a class="extern" data-extern href="https://www.youtube.com/" target="_blank" rel="noopener"><span>Direkt bei <span data-name>YouTube</span> ansehen</span><span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a>
</div>
</div>
<figcaption><span id="vid-cap">Flutung, „Pegel“ (Musikvideo, 2026) · Player von <span data-name>YouTube</span></span><span class="status" aria-live="polite"></span></figcaption>
</figure>
<div class="video-info">
<div class="info">
<h3 id="vid-titel">Pegel</h3>
<p class="meta"><span>Regie <b>Ilka Pietsch</b></span><span>gedreht am Cottbuser Ostsee</span><span>Länge <b>3:52</b></span></p>
<p class="datenfluss" style="margin-top:14px">Der Text im Video ist gesungen und hat deutsche und englische Untertitel. Im Player über „Untertitel“ einschalten. Das Video startet erst, wenn du im Player auf Abspielen drückst, und nie mit Ton von selbst.</p>
</div>
<div class="weitere">
<h4>Weitere Videos</h4>
<ul>
<li><a href="https://www.youtube.com/" target="_blank" rel="noopener"><span class="t">Förderbrücke (live im Lokschuppen Sandow)</span><time datetime="PT6M2S">6:02</time><span class="sr"> (YouTube, neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
<li><a href="https://www.youtube.com/" target="_blank" rel="noopener"><span class="t">Restloch (Visualizer)</span><time datetime="PT3M58S">3:58</time><span class="sr"> (YouTube, neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
<li><a href="https://www.youtube.com/" target="_blank" rel="noopener"><span class="t">Studio-Tagebuch, Folge 1</span><time datetime="PT11M40S">11:40</time><span class="sr"> (YouTube, neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a></li>
</ul>
</div>
</div>
</section>
<!-- ===== Variante mix: DJ-Set über SoundCloud oder Mixcloud ===== -->
<section class="block" data-block="mix" aria-labelledby="mix-titel" hidden>
<div class="raster">
<figure class="einbettung" id="e-mix" data-art="audio" aria-labelledby="mix-cap">
<div class="flaeche" data-zustand="platzhalter">
<div class="ebene platz platz-audio">
<img class="cover" src="bilder/player-einbettung-zwei-klick-mix.webp" width="640" height="640" alt="Cover des Sets „Ufersicherung“: wellige weiße Ringe auf Ultramarin wie Rillen einer Platte, unten eine Wasserlinie">
<div class="kopfzeile">
<p class="label">DJ-Set · 10 Titel · 62 Min.</p>
<p class="ptitel">Ufersicherung<small>Flutung live</small></p>
</div>
<div class="unten">
<p class="datenfluss" id="df-mix">Beim Laden überträgt <span data-name>SoundCloud</span> Daten, u. a. deine IP-Adresse. Mehr in der <a href="#datenschutz">Datenschutzerklärung</a>.</p>
<button class="taste laden" type="button" aria-describedby="df-mix"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M2 1v12l11-6z" fill="currentColor"/></svg><span>Player von <span data-name>SoundCloud</span> laden</span></button>
<div class="zeile-knoepfe">
<label class="kaestchen"><input type="checkbox" class="kuenftig"><span><span data-name>SoundCloud</span> künftig direkt laden</span></label>
<a class="extern" data-extern href="https://soundcloud.com/" target="_blank" rel="noopener"><span>Direkt bei <span data-name>SoundCloud</span> öffnen</span><span class="sr"> (neuer Tab)</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></a>
</div>
</div>
</div>
<div class="ebene laedt" aria-hidden="true"><div class="balken"></div><p>Player wird geladen</p><small data-ziel></small></div>
<div class="ebene fehler"></div>
</div>
<figcaption><span id="mix-cap">Flutung, „Ufersicherung“ (Live-Set, Lokschuppen Sandow, 03.10.2026) · Player von <span data-name>SoundCloud</span></span><span class="status" aria-live="polite"></span></figcaption>
</figure>
<div class="info">
<h3 id="mix-titel">Ufersicherung</h3>
<p class="meta"><span>Live-Set <b>03.10.2026</b></span><span>Lokschuppen Sandow, Cottbus</span><span>Länge <b>62:14</b></span></p>
<ol class="tracks" aria-label="Tracklist mit Zeitmarken">
<li><time datetime="PT0S">00:00</time><span>Restloch (Intro-Edit)<span class="wer">Flutung</span></span></li>
<li><time datetime="PT4M12S">04:12</time><span>Kalter Strom<span class="wer">Oszkar Lind</span></span></li>
<li><time datetime="PT9M40S">09:40</time><span>Grubenlicht<span class="wer">Halde & Hain</span></span></li>
<li><time datetime="PT15M5S">15:05</time><span>Nightshift Ferry<span class="wer">Tamsin Rook</span></span></li>
<li><time datetime="PT21M30S">21:30</time><span>Pegel (Dub)<span class="wer">Flutung</span></span></li>
<li><time datetime="PT27M48S">27:48</time><span>Sandrauschen<span class="wer">Kippenkind</span></span></li>
<li><time datetime="PT34M10S">34:10</time><span>Tiefe Böschung<span class="wer">Oda Vehse</span></span></li>
<li><time datetime="PT40M55S">40:55</time><span>Förderbrücke (unveröffentlicht)<span class="wer">Flutung</span></span></li>
<li><time datetime="PT48M20S">48:20</time><span>Blaue Stunde<span class="wer">Lumen Kolonie</span></span></li>
<li><time datetime="PT55M2S">55:02</time><span>Ostsee ohne Meer (Outro)<span class="wer">Flutung</span></span></li>
</ol>
<p class="platzhalter-recht">Mixe mit Titeln anderer Künstler:innen nicht als eigene Datei hosten, nur über Plattformen, die das lizenzieren.</p>
</div>
</div>
</section>
<div class="einwilligung" id="datenschutz">
<p id="einw-text" aria-live="polite" tabindex="-1"></p>
<button class="kontur" type="button" id="widerrufen" hidden><span>Einwilligung widerrufen</span><svg class="kontur__svg" aria-hidden="true" focusable="false"><rect width="100%" height="100%"/></svg></button>
</div>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"><b></b><small></small></div>
<div class="pult">
<div class="zeile">Variante
<button type="button" data-v="release" aria-pressed="true">Release</button>
<button type="button" data-v="video" aria-pressed="false">Video</button>
<button type="button" data-v="mix" aria-pressed="false">Mix</button>
</div>
<div class="zeile">Laden
<button type="button" data-laden="ok" aria-pressed="true">klappt</button>
<button type="button" data-laden="blockiert" aria-pressed="false">blockiert</button>
</div>
<div class="werte" id="werte"></div>
</div>
<svg class="zeiger" id="zeiger" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 2l16 9-7 1.8L8.4 21z" fill="#eeebe3" stroke="#111113" stroke-width="1.6" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, el = document) => el.querySelector(s), $$ = (s, el = document) => [...el.querySelectorAll(s)];
/* ---------- Anbieter: Name, Höhe (Token), Embed-Adresse, Link nach außen ---------- */
// data-src ist die echte Einbettung. Sie wird erst nach Klick in das iframe gesetzt; vorher lädt nichts von der Plattform.
const ANBIETER = {
spotify: { name: 'Spotify', h: '--h-spotify', src: 'https://open.spotify.com/embed/album/BEISPIEL?utm_source=generator', aussen: 'https://open.spotify.com/', host: 'open.spotify.com' },
apple: { name: 'Apple Music', h: '--h-apple', src: 'https://embed.music.apple.com/de/album/ostsee-ohne-meer/BEISPIEL', aussen: 'https://music.apple.com/', host: 'embed.music.apple.com' },
bandcamp: { name: 'Bandcamp', h: '--h-bandcamp', src: 'https://bandcamp.com/EmbeddedPlayer/album=BEISPIEL/size=large/bgcol=111113/linkcol=2d3bff/tracklist=true/transparent=true/', aussen: 'https://bandcamp.com/', host: 'bandcamp.com' },
youtube: { name: 'YouTube', h: null, src: 'https://www.youtube-nocookie.com/embed/BEISPIEL?rel=0', aussen: 'https://www.youtube.com/', host: 'www.youtube-nocookie.com' },
vimeo: { name: 'Vimeo', h: null, src: 'https://player.vimeo.com/video/BEISPIEL?dnt=1', aussen: 'https://vimeo.com/', host: 'player.vimeo.com' },
soundcloud: { name: 'SoundCloud', h: '--h-soundcloud', src: 'https://w.soundcloud.com/player/?url=https%3A//soundcloud.com/flutung/ufersicherung&visual=true', aussen: 'https://soundcloud.com/', host: 'w.soundcloud.com' },
mixcloud: { name: 'Mixcloud', h: '--h-mixcloud', src: 'https://player-widget.mixcloud.com/widget/iframe/?feed=%2Fflutung%2Fufersicherung%2F', aussen: 'https://www.mixcloud.com/', host: 'player-widget.mixcloud.com' },
};
const VARIANTEN = {
release: { fig: 'e-release', wahl: ['spotify', 'apple', 'bandcamp'], legende: 'Hören mit', label: 'Musik · Neues Album', titel: 'Anhören', nav: 'Musik', werte: 'Höhe <b>352 / 450 / 470 px</b> je Anbieter' },
video: { fig: 'e-video', wahl: ['youtube', 'vimeo'], legende: 'Ansehen mit', label: 'Videos · Neues Video', titel: 'Ansehen', nav: 'Videos', werte: 'Seitenverhältnis <b>16 : 9</b> · youtube-nocookie · Vimeo <b>dnt=1</b>' },
mix: { fig: 'e-mix', wahl: ['soundcloud', 'mixcloud'], legende: 'Hören mit', label: 'Mixe · Live-Set', titel: 'Mixe', nav: 'Mixe', werte: 'Höhe <b>300 / 400 px</b> je Anbieter' },
};
const TITEL = { 'e-release': 'Flutung – Ostsee ohne Meer', 'e-video': 'Flutung – Pegel (Musikvideo)', 'e-mix': 'Flutung – Ufersicherung (Live-Set)' };
/* ---------- Einwilligung: wie consent-banner-gestaltet, Kategorie „medien“ ---------- */
const SCHLUESSEL = 'flutung.einwilligung', VERSION = 3;
const liesEinw = () => { try { const e = JSON.parse(localStorage.getItem(SCHLUESSEL)); return e && e.version === VERSION ? e : null; } catch (e) { return null; } };
const medienErlaubt = () => !!(liesEinw() || {}).kategorien?.medien;
function setzeEinw(medien) {
try { localStorage.setItem(SCHLUESSEL, JSON.stringify({ version: VERSION, zeit: new Date().toISOString(), kategorien: { notwendig: true, medien } })); } catch (e) {}
document.dispatchEvent(new CustomEvent('einwilligung', { detail: { medien } })); // alle Platzhalter hören darauf
}
/* ---------- Zustand ---------- */
const q = new URLSearchParams(location.search);
// Video-Variante: das Standbild füllt die Breite, das Pult startet eingeklappt, damit es das Bild nicht verdeckt
if (q.get('v') === 'video') document.querySelector('[data-fundus-pult]').setAttribute('data-fundus-pult', 'zu');
const zustand = { v: VARIANTEN[q.get('v')] ? q.get('v') : 'release', anbieter: { 'e-release': 'spotify', 'e-video': 'youtube', 'e-mix': 'soundcloud' }, blockiert: false };
const uhren = new Map();
function anbieterSetzen(fig, id) {
const a = ANBIETER[id]; zustand.anbieter[fig.id] = id;
fig.dataset.anbieter = id;
if (a.h) fig.style.setProperty('--h', `var(${a.h})`);
fig.dataset.src = a.src;
$$('[data-name]', fig).forEach(s => s.textContent = a.name);
$$('[data-extern]', fig).forEach(l => l.href = a.aussen);
$('[data-ziel]', fig).textContent = a.host;
}
/* ---------- Simulierter Player (srcdoc). Im Einsatz: iframe.src = fig.dataset.src ---------- */
const abs = p => new URL(p, location.href).href;
function spieler(fig) {
const a = ANBIETER[zustand.anbieter[fig.id]], video = fig.dataset.art === 'video';
const tracks = $$('.tracks li', fig.closest('.block')).map(li => li.querySelector('span:not(.nr):not(.wer)')?.firstChild?.textContent.trim()).filter(Boolean);
const bild = abs($('.platz img', fig).getAttribute('src'));
const css = `*{box-sizing:border-box;margin:0}html,body{height:100%}body{font:13px/1.4 system-ui,sans-serif;background:#232326;color:#f1f0ec;display:grid;grid-template-rows:auto 1fr auto}
.k{display:flex;justify-content:space-between;gap:8px;padding:8px 12px;font:11px/1.3 ui-monospace,monospace;color:#b9b7b1;border-bottom:1px solid #3a3a3e}
button{font:inherit;color:inherit;background:#34343a;border:1px solid #55555c;border-radius:4px;min-height:32px;padding:0 10px;cursor:pointer}button:focus-visible{outline:2px solid #fff;outline-offset:2px}
.p{display:grid;grid-template-columns:auto 1fr;gap:12px;padding:12px;min-height:0}.p img{width:${video ? '100%' : '96px'};height:${video ? '100%' : '96px'};object-fit:cover;border-radius:4px}
ol{list-style:none;padding:0;overflow:auto;grid-column:1/-1}li button{width:100%;text-align:left;background:none;border:0;border-bottom:1px solid #34343a;border-radius:0;min-height:30px}li button[aria-current]{color:#9fa6ff}
.f{display:flex;align-items:center;gap:10px;padding:8px 12px;border-top:1px solid #3a3a3e}.bar{flex:1;height:4px;background:#45454b}.bar i{display:block;height:100%;width:0;background:#f1f0ec}
.v{position:relative;padding:0;display:block}.v img{position:absolute;inset:0}.v .gross{position:absolute;left:50%;top:50%;translate:-50% -50%;min-height:56px;padding:0 22px;background:rgba(17,17,19,.85)}.cc{position:absolute;left:12px;bottom:12px;background:rgba(17,17,19,.85);padding:4px 8px;border-radius:3px;display:none}.cc.an{display:block}@media (max-width:480px){.k span:last-child{display:none}}`;
const kopf = `<div class="k"><span>Simulierter Player von ${a.name} · Demo, kein Ton</span><span>im Einsatz: ${a.host}</span></div>`;
const mitte = video
? `<div class="p v"><img src="${bild}" alt=""><button class="gross" id="play" aria-label="Abspielen">▶ Abspielen</button><p class="cc" id="cc">[Untertitel] Der Pegel steigt, wir bleiben hier …</p></div>`
: `<div class="p"><img src="${bild}" alt=""><div><b>${TITEL[fig.id].split(' – ')[1]}</b><br><span style="color:#b9b7b1">Flutung</span><br><br><button id="play" aria-label="Abspielen">▶ Abspielen</button></div><ol aria-label="Titel">${tracks.map((t, i) => `<li><button${i ? '' : ' aria-current="true"'}>${i + 1}. ${t}</button></li>`).join('')}</ol></div>`;
const fuss = `<div class="f"><span id="zeit">0:00</span><div class="bar" aria-hidden="true"><i id="pos"></i></div>${video ? '<button id="untertitel" aria-pressed="false">Untertitel</button>' : ''}</div>`;
const js = `const p=document.getElementById('play'),pos=document.getElementById('pos'),z=document.getElementById('zeit'),u=document.getElementById('untertitel');let t=0,l=0;
p.onclick=()=>{l=!l;p.textContent=l?'❚❚ Pause':'▶ Abspielen';p.setAttribute('aria-label',l?'Pause':'Abspielen')};
setInterval(()=>{if(!l)return;t+=1;pos.style.width=Math.min(100,t/2.3)+'%';z.textContent=Math.floor(t/60)+':'+String(t%60).padStart(2,'0')},1000);
if(u)u.onclick=()=>{const an=u.getAttribute('aria-pressed')!=='true';u.setAttribute('aria-pressed',an);document.getElementById('cc').classList.toggle('an',an)};
document.querySelectorAll('li button').forEach(b=>b.onclick=()=>{document.querySelectorAll('li button').forEach(x=>x.removeAttribute('aria-current'));b.setAttribute('aria-current','true')});
window.demoSpielen=()=>{p.click();u&&u.click()};`;
return `<!DOCTYPE html><html lang="de"><head><meta charset="utf-8"><style>${css}</style></head><body>${kopf}${mitte}${fuss}<script>${js}<\/script></body></html>`;
}
/* ---------- Zustände: platzhalter → laedt → geladen | fehler; zurück ohne Neuladen ---------- */
const status = (fig, text) => { $('figcaption .status', fig).innerHTML = text; };
function laden(fig, { fokus = false } = {}) {
const fl = $('.flaeche', fig), a = ANBIETER[zustand.anbieter[fig.id]];
clearTimeout(uhren.get(fig));
$('iframe', fl)?.remove();
fl.dataset.zustand = 'laedt'; status(fig, 'Player wird geladen');
const ifr = document.createElement('iframe');
ifr.title = `${a.name}-Player: ${TITEL[fig.id]}`;
ifr.setAttribute('allow', 'encrypted-media; fullscreen; picture-in-picture; clipboard-write');
ifr.loading = 'lazy';
let fertig = false;
// Zeitüberschreitung: Blocker oder Netz. Im Einsatz 8 s, in der Demo 1,6 s
uhren.set(fig, setTimeout(() => { if (!fertig) fehler(fig, ifr); }, 1600));
if (zustand.blockiert) return; // Demo: blockiert lädt nie
setTimeout(() => { // Demo: 0,96 s Ladezeit (2 Takte à 6 × 80 ms)
if (fl.dataset.zustand !== 'laedt') return;
ifr.addEventListener('load', () => {
fertig = true; clearTimeout(uhren.get(fig));
fl.dataset.zustand = 'geladen';
status(fig, `Geladen von ${a.name} · <button type="button" data-entfernen>Player entfernen</button>`);
if (fokus) ifr.focus(); // Fokus wandert in den Player
}, { once: true });
ifr.srcdoc = spieler(fig); // Im Einsatz: ifr.src = fig.dataset.src;
fl.append(ifr);
}, reduziert() ? 300 : 960);
}
function fehler(fig, ifr) {
const fl = $('.flaeche', fig), a = ANBIETER[zustand.anbieter[fig.id]];
ifr?.remove(); fl.dataset.zustand = 'fehler';
$('.fehler', fl).innerHTML = `<b><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 1.5 15 14H1z" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M8 6v4M8 11.5v1.5" stroke="currentColor" stroke-width="1.6"/></svg>Player nicht geladen</b>
<p>Der Player von ${a.name} ließ sich nicht laden. Vielleicht blockiert ein Browser-Schutz die Verbindung. Hören geht trotzdem direkt bei ${a.name}.</p>
<div class="zeile-knoepfe"><a class="kontur" data-extern href="${a.aussen}" target="_blank" rel="noopener"><span>Direkt bei ${a.name} öffnen</span><span class="sr"> (neuer Tab)</span><svg class="pfeil" viewBox="0 0 12 12" aria-hidden="true"><path d="M3 1.5h7.5V9M10.5 1.5 1.5 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg><svg class="kontur__svg" aria-hidden="true" focusable="false"><rect width="100%" height="100%"/></svg></a>
<button class="kontur" type="button" data-nochmal><span>Nochmal versuchen</span><svg class="kontur__svg" aria-hidden="true" focusable="false"><rect width="100%" height="100%"/></svg></button></div>`;
status(fig, 'Laden fehlgeschlagen');
}
function zurueck(fig, { fokus = false } = {}) {
const fl = $('.flaeche', fig), ifr = $('iframe', fl);
clearTimeout(uhren.get(fig));
const fertig = () => { $('iframe', fl)?.remove(); fl.classList.remove('schliesst'); fl.dataset.zustand = 'platzhalter'; status(fig, ''); $('.kuenftig', fig).checked = medienErlaubt(); if (fokus) $('.laden', fig).focus(); };
if (ifr && !reduziert() && fl.dataset.zustand === 'geladen') { fl.classList.add('schliesst'); setTimeout(fertig, 200); } else fertig();
}
/* ---------- Ereignisse ---------- */
$$('.einbettung').forEach(fig => {
$('.laden', fig).addEventListener('click', () => {
if ($('.kuenftig', fig).checked) setzeEinw(true); // setzt die Kategorie „Medien“ und lädt alle Platzhalter
else laden(fig, { fokus: !html.classList.contains('vorfuehrung') });
});
fig.addEventListener('click', e => {
if (e.target.closest('[data-entfernen]')) { zurueck(fig, { fokus: true }); melde('Player entfernt', `Es werden keine Daten mehr an ${ANBIETER[zustand.anbieter[fig.id]].name} gesendet.`); }
if (e.target.closest('[data-nochmal]')) laden(fig, { fokus: true });
});
});
document.addEventListener('einwilligung', e => {
$$('.einbettung').forEach(fig => {
$('.kuenftig', fig).checked = e.detail.medien;
const z = $('.flaeche', fig).dataset.zustand;
if (e.detail.medien && z !== 'geladen' && z !== 'laedt') laden(fig, { fokus: fig.id === VARIANTEN[zustand.v].fig && !html.classList.contains('vorfuehrung') });
if (!e.detail.medien && z !== 'platzhalter') zurueck(fig);
});
einwText();
});
function einwText() {
const e = liesEinw(), an = !!e?.kategorien?.medien;
const zeit = e ? new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(e.zeit)) : '';
$('#einw-text').innerHTML = an ? `<b class="an">Externe Medien: erlaubt</b> <span>seit ${zeit}. Player laden direkt.</span>` : `<b>Externe Medien: nicht erlaubt</b> <span>Player laden erst nach Klick.</span>`;
$('#widerrufen').hidden = !an;
}
$('#widerrufen').addEventListener('click', () => { setzeEinw(false); melde('Einwilligung widerrufen', 'Alle Player sind wieder Platzhalter. Die Seite wurde nicht neu geladen.'); $('#einw-text').focus(); });
// Demo: Links zu Plattformen nicht öffnen, sondern erklären (im Einsatz entfernen)
document.addEventListener('click', e => {
const a = e.target.closest('a'); if (!a) return;
if (a.target === '_blank') { e.preventDefault(); melde('Öffnet in neuem Tab', a.href.replace('https://', '')); }
else if (a.getAttribute('href')?.startsWith('#') && a.getAttribute('href') !== '#musik') e.preventDefault();
});
let toastUhr;
function melde(t, i) { const el = $('#toast'); $('b', el).textContent = t; $('small', el).textContent = i; el.classList.add('an'); clearTimeout(toastUhr); toastUhr = setTimeout(() => el.classList.remove('an'), 3400); }
/* ---------- Variante und Anbieterwahl ---------- */
function renderWahl() {
const V = VARIANTEN[zustand.v], fs = $('#anbieter-wahl');
$('#wahl-legende').textContent = V.legende;
$$('label', fs).forEach(l => l.remove());
V.wahl.forEach(id => {
const l = document.createElement('label');
l.innerHTML = `<input type="radio" name="anbieter" value="${id}"${zustand.anbieter[V.fig] === id ? ' checked' : ''}>${ANBIETER[id].name}`;
fs.append(l);
});
}
$('#anbieter-wahl').addEventListener('change', e => {
const fig = $('#' + VARIANTEN[zustand.v].fig), war = $('.flaeche', fig).dataset.zustand;
anbieterSetzen(fig, e.target.value);
if (medienErlaubt() || war === 'geladen') laden(fig); else zurueck(fig);
werte();
});
function werte() { const V = VARIANTEN[zustand.v]; $('#werte').innerHTML = `${V.werte} · Player auf <b>300 ms</b> expo.out, zurück <b>200 ms</b> expo.in · Laden <b>steps(6) × 80 ms</b>`; }
function variante(v, schreiben = true) {
zustand.v = VARIANTEN[v] ? v : 'release';
const V = VARIANTEN[zustand.v];
html.dataset.variante = zustand.v;
$$('.block').forEach(b => { b.hidden = b.dataset.block !== zustand.v; });
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === zustand.v)));
$('#kopf-label').textContent = V.label; $('#kopf-titel').textContent = V.titel;
$('#nav').innerHTML = ['Musik', 'Videos', 'Mixe', 'Live', 'Presse'].map(n => `<li${n === V.nav ? ' class="jetzt"' : ''}><a href="#musik"${n === V.nav ? ' aria-current="page"' : ''}>${n}</a></li>`).join('');
renderWahl(); werte();
if (schreiben) { const p = new URLSearchParams(location.search); p.set('v', zustand.v); history.replaceState(null, '', '?' + p.toString().replace(/=(?=&|$)/g, '')); }
}
$$('[data-v]').forEach(b => b.addEventListener('click', () => { stopp(); variante(b.dataset.v); }));
$$('[data-laden]').forEach(b => b.addEventListener('click', () => { zustand.blockiert = b.dataset.laden === 'blockiert'; $$('[data-laden]').forEach(x => x.setAttribute('aria-pressed', String(x === b))); }));
/* ---------- Vorführung ---------- */
const zeiger = $('#zeiger');
let lauf = 0, wecker = [];
const warte = ms => new Promise(ok => { const id = setTimeout(ok, ms); wecker.push(id); });
async function hin(el, l, dx = .5, dy = .55) {
if (!el || l !== lauf) throw 0;
const r = el.getBoundingClientRect();
zeiger.style.transform = `translate(${Math.round(r.left + r.width * dx)}px, ${Math.round(r.top + r.height * dy)}px)`;
await warte(reduziert() ? 200 : 720); if (l !== lauf) throw 0;
}
async function klick(el, l) { await hin(el, l); zeiger.classList.add('klick'); el.classList.add('ist-hover', 'drueckt'); await warte(140); zeiger.classList.remove('klick'); el.classList.remove('drueckt'); el.click(); await warte(60); el.classList.remove('ist-hover'); }
const blockiert = an => { zustand.blockiert = an; $$('[data-laden]').forEach(x => x.setAttribute('aria-pressed', String((x.dataset.laden === 'blockiert') === an))); };
async function imPlayerSpielen(fig, l) {
const ifr = $('iframe', fig); if (!ifr) return;
await hin(ifr, l, .3, .45);
try { ifr.contentWindow.demoSpielen(); } catch (e) {}
}
async function szene(l) {
const V = VARIANTEN[zustand.v], fig = $('#' + V.fig);
zeiger.style.transform = `translate(${innerWidth * .55}px, ${innerHeight * .92}px)`;
await warte(500);
if (zustand.v === 'mix') {
await klick($('.kaestchen', fig), l); await warte(400);
await klick($('.laden', fig), l); await warte(2200);
await imPlayerSpielen(fig, l); await warte(1600);
$('#datenschutz').scrollIntoView({ block: 'center' }); await warte(300);
await klick($('#widerrufen'), l); await warte(1200);
scrollTo(0, 0); await warte(300);
await klick($$('#anbieter-wahl label')[1], l); await warte(1200);
await klick($$('#anbieter-wahl label')[0], l); await warte(900);
return;
}
await klick($('.laden', fig), l); await warte(2100);
await imPlayerSpielen(fig, l); await warte(1900);
await klick($('[data-entfernen]', fig), l); await warte(1200);
await klick($$('#anbieter-wahl label')[1], l); await warte(900);
blockiert(true);
await klick($('.laden', fig), l); await warte(2400);
blockiert(false);
await klick($('[data-nochmal]', fig), l); await warte(2000);
await klick($('[data-entfernen]', fig), l); await warte(700);
await klick($$('#anbieter-wahl label')[0], l); await warte(900);
}
function stopp() { lauf++; wecker.forEach(clearTimeout); wecker = []; html.classList.remove('vorfuehrung'); }
function grundzustand() {
try { localStorage.removeItem(SCHLUESSEL); } catch (e) {}
blockiert(false);
$$('.einbettung').forEach(fig => { anbieterSetzen(fig, VARIANTEN[Object.keys(VARIANTEN).find(k => VARIANTEN[k].fig === fig.id)].wahl[0]); zurueck(fig); $('.kuenftig', fig).checked = false; });
renderWahl(); einwText();
}
function neustart() { stopp(); grundzustand(); variante(zustand.v, false); scrollTo(0, 0); }
async function vorfuehren() {
neustart(); html.classList.add('vorfuehrung'); const l = ++lauf;
try { while (l === lauf) { await szene(l); grundzustand(); await warte(400); } } catch (e) { /* abgebrochen */ }
}
function pause() { lauf++; wecker.forEach(clearTimeout); wecker = []; }
function weiter() { if (html.classList.contains('vorfuehrung')) vorfuehren(); }
// Start: Anbieter setzen; liegt eine Einwilligung vor, laden die Player direkt
$$('.einbettung').forEach(fig => anbieterSetzen(fig, zustand.anbieter[fig.id]));
variante(zustand.v, false); einwText();
if (medienErlaubt() && !q.has('vorschau')) $$('.einbettung').forEach(fig => { $('.kuenftig', fig).checked = true; laden(fig); });
})();
</script>
</body>
</html>