Kartenfächer nach Containerbreite
Sieben Stoffmuster liegen im schmalen Container als Stapel und fächern sich ohne Breakpoint und ohne Skript zu Bogen, Reihe oder Halbkreis auf.
Kollektion, Programm-Highlights, Produktreihe im Kreativstudio oder in der Mode: die Karten öffnen sich wie in der Hand gehalten.
Fünf bis sieben Karten liegen in schmalem Container als leicht versetzter Stapel. Wird der Platz breiter, fächern sie sich stufenlos zu einem Bogen auf, jede mit eigenem Winkel und Abstand. Es gibt keinen Breakpoint und kein Skript: Winkel und Versatz ergeben sich aus der Position der Karte und der Breite des Containers.
Die Komposition reagiert wie ein Objekt auf dem Tisch statt in Stufen umzubrechen. In einer Seitenspalte bleibt sie ruhig, in voller Breite wird sie zur Geste.
Wann einsetzen
Wenige Bildkarten mit Gewicht: Kollektion, Programm-Highlights, Projekte, Produkte einer Reihe. Kreativstudio, Mode, Kultur, D2C.
Wann nicht
Nicht für Inhalte, die man vergleichen oder lesen muss (Texte auf gedrehten Karten). Nicht für mehr als sieben Karten. Nicht in ruhigen Rezepten (Kanzlei, Praxis, Pflege). Nicht zusammen mit einem zweiten verspielten Effekt in derselben Ansicht.
Varianten
- bogen: Winkel von −12° bis +12° über die Reihe
- facher-gerade: nur Versatz, kein Winkel
- kreis: bei sehr breitem Container als Halbkreis
Umsetzung
- Liste mit
container-type: inline-size. Karten absolut oder im Grid übereinander (grid-area: 1/1). --t:calc((100cqi - 20rem) / 40rem), begrenzt mitclamp(0, …, 1), als Auffächer-Grad.--i: calc(sibling-index() - (sibling-count() + 1) / 2) als Position relativ zur Mitte.- rotate:
calc(var(--i) * 6deg * var(--t)); translate:calc(var(--i) * 4rem * var(--t))calc(abs(var(--i)) * 0.6rem *var(--t)). - Fokus und Hover holen die Karte nach vorn (z-index, kleiner Versatz nach oben).
@supports-Fallback: statischer Stapel bzw. Raster. Ohne sibling-index()--iper Inline-Style setzen.
Robust umsetzen
Telefon. Schmaler Container ergibt den ruhigen Stapel. Wischen ist nicht nötig, die Karten stehen als Liste darunter.
Ohne Bewegung. Keine Zeitbewegung. Hover-Anheben ohne Übergang.
Barrierefrei. DOM-Reihenfolge = Lesereihenfolge. Fokus holt die Karte sichtbar nach vorn. Kein Text auf stark gedrehten Karten. Bei 200 % Zoom wird der Container schmaler, das Ergebnis ist der Stapel. Kontrast normal.
Code
<!DOCTYPE html>
<!--
Kartenfächer nach Containerbreite
Fundus · Galerien & Slider · https://cecodesign.de/lab/fundus/galerien-slider/karten-faecher-container/
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>Kartenfächer nach Containerbreite · 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: 'Zalando Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/zalando-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Fragment Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fragment-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; 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; }
/* Werkbank (dunkel) und Rezept mode-beauty-kollektion (im Rahmen) */
:root {
--bank: #151413; --hell: #efebe3; --leise: rgba(239, 235, 227, .68); --linie: rgba(239, 235, 227, .16); --akzent: #c9a27a;
--grund: #f6f3ee; --text: #121010; --grund-2: #ebe7e0; --linie-r: #cecdc6; --text-2: #726D63; --sale: #a3402a;
--ui-ein: 300ms cubic-bezier(0.16, 1, 0.3, 1); /* token-toggle-asymmetrisch: öffnen */
--ui-aus: 195ms cubic-bezier(0.7, 0, 0.84, 0); /* schließen = 300 × 0,65 */
--ui: 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--bank); color: var(--hell); font-family: 'Geist Mono', ui-monospace, monospace; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; padding: clamp(14px, 3vw, 40px); }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* ——— Pult (in der Leiste über der Bühne) ——— */
.pult { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 22px; padding-bottom: 16px; color: var(--leise); }
.wahl { display: flex; flex-wrap: wrap; gap: 3px; padding: 3px; border: 1px solid var(--linie); border-radius: 999px; }
.pult button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 0; border-radius: 999px; padding: 0 12px; min-height: 32px; cursor: pointer; transition: color var(--ui), background-color var(--ui); }
.pult button:hover { color: var(--hell); }
.pult button[aria-pressed="true"] { background: var(--hell); color: var(--bank); }
.regler { display: flex; align-items: center; gap: 10px; }
.regler input { width: clamp(110px, 16vw, 200px); accent-color: var(--akzent); }
.regler output { min-width: 4.6em; color: var(--hell); font-variant-numeric: tabular-nums; }
.kopf { display: flex; flex-wrap: wrap; gap: 8px 24px; margin-bottom: 10px; color: var(--leise); }
.kopf b { color: var(--hell); font-weight: 500; font-variant-numeric: tabular-nums; }
.kopf .t b { color: var(--akzent); }
.stand i { font-style: normal; color: #8fd0a4; }
.stand i.nein { color: #f0a383; }
/* ——— Bühne mit ziehbarem Rahmen ——— */
.buehne { position: relative; }
.halter { position: relative; width: max-content; transform-origin: 0 0; }
.rahmen { container: seite / inline-size; position: relative; width: 1240px; min-width: 320px; background: var(--grund); color: var(--text); border-radius: 3px; overflow: clip; box-shadow: 0 30px 80px -30px rgba(0, 0, 0, .8); font: 400 1rem/1.5 'Zalando Sans', system-ui, sans-serif; }
.griff { position: absolute; top: 200px; right: -16px; z-index: 3; width: 10px; height: 64px; border-radius: 6px; background: var(--hell); cursor: ew-resize; touch-action: none; box-shadow: 0 0 0 4px var(--bank); transition: scale var(--ui), background-color var(--ui); }
.griff::before { content: ""; position: absolute; inset: 22px 3px; border-block: 1px solid rgba(20, 20, 19, .5); }
.griff::after { content: ""; position: absolute; inset: -12px -14px; }
.griff:hover, .griff.zieht, .vorfuehrung .griff { background: var(--akzent); scale: 1.15; }
/* ——— Seite im Rahmen: Weberei Kettbaum ——— */
.r-nav { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 20px clamp(16px, 3.5vw, 44px); border-bottom: 1px solid var(--linie-r); }
.r-marke { font-weight: 500; font-stretch: 125%; font-size: 1.125rem; letter-spacing: .02em; text-transform: uppercase; }
.etikett { font: 400 .75rem/1.4 'Fragment Mono', ui-monospace, monospace; letter-spacing: .06em; text-transform: uppercase; }
.r-links { display: flex; gap: 1.6em; color: var(--text-2); }
.r-kopf { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: end; gap: 18px 40px; padding: 40px clamp(16px, 3.5vw, 44px) 0; }
.r-kopf h2 { font-weight: 400; font-stretch: 125%; font-size: clamp(2rem, 4.2vw, 4.25rem); line-height: 1.05; letter-spacing: -.01em; max-width: 15ch; margin-top: 10px; text-wrap: balance; }
.r-kopf p { max-width: 34ch; color: var(--text-2); }
/* =====================================================================
DAS MUSTER: Fächer aus Position (sibling-index) und Containerbreite (typisierte Division)
===================================================================== */
.faecher-feld { container-type: inline-size; padding: 28px clamp(16px, 3.5vw, 44px) 12px; }
.faecher { --kb: min(13rem, 60cqi); --t: 0; --tk: 0; list-style: none; display: grid; place-items: center; min-height: calc(var(--kb) * 1.45 + 5rem); padding-block: 2rem 2.4rem; }
.karte {
grid-area: 1 / 1; width: var(--kb);
/* Rückfall: Index aus dem style-Attribut (--idx), Hälfte aus dem Elternteil (--halb) */
--i: var(--idx, 0);
--h: var(--halb, 3);
--u: calc(var(--i) / var(--h)); /* -1 … 1 */
}
@supports (width: calc(sibling-index() * 1px)) {
:root:not(.ohne-index) .karte { --i: calc(sibling-index() - (sibling-count() + 1) / 2); --h: calc((sibling-count() - 1) / 2); }
}
/* Auffächer-Grad: Länge durch Länge ergibt eine Zahl. 0 bei 22rem Container, 1 ab 60rem */
@supports (width: calc(100cqi / 1rem * 1px)) {
:root:not(.ohne-arithmetik) .faecher { --t: clamp(0, (100cqi - 22rem) / 38rem, 1); --tk: clamp(0, (100cqi - 26rem) / 34rem, 1); }
}
/* Rückfall ohne typisierte Division: Stufen per Container-Query */
@container (width >= 40rem) { :root.ohne-arithmetik .faecher { --t: .5; --tk: .5; } }
@container (width >= 60rem) { :root.ohne-arithmetik .faecher { --t: 1; --tk: 1; } }
@supports not (width: calc(100cqi / 1rem * 1px)) {
@container (width >= 40rem) { .faecher { --t: .5; --tk: .5; } }
@container (width >= 60rem) { .faecher { --t: 1; --tk: 1; } }
}
/* Variante bogen: Winkel bis ±12°, äußere Karten sinken auf einem Bogen */
.karte {
--breite: min(26rem, (100cqi - var(--kb)) / 2 - .5rem);
translate: calc(var(--u) * var(--t) * var(--breite) + var(--i) * .45rem * (1 - var(--t)))
calc(var(--u) * var(--u) * var(--t) * 2.6rem + var(--i) * -.25rem * (1 - var(--t)));
rotate: calc(var(--u) * (12deg * var(--t) + 3deg * (1 - var(--t))));
transform-origin: 50% 90%;
}
/* Variante facher-gerade: nur Versatz, kein Winkel */
[data-v="facher-gerade"] .karte {
--breite: min(30rem, (100cqi - var(--kb)) / 2 - .5rem);
translate: calc(var(--u) * var(--t) * var(--breite) + var(--i) * .45rem * (1 - var(--t))) calc(var(--i) * -.25rem * (1 - var(--t)));
rotate: none;
}
/* Variante kreis: Drehpunkt unter der Karte, bei voller Breite ein Halbkreis (±80°) */
[data-v="kreis"] .faecher { align-items: start; padding-block: 2.5rem calc(2.4rem + var(--kb) * 1.1 * var(--tk)); }
[data-v="kreis"] .karte {
translate: 0 calc(var(--i) * -.25rem * (1 - var(--tk)));
rotate: calc(var(--u) * (80deg * var(--tk) + 3deg * (1 - var(--tk))));
transform-origin: 50% calc(100% + var(--kb) * .62);
}
/* Karte */
.karte a { display: block; text-decoration: none; background: #fbfaf7; padding: 10px 10px 12px; border-radius: 2px;
box-shadow: 0 1px 0 rgba(18, 16, 16, .06), 0 14px 30px -14px rgba(18, 16, 16, .45);
transition: transform var(--ui-aus), box-shadow var(--ui-aus); }
.muster { display: block; aspect-ratio: 4 / 5; position: relative;
/* Zackenschere an der Unterkante */
clip-path: polygon(0 0, 100% 0, 100% 97%, 97.5% 100%, 95% 97%, 92.5% 100%, 90% 97%, 87.5% 100%, 85% 97%, 82.5% 100%, 80% 97%, 77.5% 100%, 75% 97%, 72.5% 100%, 70% 97%, 67.5% 100%, 65% 97%, 62.5% 100%, 60% 97%, 57.5% 100%, 55% 97%, 52.5% 100%, 50% 97%, 47.5% 100%, 45% 97%, 42.5% 100%, 40% 97%, 37.5% 100%, 35% 97%, 32.5% 100%, 30% 97%, 27.5% 100%, 25% 97%, 22.5% 100%, 20% 97%, 17.5% 100%, 15% 97%, 12.5% 100%, 10% 97%, 7.5% 100%, 5% 97%, 2.5% 100%, 0 97%); }
.muster::after { content: ""; position: absolute; inset: 0; opacity: .5; mix-blend-mode: soft-light;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='k'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23k)'/%3E%3C/svg%3E"); }
.karte-text { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-top: 10px; }
.karte-text b { font-weight: 500; font-stretch: 112%; font-size: 1rem; }
.karte-text .etikett { color: var(--text-2); font-size: .6875rem; }
/* Fokus und Zeiger holen die Karte nach vorn: öffnen 300 ms, zurück 195 ms */
.karte:has(a:hover), .karte:has(a:focus-visible), .karte.gezeigt { z-index: 20; }
.karte a:hover, .karte a:focus-visible, .karte.gezeigt a { transform: translateY(-1.4rem) scale(1.04); box-shadow: 0 2px 0 rgba(18, 16, 16, .06), 0 26px 44px -18px rgba(18, 16, 16, .55); transition: transform var(--ui-ein), box-shadow var(--ui-ein); }
.karte a:focus-visible { outline: 2px solid var(--text); outline-offset: 4px; }
/* Stoffe: eigene CSS- und SVG-Muster */
.m-fisch { background: #6b5444 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='10'%3E%3Cg stroke-width='2.4' stroke-linecap='square'%3E%3Cpath d='M0 10L8 0M-4 5L4-5M4 15L12 5' stroke='%23a48a72'/%3E%3Cpath d='M8 0L16 10M4-5L12 5M12 5L20 15' stroke='%233e2f25'/%3E%3C/g%3E%3C/svg%3E") 0 0 / 12px 7.5px; }
.m-cord { background: repeating-linear-gradient(90deg, #8e3f22 0 3px, #b85a33 3px 6px, #6e2e17 6px 8px); }
.m-boucle { background: radial-gradient(circle at 30% 30%, #fffdf6 0 1.6px, transparent 2.2px) 0 0 / 7px 7px, radial-gradient(circle at 60% 70%, #d9d2c3 0 1.8px, transparent 2.4px) 3px 2px / 9px 8px, #ece6d9; }
.m-moire { background: repeating-radial-gradient(circle at 18% 8%, rgba(160, 175, 230, .2) 0 1.4px, transparent 1.4px 3.4px), repeating-radial-gradient(circle at 88% 104%, rgba(160, 175, 230, .2) 0 1.4px, transparent 1.4px 3.55px), linear-gradient(160deg, #24305a, #161d38); }
.m-tweed { background: radial-gradient(circle, #c6a34a 0 .9px, transparent 1.2px) 0 0 / 11px 13px, radial-gradient(circle, #a0453a 0 .8px, transparent 1.1px) 5px 7px / 17px 15px, repeating-linear-gradient(45deg, #3f5135 0 2px, #51653f 2px 4px); }
.m-satin { background: linear-gradient(118deg, #3b1530 0%, #6f2d58 34%, #b3739a 46%, #6f2d58 58%, #42183a 100%); }
.m-glen { background: repeating-linear-gradient(90deg, rgba(240, 236, 228, .5) 0 1px, transparent 1px 4px, rgba(240, 236, 228, .5) 4px 5px, transparent 5px 18px), repeating-linear-gradient(0deg, rgba(20, 22, 26, .45) 0 2px, transparent 2px 6px, rgba(20, 22, 26, .45) 6px 8px, transparent 8px 18px), #6e7277; }
/* Liste unter dem Fächer: die lesbaren Angaben */
.stoffe { padding: 8px clamp(16px, 3.5vw, 44px) 36px; }
.stoffe table { width: 100%; border-collapse: collapse; font-size: .9375rem; }
.stoffe th, .stoffe td { text-align: left; padding: 10px 12px 10px 0; border-bottom: 1px solid var(--linie-r); font-weight: 400; vertical-align: baseline; }
.stoffe thead th { color: var(--text-2); padding-top: 0; }
.stoffe td.z, .stoffe th.z { text-align: right; padding-right: 0; font-variant-numeric: tabular-nums; }
.stoffe tbody th { font-weight: 500; }
.stoffe tr:target { background: var(--grund-2); }
.stoffe .nr { font-family: 'Fragment Mono', ui-monospace, monospace; font-size: .8125rem; color: var(--text-2); }
.preis-hinweis { margin-top: 12px; color: var(--text-2); font-size: .8125rem; }
@container seite (width < 44rem) {
.stoffe .mat, .stoffe .gew { display: none; }
}
@container seite (width < 30rem) {
.r-links span:not(:last-child) { display: none; }
.stoffe .bind { display: none; }
}
/* Werte unter der Bühne */
.werte { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: 6px 32px; margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--linie); color: var(--leise); font: 400 11.5px/1.6 'Geist Mono', ui-monospace, monospace; }
.werte div { display: flex; gap: 12px; min-width: 0; }
.werte dt { color: var(--hell); flex: none; min-width: 8.5em; }
.werte dd { overflow-wrap: anywhere; }
.vorschau body { overflow: hidden; padding: 22px 26px; }
.vorschau .pult, .vorschau .werte, .vorschau .kopf .hilfe { display: none; }
.vorschau .r-kopf, .vorschau .stoffe { display: none; }
.vorschau .faecher-feld { padding-top: 48px; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (hover: none) {
.griff { width: 14px; right: -18px; }
.griff:hover { background: var(--hell); scale: 1; }
}
@media (max-width: 640px) {
.wahl { width: 100%; }
.wahl button { flex: 1; padding: 0 6px; letter-spacing: .06em; }
.regler { width: 100%; }
.regler input { flex: 1; width: auto; }
.werte dt { min-width: 7em; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.karte a { border: 1px solid CanvasText; }
}
</style>
</head>
<body>
<div class="pult mono">
<div class="wahl" role="group" aria-label="Variante">
<button type="button" data-v-wahl="bogen" aria-pressed="true">Bogen</button>
<button type="button" data-v-wahl="facher-gerade" aria-pressed="false">Gerade</button>
<button type="button" data-v-wahl="kreis" aria-pressed="false">Kreis</button>
</div>
<label class="regler">Breite <input type="range" min="320" max="1240" step="1" value="1240" data-regler aria-label="Breite des Containers in Pixel"><output data-ausgabe>1240 px</output></label>
<div class="wahl" role="group" aria-label="Rechenweg">
<button type="button" data-weg="live" aria-pressed="true">Live</button>
<button type="button" data-weg="index" aria-pressed="false">Inline-Index</button>
<button type="button" data-weg="stufen" aria-pressed="false">Stufen</button>
</div>
</div>
<div class="kopf mono">
<span>Container <b data-breite>1240</b> px</span>
<span class="t">Auffächer-Grad <span data-t-name>--t</span> <b data-t>1,00</b></span>
<span class="stand" data-stand></span>
<span class="hilfe">Griff rechts ziehen</span>
</div>
<div class="buehne" data-buehne>
<div class="halter" data-halter>
<div class="rahmen" data-rahmen>
<header class="r-nav">
<span class="r-marke">Kettbaum</span>
<span class="r-links etikett"><span>Stoffe</span><span>Weberei</span><span>Musterdienst</span></span>
</header>
<section aria-labelledby="h-kollektion">
<div class="r-kopf">
<div>
<p class="etikett">Musterbuch H/W 2026 · Mönchengladbach</p>
<h2 id="h-kollektion">Sieben Stoffe für einen langen Winter</h2>
</div>
<p>Gewebt auf vier Webstühlen im Hinterhof. Muster bis 20 × 25 cm schicken wir kostenlos, ab einem Meter schneiden wir zu.</p>
</div>
<div class="faecher-feld" data-feld>
<ol class="faecher" style="--halb: 3" aria-label="Stoffmuster, Pfeil- und Tabtaste wechseln">
<li class="karte" style="--idx: -3"><a href="#stoff-214"><span class="muster m-fisch" aria-hidden="true"></span><span class="karte-text"><b>Torf</b><span class="etikett">Nr. 214</span></span></a></li>
<li class="karte" style="--idx: -2"><a href="#stoff-188"><span class="muster m-cord" aria-hidden="true"></span><span class="karte-text"><b>Rost</b><span class="etikett">Nr. 188</span></span></a></li>
<li class="karte" style="--idx: -1"><a href="#stoff-231"><span class="muster m-boucle" aria-hidden="true"></span><span class="karte-text"><b>Kreide</b><span class="etikett">Nr. 231</span></span></a></li>
<li class="karte" style="--idx: 0"><a href="#stoff-207"><span class="muster m-moire" aria-hidden="true"></span><span class="karte-text"><b>Tinte</b><span class="etikett">Nr. 207</span></span></a></li>
<li class="karte" style="--idx: 1"><a href="#stoff-242"><span class="muster m-tweed" aria-hidden="true"></span><span class="karte-text"><b>Moos</b><span class="etikett">Nr. 242</span></span></a></li>
<li class="karte" style="--idx: 2"><a href="#stoff-196"><span class="muster m-satin" aria-hidden="true"></span><span class="karte-text"><b>Pflaume</b><span class="etikett">Nr. 196</span></span></a></li>
<li class="karte" style="--idx: 3"><a href="#stoff-225"><span class="muster m-glen" aria-hidden="true"></span><span class="karte-text"><b>Schiefer</b><span class="etikett">Nr. 225</span></span></a></li>
</ol>
</div>
<div class="stoffe">
<table>
<caption class="nur-sr">Stoffe der Kollektion Herbst/Winter 2026 mit Bindung, Material, Gewicht und Meterpreis</caption>
<thead><tr><th scope="col" class="etikett">Nr.</th><th scope="col" class="etikett">Stoff</th><th scope="col" class="etikett bind">Bindung</th><th scope="col" class="etikett mat">Material</th><th scope="col" class="etikett z gew">g/m²</th><th scope="col" class="etikett z">€/m</th></tr></thead>
<tbody>
<tr id="stoff-214"><td class="nr">214</td><th scope="row">Torf</th><td class="bind">Fischgrat</td><td class="mat">Schurwolle 100 %</td><td class="z gew">420</td><td class="z">38,00</td></tr>
<tr id="stoff-188"><td class="nr">188</td><th scope="row">Rost</th><td class="bind">Feincord</td><td class="mat">Baumwolle 98 %, Elasthan 2 %</td><td class="z gew">310</td><td class="z">24,50</td></tr>
<tr id="stoff-231"><td class="nr">231</td><th scope="row">Kreide</th><td class="bind">Bouclé</td><td class="mat">Wolle 70 %, Alpaka 30 %</td><td class="z gew">520</td><td class="z">46,00</td></tr>
<tr id="stoff-207"><td class="nr">207</td><th scope="row">Tinte</th><td class="bind">Moiré</td><td class="mat">Viskose 100 %</td><td class="z gew">180</td><td class="z">29,00</td></tr>
<tr id="stoff-242"><td class="nr">242</td><th scope="row">Moos</th><td class="bind">Donegal-Tweed</td><td class="mat">Schurwolle 100 %</td><td class="z gew">460</td><td class="z">41,50</td></tr>
<tr id="stoff-196"><td class="nr">196</td><th scope="row">Pflaume</th><td class="bind">Duchesse-Satin</td><td class="mat">Seide 100 %</td><td class="z gew">140</td><td class="z">64,00</td></tr>
<tr id="stoff-225"><td class="nr">225</td><th scope="row">Schiefer</th><td class="bind">Glencheck</td><td class="mat">Schurwolle 100 %</td><td class="z gew">390</td><td class="z">36,00</td></tr>
</tbody>
</table>
<p class="preis-hinweis">Preise je laufendem Meter inkl. MwSt., Stoffbreite 145 cm, zzgl. Versand.</p>
</div>
</section>
</div>
<div class="griff" data-griff role="presentation" title="Ziehen, um die Breite zu ändern"></div>
</div>
</div>
<dl class="werte" data-werte></dl>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const buehne = $('[data-buehne]'), halter = $('[data-halter]'), rahmen = $('[data-rahmen]'), griff = $('[data-griff]');
const regler = $('[data-regler]'), ausgabe = $('[data-ausgabe]'), breiteText = $('[data-breite]'), tText = $('[data-t]');
const stand = $('[data-stand]'), werteListe = $('[data-werte]'), karten = $$('.karte');
const MIN = 320, MAX = 1240;
const kannIndex = CSS.supports('width', 'calc(sibling-index() * 1px)');
const kannTeilen = CSS.supports('width', 'calc(100cqi / 1rem * 1px)');
const IDS = ['bogen', 'facher-gerade', 'kreis'];
const WERTE = {
bogen: [
['Grad', '--t: clamp(0, (100cqi - 22rem) / 38rem, 1) · 0 bis 352 px, 1 ab 960 px Container'],
['Position', '--i: calc(sibling-index() - (sibling-count() + 1) / 2) · --u: calc(var(--i) / var(--h)), --h = (sibling-count() - 1) / 2'],
['Versatz x', 'var(--u) × var(--t) × min(26rem, (100cqi − Kartenbreite) / 2 − .5rem) + var(--i) × .45rem × (1 − t)'],
['Versatz y', 'var(--u)² × var(--t) × 2.6rem (Bogen) − var(--i) × .25rem × (1 − t) (Stapel)'],
['Winkel', 'var(--u) × (12deg × t + 3deg × (1 − t)) · ±12° offen, ±3° im Stapel, Drehpunkt 50% 90%'],
['Karte', 'Breite min(13rem, 60cqi), Seitenverhältnis Muster 4 : 5'],
['Fokus', 'translateY(-1.4rem) scale(1.04), z-index 20; vor 300 ms cubic-bezier(0.16, 1, 0.3, 1), zurück 195 ms cubic-bezier(0.7, 0, 0.84, 0)'],
['Rückfall', 'ohne sibling-index(): --idx im style-Attribut, --halb am Elternteil · ohne Division: --t in Stufen 0 / .5 / 1 per @container (40rem, 60rem)']
],
'facher-gerade': [
['Grad', '--t: clamp(0, (100cqi - 22rem) / 38rem, 1)'],
['Versatz x', 'var(--u) × var(--t) × min(30rem, (100cqi − Kartenbreite) / 2 − .5rem)'],
['Winkel', 'keiner (rotate: none); Stapel nur über Versatz'],
['Wann', 'ruhiger, Texte bleiben gerade lesbar']
],
kreis: [
['Grad', '--tk: clamp(0, (100cqi - 26rem) / 34rem, 1) · 1 ab 960 px Container'],
['Winkel', 'var(--u) × (80deg × tk + 3deg × (1 − tk)) · ±80° = fast ein Halbkreis'],
['Drehpunkt', 'transform-origin: 50% calc(100% + Kartenbreite × .62), also unter der Karte'],
['Bühne', 'Karten oben ausgerichtet, Polster unten calc(2.4rem + Kartenbreite × 1.1 × tk): die seitlichen Karten brauchen Platz unter dem Drehpunkt']
]
};
let v = new URLSearchParams(location.search).get('v');
if (!IDS.includes(v)) v = IDS[0];
let breite = MAX, massstab = 1;
function platz() { return buehne.clientWidth - 20; }
function grenze() { const p = platz(); return p >= 700 ? Math.max(MIN, Math.min(MAX, p)) : MAX; }
function setzeBreite(w) {
breite = Math.round(Math.max(MIN, Math.min(grenze(), w)));
rahmen.style.width = breite + 'px';
massstab = Math.min(1, platz() / breite);
halter.style.transform = massstab < 1 ? `scale(${massstab})` : '';
halter.style.height = massstab < 1 ? rahmen.offsetHeight * massstab + 'px' : '';
breiteText.textContent = breite;
regler.max = grenze(); regler.value = breite; ausgabe.textContent = breite + ' px';
// --t so anzeigen, wie CSS ihn rechnet (gleiche Formel, Container = Feld ohne Polster)
const feld = $('[data-feld]'), cs = getComputedStyle(feld), rem = parseFloat(getComputedStyle(html).fontSize);
const innen = feld.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight);
let t = v === 'kreis' ? (innen - 26 * rem) / (34 * rem) : (innen - 22 * rem) / (38 * rem);
t = Math.max(0, Math.min(1, t));
if (html.classList.contains('ohne-arithmetik') || !kannTeilen) t = innen >= 60 * rem ? 1 : innen >= 40 * rem ? .5 : 0;
tText.textContent = t.toFixed(2).replace('.', ',');
}
function zeigeStand() {
const ok = x => x ? '<i>läuft</i>' : '<i class="nein">fehlt</i>';
stand.innerHTML = `sibling-index() ${ok(kannIndex)} · Länge ÷ Länge ${ok(kannTeilen)}`;
}
function setze(neu, url) {
v = neu;
html.dataset.v = neu;
$$('[data-v-wahl]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === neu)));
if (url) {
const q = new URLSearchParams(location.search); q.set('v', neu);
history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + location.hash);
}
$('[data-t-name]').textContent = neu === 'kreis' ? '--tk' : '--t';
werteListe.innerHTML = WERTE[neu].map(([a, b]) => `<div><dt>${a}</dt><dd>${b}</dd></div>`).join('');
setzeBreite(breite);
}
$$('[data-v-wahl]').forEach(b => b.addEventListener('click', () => { stopp(); setze(b.dataset.vWahl, true); }));
$$('[data-weg]').forEach(b => {
if (b.dataset.weg === 'live' && !(kannIndex && kannTeilen)) b.textContent = 'Live (teilweise)';
b.addEventListener('click', () => {
stopp();
html.classList.toggle('ohne-index', b.dataset.weg === 'index');
html.classList.toggle('ohne-arithmetik', b.dataset.weg === 'stufen');
$$('[data-weg]').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
setzeBreite(breite);
});
});
// Pfeiltasten wandern durch die Karten (Tab geht ohnehin der Reihe nach)
$('.faecher').addEventListener('keydown', e => {
if (!['ArrowLeft', 'ArrowRight'].includes(e.key)) return;
const links = $$('.karte a'), i = links.indexOf(document.activeElement);
if (i < 0) return;
e.preventDefault();
links[(i + (e.key === 'ArrowRight' ? 1 : -1) + links.length) % links.length].focus();
});
regler.addEventListener('input', () => { stopp(); setzeBreite(+regler.value); });
let zug = null;
griff.addEventListener('pointerdown', e => { stopp(); zug = { x: e.clientX, w: breite }; griff.setPointerCapture(e.pointerId); griff.classList.add('zieht'); });
griff.addEventListener('pointermove', e => { if (zug) setzeBreite(zug.w + (e.clientX - zug.x) / massstab); });
const los = () => { zug = null; griff.classList.remove('zieht'); };
griff.addEventListener('pointerup', los); griff.addEventListener('pointercancel', los);
new ResizeObserver(() => setzeBreite(breite)).observe(buehne);
// Vorführung: Container atmet zwischen Telefon- und voller Breite; oben zeigt eine Karte nach der anderen auf
let uhr = null, tick = null, start = 0, versatz = 0;
const RUNDE = 9000, UNTEN = 360;
function stopp() { cancelAnimationFrame(uhr); clearTimeout(tick); uhr = tick = null; html.classList.remove('vorfuehrung'); karten.forEach(k => k.classList.remove('gezeigt')); }
function neustart() { stopp(); versatz = 0; setzeBreite(grenze()); }
function vorfuehren() {
stopp();
html.classList.add('vorfuehrung');
if (reduziert()) {
const stufen = [grenze(), 760, 380]; let i = 0;
const schalten = () => { setzeBreite(stufen[i % stufen.length]); karten.forEach((k, j) => k.classList.toggle('gezeigt', j === (i * 2) % karten.length)); i++; tick = setTimeout(schalten, 2200); };
schalten();
return;
}
start = performance.now() - versatz;
const schritt = t => {
versatz = (t - start) % RUNDE;
const p = versatz / RUNDE, oben = grenze();
setzeBreite(UNTEN + (oben - UNTEN) * (0.5 + 0.5 * Math.cos(p * 2 * Math.PI)));
// in der weiten Phase (Anfang und Ende der Runde) hebt sich je eine Karte
const weit = p < .22 || p > .78;
const k = Math.floor(((p + .22) % 1) / .44 * karten.length * 2) % karten.length;
karten.forEach((el, j) => el.classList.toggle('gezeigt', weit && j === k));
uhr = requestAnimationFrame(schritt);
};
uhr = requestAnimationFrame(schritt);
}
function pause() { cancelAnimationFrame(uhr); clearTimeout(tick); uhr = tick = null; }
function weiter() { vorfuehren(); }
zeigeStand();
setze(v, false);
if (platz() < 700) setzeBreite(Math.max(MIN, platz()));
})();
</script>
</body>
</html>