/* PrintSupport — wiederverwendbare Bausteine: Buttons, Chips, Karten, Sheets,
   Badges, Ladeoverlay, Kennwert-Balken und Sterne. */

/* ---------- Kicker: die kleine Beschriftung über einer Überschrift -----

   „PREISVERGLEICH · DACH-RAUM", „FILAMENT", „SO ENTSTEHT DER PREIS PRO
   KILOGRAMM", „VERTRAUEN" — in den Vorlagen steht über jeder Überschrift
   und über jedem Kartenabschnitt eine Zeile in Versalien, gesperrt, in
   Schreibmaschinenschrift und gedämpft. Sie ordnet ein, bevor die
   Überschrift benennt.

   ⛔ SIE IST EIN BAUSTEIN, KEINE SCHREIBWEISE. Die Versalien entstehen über
   `text-transform`, im Markup steht der Text gemischt geschrieben. Grund:
   Vorlesehilfen buchstabieren durchgehende Großschreibung teilweise
   („P-L-A"), und der Text muss auch als gewöhnlicher Satz lesbar bleiben,
   wenn die Regel einmal nicht greift.

   ⛔ Was hier steht, DARF NIE EINE EINHEIT FESTSCHREIBEN. „PREIS PRO
   KILOGRAMM" ist auf einer Harzseite falsch; die Zeile kommt in solchen
   Fällen aus PD.util.vergleichsEinheitLang, nicht aus einem Literal
   (⛔-Regel des Projekts, siehe CLAUDE.md).

   ⚠️ Die Sperrung ist bei Versalien nötig, nicht Geschmack: Großbuchstaben
   haben gleichmäßige Seitenprofile und laufen ohne sie zusammen. 0,12em ist
   der Wert der Vorlagen. */
.kicker {
  display: block;
  font-family: var(--schrift-mono);
  font-size: var(--label-medium);
  font-weight: 500;
  line-height: var(--zeile-normal);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--on-surface-variant);
}

/* ---------- Buttons (M3: gefüllt / umrandet / Text) ---------- */

.knopf {
  position: relative;              /* Bezug für die Zustandsebene */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--abstand-s);
  min-height: 40px;
  padding: 0 var(--abstand-l);
  border-radius: var(--radius-pille);
  border: 0;
  background: var(--primary);
  color: var(--on-primary);
  font-size: var(--label-large);
  font-weight: 500;
  cursor: pointer;
  text-decoration: none;
  transition: background-color var(--dauer-kurz) ease;
}
/* Zustandsebene statt Farbwechsel.

   Alle übrigen Bedienelemente sitzen auf einer bekannten Fläche und gehen im
   Hover eine Flächenstufe höher. Der gefüllte Knopf kann das nicht: Seine
   Fläche IST die Markenfarbe, eine „Stufe höher" gibt es dort nicht. Vorher
   half ein filter: brightness(1.08) — als einziges Element der Seite, und in
   beiden Themes mit anderem Ergebnis.

   Stattdessen legt sich die eigene Vordergrundfarbe halbdurchsichtig über die
   Fläche (das Zustandsebenen-Muster aus Material 3). Das funktioniert für alle
   drei Knopfarten mit derselben Regel, weil currentColor jeweils mitwandert,
   und braucht keinen einzigen zusätzlichen Farbwert. */
.knopf::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
  transition: opacity var(--dauer-kurz) ease;
  pointer-events: none;
}
.knopf:hover::after { opacity: 0.08; }
.knopf:active::after { opacity: 0.12; }
.knopf:disabled::after { opacity: 0; }

/* ⚠️ Ohne diese Regel hat ein Knopfsymbol GAR KEINE Größe.

   Ein <svg> mit viewBox, aber ohne width/height besitzt keine Eigengröße;
   seine Breite entsteht erst aus dem Flex-Algorithmus. Bis zum 05.08.2026
   stand hier nichts, und das Ergebnis war je nach Nachbarschaft ein anderes:
   Neben einer Beschriftung drückte der Text das Symbol zusammen — gemessen
   10,5 × 10,5px am Sortierknopf, also kein Symbol mehr, sondern ein Fleck.
   Und weil das Symbol dabei dennoch Platz beanspruchte, brach ausgerechnet
   die Beschriftung um: „Sortieren" und „↑" standen untereinander, der Knopf
   war 47 statt 40px hoch. In einem Knopf OHNE Beschriftung wuchs dasselbe
   Symbol umgekehrt auf die volle Knopffläche.

   20px ist die Größe, die auch das Druckersymbol der Kopfleiste trägt —
   damit sind die Symbole der Seite endlich alle gleich groß.

   flex: 0 0 auto gehört dazu: Sonst holt sich der Flex-Algorithmus die
   fehlenden Pixel wieder beim Symbol, sobald es eng wird. */
.knopf svg {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
}

.knopf--umrandet {
  background: none;
  color: var(--primary);
  border: 1px solid var(--outline);
}

.knopf--text {
  background: none;
  color: var(--primary);
  padding: 0 var(--abstand-m);
}

/* ---------- Warum der Filterknopf KEINE Sonderform mehr hat ----------

   Vom 05.08.2026 bis zur Korrektur am selben Tag war er auf dem Handy ein
   rundes Symbol ohne Beschriftung, mit der Filterzahl als Marke. Das sparte
   eine Zeile, kostete aber die Verständlichkeit: Direkt daneben steht
   „Sortieren" als WORT. Wer beides sieht, rätselt nur bei einem — und die
   Marke hilft ausgerechnet dann nicht, wenn noch kein Filter gesetzt ist.

   Er trägt deshalb wieder seine Beschriftung, und die Zählmarke ist ersatzlos
   entfallen: Die Zahl steht als „(1)" im Text selbst, ein zweites Zeichen
   dafür wäre dieselbe Aussage zweimal. Damit brauchte es auch die
   unsichtbare Beschriftung nicht mehr — der zugängliche Name ist wieder
   einfach der sichtbare Text.

   Der Preis steht in js/seite-liste.js bei filterKnopfInhalt. */

.knopf--voll { width: 100%; }

/* Kauf-Abschluss: der einzige Knopf der Seite, der zu einem fremden Shop
   führt. Die Zustandsebene oben braucht dafür keine Sonderregel — sie ist
   currentColor und wandert mit der Schriftfarbe mit. */
.knopf--kauf { background: var(--kauf); color: var(--on-kauf); }

/* Nicht lieferbar: grau und leise, aber weiterhin anklickbar. Der Shop kann
   Restbestände führen oder ein Lieferdatum nennen — ein `disabled` nähme dem
   Besucher diesen Weg, obwohl PrintSupport nur den letzten bekannten Stand
   kennt. Deshalb wird hier ausschließlich die Farbe zurückgenommen; die
   Aussage selbst steht als Text darüber („Nicht lieferbar"), nie nur im
   Farbton.

   ⚠️ Die Umrandung ist nicht Zierde. Die gedämpfte Fläche liegt nur 1,11:1
   (hell) bzw. 1,28:1 (dunkel) neben der Kartenfläche der Kaufbox — der Knopf
   hätte ohne Kante keine erkennbare Form mehr, obwohl er anklickbar bleibt.
   Dieselbe Falle wie bei --surface-container-lowest neben --background. Mit
   --outline steht die Kante bei 3,8:1 hell und 5,5:1 dunkel, also über den
   geforderten 3:1 für Bedienelemente. */
.knopf--kauf-aus {
  background: var(--kauf-aus);
  color: var(--on-kauf-aus);
  border: 1px solid var(--outline);
}

/* ---------- Filter-Chips (M3 FilterChip) ---------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--abstand-xs);
  min-height: 32px;
  padding: 0 var(--abstand-m);
  border-radius: var(--radius-s);
  border: 1px solid var(--outline);
  background: none;
  color: var(--on-surface-variant);
  font-size: var(--label-large);
  cursor: pointer;
  transition: background-color var(--dauer-kurz) ease, border-color var(--dauer-kurz) ease;
}
/* Zustandsebene statt Flächenstufe — der Chip hat keine eigene Fläche
   (`background: none`), also scheint der Untergrund durch. Das ist hier
   Pflicht und nicht Geschmack: Chips stehen in der Filterspalte auf der
   Karte, im Sheet auf der Sheet-Fläche und in der Bauartzeile der
   Druckerseite auf der Seitenfläche. Eine feste Hoverfarbe müsste für alle
   drei gleichzeitig stimmen und tat es hell für keine (siehe theme.css). */
.chip:hover { background: var(--zustand-hover); }
.chip:active { background: var(--zustand-aktiv); }
/* ⛔ Der Rand bleibt STEHEN und wird eingefärbt, statt zu verschwinden.
   Vorher stand hier `border-color: transparent`, und damit war der gewählte
   Chip hell nur noch an seiner Füllung zu erkennen: --secondary-container
   gegen die Kartenfläche sind 1,09:1. Der ausgewählte Zustand hatte also
   WENIGER erkennbare Form als der nicht ausgewählte (dessen --outline-Rand
   bei 5,03:1 steht) — genau verkehrt herum.
   --secondary ist der Vollton derselben Rolle wie die Füllung: 4,87:1 hell,
   9,54:1 dunkel. Das Häkchen davor bleibt, es trägt die Aussage ohne Farbe;
   der Rand trägt die Grenze des Bedienelements (WCAG 1.4.11). */
.chip[aria-pressed="true"] {
  background: var(--secondary-container);
  color: var(--on-secondary-container);
  border-color: var(--secondary);
}
.chip[aria-pressed="true"]::before { content: "✓"; font-size: var(--label-medium); }

.chip-reihe { display: flex; flex-wrap: wrap; gap: var(--abstand-s); }

/* ---------- Angebots-Kachel (FilamentCard.kt + Website-Vorgaben) ---------- */

/* Das Raster passt sich der Breite an, statt eine feste Spaltenzahl zu haben.
   auto-fill (nicht auto-fit): leere Spuren bleiben erhalten, dadurch behält
   ein einzelner Treffer seine natürliche Spaltenbreite und steht linksbündig,
   statt sich über die ganze Fläche zu strecken. */
.kachel-liste {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--abstand-l);
}
/* Tablet: bewusst genau zwei Spalten. */
@media (min-width: 640px) and (max-width: 1023px) {
  .kachel-liste { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* ⛔ MINDESTBREITE 350px (23.09.2026, Bildvorlage mit vier Kacheln je Reihe).
   Das ist die schmalste Kachel, die aussieht wie die Vorlage: großer Preis und
   die Schalter daneben (Containerschwelle beim Preis weiter unten — wer sie
   ändert, ändert diese Zahl mit). Bei 320px entstanden Kacheln von 327–349px
   mit dem KLEINEN Preis.
   Gemessen (Liste, Filterspalte offen / eingeklappt): 1600px 1012 / 1448px,
   1920px und breiter 1132 / 1568px — das Inhaltsmaß der Seite deckelt dort.
   Eingeklappt stehen damit ab 1600px vier Kacheln (350 bzw. 380px; Vorlage:
   388px). Mit offener Spalte sind vier in Vorlagengröße auf keiner Breite
   möglich (4 × 350 + 48 = 1448px).
   ⚠️ Ab 1280px sind es mindestens ZWEI Spalten: Bei 1280px mit offener
   Spalte ist die Liste 692px breit, und 350px ließen eine einzige Kachel
   über die ganze Breite stehen. Dort werden es zwei à 338px — mit dem
   kleinen Preis, bewusst hingenommen.
   `min(…, 100%)` wie überall: eine Mindestbreite ist sonst auch dann eine,
   wenn weniger Platz da ist (CLAUDE.md, „Mindestbreiten"). */
@media (min-width: 1024px) {
  .kachel-liste {
    grid-template-columns: repeat(auto-fill, minmax(min(350px, 100%), 1fr));
  }
}
@media (min-width: 1280px) {
  .kachel-liste {
    grid-template-columns:
      repeat(auto-fill, minmax(min(350px, calc((100% - var(--abstand-l)) / 2)), 1fr));
  }
}

/* ---------- Tabellenansicht der Angebote (02.09.2026) ----------

   Zweite Darstellung derselben Auswahl (PD.zustand.listenansicht). Sie trägt
   dieselben Angaben wie die Kachel, nur in Spalten — für lange Listen die
   Ansicht, in der sich Preise Zeile für Zeile lesen lassen.

   ⛔ Der Rahmen rollt WAAGERECHT, statt die Tabelle zu quetschen. Acht
   Spalten passen unterhalb von rund 900px Listenbreite nicht mehr nebeneinander;
   ohne diesen Kasten drückte die Tabelle das ganze Dokument breiter, und
   `overflow-x: clip` am <html> schnitte sie lautlos ab.

   ⚠️ `tabindex="0"` bekommt der Rahmen NICHT von Hand: Chrome und Firefox
   machen einen rollbaren Kasten von selbst tastaturerreichbar. Ein
   ausdrückliches tabindex fügte einen Tabstopp ohne Beschriftung hinzu. */
.tabelle-rahmen {
  overflow-x: auto;
  border-radius: var(--radius-m);
  background: var(--surface-container);
  box-shadow: var(--karten-kante);

  /* ⛔ 1px POLSTER — es hält die Kante frei, und darum geht es hier
     (17.09.2026). --karten-kante ist seit demselben Tag ein inset-Schatten:
     Er liegt über der Fläche, aber UNTER den Kindern. Ohne dieses Polster
     deckt der klebende Tabellenkopf die Oberkante, und in der Angebotstabelle
     der Detailseite decken die Zellen auch die Seiten — gemessen blieben von
     91px linker Kante noch 3px übrig, die Tabelle stand ohne Umrandung da.
     Das Polster schiebt die Kinder um genau die Strichstärke nach innen;
     seither tragen linke, rechte und obere Kante wieder 100 %.

     ⛔ NICHT als `outline` — gemessen und verworfen: Der Tabellenkopf ist
     `position: sticky` und bildet damit einen eigenen Stapelkontext; er malt
     auch über die Kontur des Elternteils. Übrig blieben zwei Drittel der
     Seitenkanten und keine Oberkante. Ein Weg, der die Hälfte des Problems
     löst, sieht aus wie einer, der es löst.

     ⛔ NICHT als `border`: Ein Rahmen wüchse in die Breite. Das Polster tut
     das nicht — es verkleinert den Inhaltskasten, gegen den die
     Spaltenschwellen unten rechnen, um 2px. Nachgemessen an 15 Breiten
     beiderseits jeder Schwelle (1030/1021/1019 … 600/591/589): gleiche Zahl
     sichtbarer Spalten wie ohne Polster, kein Rollen, kein Überlauf.

     ⛔ Und NICHT als ::after mit `position: absolute`: Dieser Kasten ROLLT
     waagerecht, und ein absolut liegendes Kind eines Rollkastens rollt mit —
     die Umrandung wanderte beim Rollen aus dem Bild. */
  padding: 1px;

  /* Containerkontext für die Spaltenregeln unten: Maßgeblich ist die Breite
     der LISTE, nicht die des Fensters. Neben der 420px breiten Filterspalte
     bleiben bei 1440px Fenster 852px — eingeklappt sind es 1272px. Dieselbe
     Fensterbreite trägt also einmal alle Spalten und einmal nicht. */
  container-type: inline-size;
}

.angebots-tabelle {
  width: 100%;
  border-collapse: collapse;
  /* Ohne Mindestbreite fielen die Spalten in sich zusammen (jede Zelle bricht
     dann Wort für Wort um), statt den Rahmen rollen zu lassen.
     ⚠️ 580px statt 620px seit dem 03.09.2026: Das ist das gemessene
     Mindestmaß der kleinsten Stufe (Produkt 220 + Preis 163 + Aktionen 196).
     Mit 620px stand die Tabelle zwischen 580 und 620px Rahmenbreite breiter
     da, als ihr Inhalt verlangt — und rollte deshalb, obwohl alles
     hineingepaßt hätte. Sie darf nie breiter sein wollen als nötig, sonst
     erzeugt die Untergrenze selbst den Anschnitt, den sie verhindern soll.
     280px ist das Maß der kleinsten Stufe (Produkt 120 + Preis 163). */
  min-width: 280px;
  font-size: var(--body-medium);
}

/* ⚠️ 6px seitlich statt 8: Bei sieben Spalten sind das 28px Unterschied, und
   genau die entscheiden bei 1012px Listenbreite darüber, ob die volle Tabelle
   hineinpasst oder um 6px rollt. „Kompakt" ist hier kein Geschmack, sondern
   der Zweck dieser Darstellung. */
.angebots-tabelle th,
.angebots-tabelle td {
  padding: var(--abstand-m) 6px;
  text-align: left;
  vertical-align: middle;
  border-bottom: 1px solid var(--outline-variant);
}
.angebots-tabelle tbody tr:last-child th,
.angebots-tabelle tbody tr:last-child td { border-bottom: 0; }

/* Der Tabellenkopf klebt beim Rollen — bei sechzig Zeilen ist eine
   Zahlenspalte ohne Überschrift nicht mehr zuzuordnen.
   ⚠️ Er braucht eine eigene Fläche: Ohne sie scrollen die Zeilen sichtbar
   durch ihn hindurch. */
.angebots-tabelle thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-container);
  font-family: var(--schrift-mono);
  font-size: var(--label-medium);
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--on-surface-variant);
  white-space: nowrap;
}

.tabellen-zeile:hover { background-color: var(--zustand-hover); }

/* Die Produktspalte: Bild und Name nebeneinander, Badges darunter. */
.tz-name {
  display: flex;
  align-items: center;
  gap: var(--abstand-s);
  color: inherit;
  text-decoration: none;
  font-weight: 500;
}
.tz-name:hover .tz-titel { text-decoration: underline; }

.tz-bild {
  flex: 0 0 auto;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-s);
  overflow: hidden;
  background: var(--surface-container-highest);
}
.tz-bild img { width: 100%; height: 100%; object-fit: contain; }

.tz-titel { min-width: 0; }

.tz-badges {
  display: flex;
  flex-wrap: wrap;
  gap: var(--abstand-xs);
  margin-top: var(--abstand-xs);
}

/* Abgelesene Werte in der Schreibmaschinenschrift — dieselbe Regel wie auf
   der Kachel: Was gemessen ist, sieht überall gleich aus. */
.angebots-tabelle .sp-angebot,
.angebots-tabelle .sp-vergleich {
  font-family: var(--schrift-mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ⚠️ Die Produktspalte braucht eine Mindestbreite, sonst gibt sie ihren Platz
   an die schmalen Zahlenspalten ab: Gemessen brach „Prusament PETG Basic
   1,75 mm 5 kg" auf VIER Zeilen um, und die Zeilenhöhe verdreifachte sich.
   Sie bekommt zugleich die ganze Restbreite (100 %), damit die übrigen
   Spalten nur so breit werden, wie ihr Inhalt es verlangt. */
.angebots-tabelle .sp-produkt { min-width: 220px; width: 100%; }

/* ⚠️ Die Spalte, nach der sortiert ist — in der Akzentfarbe und fett, wie der
   Preis auf der Kachel. Sie ist der Grund, warum diese Liste in dieser
   Reihenfolge steht. */
.tz-vergleichspreis {
  font-family: var(--schrift-serif);
  font-size: var(--title-medium);
  font-weight: 700;
  color: var(--primary);
}

.angebots-tabelle .sp-farbe { white-space: nowrap; }
.angebots-tabelle .sp-farbe .kachel-farbpunkt { margin-right: var(--abstand-xs); }

/* ⛔ DIE STUFEN STEHEN HINTER DEN GRUNDREGELN DER SPALTEN, UND DAS IST
   PFLICHT. Eine Containerabfrage erhöht die Spezifität NICHT — es entscheidet
   allein die Reihenfolge in der Datei. Stufe 6 setzt `min-width` an derselben
   Spalte wie `.sp-produkt` oben; stand sie davor, gewann die Grundregel, die
   Produktspalte blieb bei 220px und die Tabelle rollte bei 390px Fenster um
   41px weiter — gemessen, beim Bauen genau so passiert. Dieselbe Falle wie
   bei den Kategorien der Kopfleiste (css/layout.css). */

/* ---------- Was bei wenig Platz zuerst geht ----------

   ⛔ NACH WICHTIGKEIT, NICHT NACH POSITION. Die Reihenfolge ist unverändert:
   von rechts nach links gibt die Tabelle die entbehrlichsten Angaben ab —
   Farbe (steht als Punkt auch in der Kachel), Material (steckt meist schon im
   Produktnamen), Shop (steht in der Kaufbox der Detailseite).

   ⛔ DIE SCHWELLEN SIND AM 03.09.2026 KORRIGIERT WORDEN, UND DAS WAR EIN
   ECHTER FEHLER, KEINE FEINABSTIMMUNG.

   Sie standen auf der BREITE DER SPALTE, die sie ausblenden — 940px für eine
   98px breite Farbspalte. Richtig ist die Breite, die die Tabelle MIT dieser
   Spalte braucht. Zwischen beiden Zahlen lag ein Fenster, in dem die Spalte
   noch stand, die Tabelle aber schon rollte, und dann schob sich der klebende
   Block über sie: Werte endeten mitten in der Zahl, Überschriften liefen
   ineinander. Gemessen an der laufenden Seite, Filterspalte 290 → 640px:

     Rahmen 982   Tabelle 1007   rollte  25px   (Farbe stand noch)
     Rahmen 752   Tabelle  834   rollte  82px   (Shop stand noch)
     Rahmen 632   Tabelle  734   rollte 102px

   Jede dieser Lagen liegt IM Ziehbereich der Filterspalte — der Fehler war
   also nicht der Ausnahmefall, sondern der Regelfall beim Verbreitern.

   Gemessene Spaltenbreiten: Produkt 220 (Mindestmaß) · Material 75 ·
   Farbe 98 · Angebot 155 · Shop 100 · Vergleich 163 · Aktionen 196. Daraus
   die Breite, die die Tabelle in jeder Stufe braucht — und daraus die
   Schwelle, jeweils mit etwas Reserve, weil die Spaltenbreiten am INHALT
   hängen (ein längerer Shopname macht die Shopspalte breiter):

     Stufe                        braucht   Schwelle
     alle sieben Spalten           1007px    unter 1020px fällt Farbe
     ohne Farbe                     909px    unter  920px fällt Material
     ohne Farbe, Material           834px    unter  850px fällt Shop
     ohne Farbe, Material, Shop     734px    unter  750px fällt Angebot
     Produkt · Preis · Aktionen      579px    unter  590px fällt Aktionen
     Produkt · Preis                 383px    unter  400px schrumpft Produkt
     Produkt (schmal) · Preis        283px    paßt bis 320px Fenster hinunter

   ⛔ DIE VIERTE STUFE IST NEU, UND SIE IST DER PREIS FÜR DIE ZUSAGE, DASS DER
   VERGLEICHSPREIS IMMER VOLLSTÄNDIG SICHTBAR IST. Unterhalb von 750px passen
   Produkt, Angebot, Preis und Aktionen nicht mehr nebeneinander — dann muss
   eine davon weichen, sonst rollt die Tabelle und der Preis wandert aus dem
   Bild. Es ist das Angebot: Menge und Rollenpreis ERKLÄREN den
   Vergleichspreis, sie ersetzen ihn nicht, und beide stehen vollständig auf
   der Detailseite. Bei 1440px Fenster und ganz ausgezogener Filterspalte
   (640px) bleiben dem Rahmen 632px — genau diese Lage.

   ⚠️ `display: none` und nicht `visibility: hidden`: Eine unsichtbare Zelle
   belegte ihre Spaltenbreite weiter, und genau die soll frei werden. */
@container (max-width: 1019px) {
  .angebots-tabelle .sp-farbe { display: none; }
}
@container (max-width: 919px) {
  .angebots-tabelle .sp-material { display: none; }
}
@container (max-width: 849px) {
  .angebots-tabelle .sp-shop { display: none; }
}
@container (max-width: 749px) {
  .angebots-tabelle .sp-angebot { display: none; }
}

/* ⛔ STUFE 5 UND 6 — UND HIER WIRD ES UNBEQUEM.

   Unter 580px passen auch Produkt, Preis und Aktionen nicht mehr
   nebeneinander (220 + 163 + 196 = 579). Bis zum 03.09.2026 rollte die
   Tabelle dann, und die Aktionsspalte stand halb im Bild — gemessen 8px
   angeschnitten bei 1280px Fenster und 540px Filterspalte, 144px bei 1024px
   Fenster. Genau der gemeldete Zustand, nur eine Stufe kleiner.

   Es gibt dort keine Wahl, die nichts kostet: Der Vergleichspreis soll
   vollständig sichtbar bleiben, und zwei Spalten sind schon 383px breit.
   Also fällt die Aktionsspalte — als LETZTE, nach allem anderen, und nur
   dort, wo sie sonst angeschnitten stünde. Was sie trägt, bleibt erreichbar:
   Die Zeile selbst führt über den Produktnamen auf die Detailseite, und dort
   stehen Merken, Vergleichen und der Weg in den Shop vollständig. Wer sie in
   der Liste braucht, hat daneben die Kachelansicht.

   ⚠️ WELCHE FENSTER DAS ÜBERHAUPT TRIFFT: bei 1440px und breiter keine —
   dort bleiben dem Rahmen selbst mit ganz ausgezogener Filterspalte 632px.
   Bei 1280px trifft es die letzten rund 100px des Ziehwegs, bei 1024px den
   größeren Teil davon. Das ist der Preis dafür, dass nichts mehr
   angeschnitten ist.

   Stufe 6 gilt nur noch dem Telefon (Rahmen 289px bei 320px Fenster): Dort
   sind selbst Produkt und Preis mit 383px zu breit. Die Produktspalte gibt
   ihr Vorschaubild ab und geht auf 120px Mindestbreite — zusammen 283px, und
   damit paßt die Tabelle bis 320px hinunter vollständig ins Bild.
   ⚠️ Das Bild ist das Erste, was geht, nicht der Name: Es wiederholt nur,
   was der Name sagt. */
@container (max-width: 589px) {
  .angebots-tabelle .sp-aktionen { display: none; }
}
@container (max-width: 399px) {
  .angebots-tabelle .sp-produkt { min-width: 120px; }
  .angebots-tabelle .tz-bild { display: none; }
}

/* ⛔ NICHTS KLEBT MEHR AM RECHTEN RAND (03.09.2026) — UND DAS IST DIE
   EIGENTLICHE BEHEBUNG, NICHT DIE KORREKTUR DER SCHWELLEN OBEN.

   Hier standen `position: sticky` an Preis und Aktionen, dazu eigene
   Flächen, ein z-index für die Köpfe, eine Hover-Ebene und eine Ausnahme
   unter 580px. Der Grund war richtig: Rollt die Tabelle, wandern sonst
   ausgerechnet Preis und Schalter aus dem Bild.

   Aber Kleben heisst ÜBERDECKEN. Was unter einer klebenden Spalte
   hindurchrollt, wird an ihrer Kante abgeschnitten — mitten im Wort, mitten
   in der Zahl, an einer Stelle, die niemand wählt. Genau das ist gemeldet
   worden. Es liess sich nicht abstellen, solange die Tabelle überhaupt
   rollt: Ein Anschnitt an einer beliebigen Stelle ist die Bauart des
   Klebens, kein Fehler darin.

   Mit den korrigierten Schwellen oben rollt die Tabelle in ihrem ganzen
   Arbeitsbereich nicht mehr — sie gibt vorher eine Spalte ab. Das Kleben
   hätte damit nichts mehr zu tun: Oberhalb von 580px rollt nichts, unterhalb
   war es ohnehin abgeschaltet (die beiden Spalten und die 220px der
   Produktspalte überlappten sich dort). Eine Regel, die in keiner Lage mehr
   wirkt, bleibt nicht als Vorrat stehen — sie ist entfallen.

   ⚠️ Die Aktionsspalte behält ihre feste Breite. Sie ist jetzt kein Anker
   mehr für eine klebende Nachbarin, aber weiterhin das Maß, das Merken,
   Vergleichen und den Shop-Knopf nebeneinander trägt — ohne sie schwänkte
   die Spaltenbreite mit der Länge des Knopftextes. */
.angebots-tabelle {
  /* zwei Symbolknöpfe à 36px, der Shop-Knopf und die Lücken dazwischen,
     plus das Zellpolster. */
  --tz-aktionen: 196px;
}

/* ⛔ min-width, NICHT width: `width` ist im automatischen Tabellenlayout ein
   Vorschlag, `min-width` eine Untergrenze. Mit `width` stauchte der
   Algorithmus die Spalte gemessen auf 183px. */
.angebots-tabelle .sp-aktionen { min-width: var(--tz-aktionen); }

/* ---------- Die Preisspalte ist ein eigener Bereich (03.09.2026) ----------

   Sie ist der Grund, warum die Liste in dieser Reihenfolge steht, und soll
   als eigener Streifen lesbar sein statt als eine Spalte unter sieben.

   ⚠️ Die Trennung liegt auf BEIDEN Seiten. Links stand sie schon (damals
   als Kante des klebenden Blocks); allein trennte sie den Preis nur von dem,
   was vor ihm steht, während er nach rechts in die Aktionsspalte auslief.
   Zwei Linien über alle Zeilen hinweg ergeben eine durchgehende Bahn — das
   ist der Unterschied zwischen einer Spaltengrenze und einem Bereich.

   ⚠️ MITTIG statt rechtsbündig, auf Wunsch vom 03.09.2026. Rechtsbündig
   fänden Ziffern gleicher Stelligkeit übereinander — der übliche Grund für
   rechtsbündige Zahlen. Hier trägt das weniger weit, weil in der Spalte
   ohnehin gemischte Einheiten und Währungen stehen können und jede Zahl ihr
   `€/kg` mitführt; `tabular-nums` bleibt, die Ziffern haben also weiterhin
   gleiche Laufweite. */
.angebots-tabelle .sp-vergleich {
  text-align: center;
}

/* ---------- Jede Spalte ein eigener Streifen (03.09.2026) ----------

   Vorher trennten nur waagerechte Linien die Zeilen; senkrecht stand allein
   die Preisspalte in ihren beiden Kanten. Die übrigen Werte lagen dadurch
   linksbündig irgendwo in einer breiten Spalte — „5 kg · 98,26 €" begann
   dicht am Material und endete lange vor dem Shop, und beim Lesen über
   sechzig Zeilen war nicht zu sehen, wo eine Spalte aufhört.

   ⚠️ `tr > * + *` statt `:not(:last-child)`: Die Linie gehört ZWISCHEN zwei
   Spalten, nicht an die Außenkante der Tabelle. Mit `:not(:last-child)`
   bekäme die letzte SICHTBARE Spalte eine Kante, sobald die Aktionsspalte
   unter 590px ausgeblendet ist — `:last-child` zählt das Markup, nicht das
   Sichtbare. Der Nachbarschaftsselektor kennt das Problem nicht: Er setzt die
   Linie immer links am folgenden Element, und ein `display: none` dazwischen
   verschiebt sie stillschweigend auf die nächste sichtbare Spalte.

   ⚠️ `> * + *` und nicht `td + td`: Die erste Zelle jeder Zeile ist ein
   `<th scope="row">` (der Produktname ist die Überschrift seiner Zeile), die
   Kopfzeile besteht ganz aus `th`. Ein Selektor auf `td` ließe die Fuge
   zwischen Produkt und Material aus — und genau die ist die erste. */
.produkt-tabelle tr > * + * {
  border-left: 1px solid var(--outline-variant);
}

/* ⛔ Die Preisspalte behält ihre STÄRKERE Kante (`--outline` statt
   `--outline-variant`, gemessen 5,16:1 gegen 1,22:1). Sie ist der Grund,
   warum die Liste in dieser Reihenfolge steht, und war bisher die einzige mit
   senkrechten Linien. Bekämen jetzt alle Spalten dieselbe Haarlinie, ginge
   genau diese Auszeichnung verloren — aus einem Bereich würde eine Spalte
   unter sieben. Zwei Stärken, zwei Aussagen: Fuge und Bereichsgrenze. */
.produkt-tabelle .sp-vergleich,
.produkt-tabelle .sp-vergleich + * {
  border-left-color: var(--outline);
}

/* ---------- Mittig in ihrer Spalte ----------

   Alle Wertspalten stehen mittig, Kopf wie Zelle. Bei kurzen Werten in
   breiten Spalten (ein Materialkürzel, ein Shopname) klebte der Text sonst an
   der linken Fuge und ließ rechts daneben eine Lücke, die wie eine fehlende
   Angabe aussah.

   ⚠️ Die PRODUKTSPALTE bleibt linksbündig, und das ist Absicht: Sie trägt die
   ganze Restbreite (`width: 100%`) und Namen sehr verschiedener Länge. Mittig
   gesetzt hätte sie zwei ausgefranste Kanten statt einer Flucht — und der
   Name ist die Zeile, an der man die Liste entlangliest. */
.produkt-tabelle .sp-material,
.produkt-tabelle .sp-farbe,
.produkt-tabelle .sp-angebot,
.produkt-tabelle .sp-shop,
.produkt-tabelle .sp-aktionen {
  text-align: center;
}

.tz-aktionen {
  display: flex;
  align-items: center;
  /* mittig wie die Spalte, in der sie steht */
  justify-content: center;
  gap: var(--abstand-xs);
}
.tz-shop { min-height: 32px; padding: 0 var(--abstand-m); white-space: nowrap; }

/* Ein inkompatibles Angebot bleibt sichtbar und wird gedämpft — dieselbe
   Entscheidung wie bei der Kachel: Es verschwindet nur auf ausdrücklichen
   Wunsch (Filter „Inkompatible ausblenden"). */
.tabellen-zeile--inkompatibel { opacity: 0.72; }
.tabellen-zeile--im-vergleich { background-color: var(--zustand-aktiv); }

/* Fläche statt Kasten: Die Kachel hebt sich über ihre eigene Flächenstufe von
   der Seite ab, nicht über Schatten und Rahmen. --surface-container ist die
   erste Stufe, die in beiden Themes wirklich sichtbar von --background
   abweicht; --surface-container-lowest lag nur 1,05:1 daneben und war
   deshalb ohne Rahmen unsichtbar.

   Der 2px-Rahmen bleibt transparent stehen, weil die Vergleichsauswahl ihn
   einfärbt — ohne ihn würde die Kachel beim Auswählen um 4px springen. */
/* ⛔ EINE LÜCKE, EIN POLSTER (22.09.2026, Bauart der Druckerkachel).

   Bis dahin brachte jeder Block sein eigenes Polster mit — die Titelspalte
   oben, der Fuß seitlich, der Shop-Knopf unten. Fehlte ein Block, fehlte auch
   sein Rand, und zwei Kacheln nebeneinander hatten verschiedene Innenmaße.
   Jetzt polstert die Kachel einmal, und die Abstände zwischen den Blöcken
   kommen aus `gap`: Was entfällt, hinterlässt keine Lücke.

   ⚠️ `container-type: inline-size` macht die KACHEL zum Bezug ihrer eigenen
   Abfragen — wie bei .dr-zeile. Wie breit sie ist, hängt an der Spaltenzahl
   des Rasters und nicht am Fenster; eine Medienabfrage träfe die
   Umbruchstelle darin nicht (die Filterspalte verschiebt sie zusätzlich).

   ⚠️ `overflow: hidden` ist entfallen: Nichts ragt mehr über die Kante (das
   Bild hat seine eigene Rundung), und der Zuschnitt hätte den Fokusring der
   Schalter an der Kachelkante beschnitten. */
.filament-kachel {
  position: relative;              /* Bezug für die Klickfläche der Kachel */
  container-type: inline-size;
  container-name: kachel;
  display: flex;
  flex-direction: column;
  /* 12px Lücke, 16px Polster (Bildvorlage, 23.09.2026; vorher 8 / 12). Die
     Lücke ist zugleich der Abstand Preisblock → „Zum Shop". */
  gap: var(--abstand-m);
  padding: var(--abstand-l);
  background: var(--surface-container);
  border-radius: var(--radius-l);
  border: 2px solid transparent;
  transition: background-color var(--dauer-kurz) ease, border-color var(--dauer-kurz) ease;
}
/* Kein Anheben, kein Schatten: Die Kachel liegt weiterhin in der Seite, sie
   schwebt nicht. Eine Zustandsebene reicht als Rückmeldung — sie kommt aus
   der gemeinsamen Regel „Zustandsebene auf eigener Fläche" weiter unten. */
.filament-kachel--im-vergleich { border-color: var(--primary); }

/* ⛔ BILD LINKS, TEXTSPALTE RECHTS (Bildvorlage, 23.09.2026). Neben dem
   Bild stehen Name, Merkmalszeile, Herkunftszeile und Abzeichen
   untereinander (.kachel-titel). Oben bündig: Die Namenszeile beginnt an
   der Oberkante des Bildes. */
.kachel-kopf {
  display: flex;
  align-items: flex-start;
  gap: var(--abstand-l);
  min-width: 0;
}

/* Die Textspalte. Ihre Lücke ist der EINE Abstand zwischen ihren Zeilen —
   keine Zeile bringt einen eigenen Rand mit, und was entfällt, hinterlässt
   nichts. `min-width: 0` ist Pflicht, sonst kürzt der Name nicht, sondern
   schiebt die Spalte über die Kachel hinaus. */
.kachel-titel {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--abstand-s);
}

/* Bild + Titel sind ein echter Link zur Detailseite. Sein ::after legt sich
   über die ganze Kachel, sodass ein Klick irgendwo auf der Kachel das Detail
   öffnet — Symbole und Shop-Button liegen darüber und behalten ihre eigene
   Wirkung. Nur EIN Link, also auch nur ein Tab-Stopp. */
/* Er trägt seit dem 23.09.2026 nur noch die Kopfzeile (Bild + Textspalte).
   ⚠️ Kein `flex: 1` — er soll NICHT wachsen. Der Ausgleich einer zu hohen
   Rasterreihe gehört nach unten (margin-top: auto am Fuß); wüchse der
   Hauptbereich mit, verteilte sich die Höhe auf die Textblöcke. */
.kachel-hauptbereich {
  display: flex;
  flex-direction: column;
  gap: var(--abstand-xs);
  min-width: 0;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.kachel-hauptbereich::after {
  content: "";
  position: absolute;
  inset: 0;
}

/* Feste Bildgröße, schrumpft nie mit.
   ⚠️ 56px statt 88px (22.09.2026): Die Kachel ist kompakter geworden, und das
   Bild steht jetzt neben EINER Namenszeile statt neben der ganzen
   Titelspalte. 88px hätten die Kopfzeile höher gemacht als alles darunter.
   Die Rundung folgt nicht mehr der Kachelecke — das Bild liegt seit dem
   gemeinsamen Polster nicht mehr an ihr an. */
/* ⚠️ 64px seit dem 23.09.2026 (Bildvorlage): Das Bild steht jetzt neben der
   ganzen Textspalte, nicht neben einer Namenszeile. */
.kachel-bild {
  flex: 0 0 64px;
  width: 64px;
  height: 64px;
  border-radius: var(--radius-s);
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* contain, nicht cover: Die Bilder kommen aus verschiedenen Shops und haben
   jedes Seitenverhältnis. cover hätte ein liegendes Bild links und rechts
   beschnitten und ein hochkantes oben und unten — sichtbar wäre nur der
   mittlere Ausschnitt. Der frei bleibende Raum trägt die Kachelfläche. */
.kachel-bild img { width: 100%; height: 100%; object-fit: contain; }

/* Der Platzhalter steht ohne eigene Fläche und ohne Kontur direkt auf der
   Kachel; die PNG bringt ihre Transparenz selbst mit. Nur der Innenabstand
   bleibt, damit die Spule nicht an die Kanten stößt. */
.bild-platzhalter { padding: var(--abstand-s); }
/* ---------- Das Kompatibilitätszeichen (Bildvorlage, 23.09.2026) ----------

   Ein 20px-Punkt, dessen Mitte auf der rechten Bildkante und 8px unter der
   Bildoberkante sitzt; die Tippfläche darum ist 44px (WCAG 2.5.5).

   ⚠️ `.kachel-kompat` hat `display: contents`: Als leeres Kind der
   Flex-Spalte bekäme sie sonst die 12px-Lücke der Kachel ab. Knopf und Blatt
   sind absolut gesetzt, ihr Bezugsrahmen ist die Kachel.

   ⚠️ Die Lage ist GERECHNET aus Polster (16px) und Bildbreite (64px), nicht
   am Bild gemessen — der Knopf steht außerhalb des Detail-Links und kann das
   Bild nicht zum Bezugsrahmen nehmen. Wer Polster oder Bildbreite ändert,
   ändert diese Zeilen mit. Im Merkzettel-Auswahlmodus steht das Kästchen
   (24px) samt Lücke (16px) vor dem Bild. */
.kachel-kompat { display: contents; }
.kompat-zeichen {
  --kompat-bild-rechts: calc(var(--abstand-l) + 64px);
  position: absolute;
  z-index: 2;
  top: calc(var(--abstand-l) + 8px - 22px);
  left: calc(var(--kompat-bild-rechts) - 22px);
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.filament-kachel--waehlbar .kompat-zeichen {
  --kompat-bild-rechts: calc(var(--abstand-l) + 24px + var(--abstand-l) + 64px);
}
/* Der Punkt. Die Glyphe ist aus der Kachelfläche „ausgestanzt"
   (--surface-container) — auf allen vier hellen Punkten über 5:1. Der Ring
   in derselben Farbe trennt ihn vom Produktbild, auf dem er aufliegt.
   ⛔ Die vier Stufen unterscheidet das ZEICHEN (✕ ! ? ✓), die Farbe kommt
   hinzu. Jeder Punkt hält gegen die Kachel mehr als 3:1 (WCAG 1.4.11). */
.kompat-punkt {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  box-shadow: 0 0 0 2px var(--surface-container);
  color: var(--surface-container);
  font-size: var(--label-medium);
  font-weight: 800;
  line-height: 1;
}
.kompat-punkt svg { width: 14px; height: 14px; }
.kompat-zeichen--inkompatibel .kompat-punkt { background: var(--error); }
.kompat-zeichen--eingeschraenkt .kompat-punkt { background: var(--warnung-erhoeht); }
.kompat-zeichen--ungeprueft .kompat-punkt { background: var(--outline); }
.kompat-zeichen--kompatibel .kompat-punkt { background: var(--balken-gruen); }

/* Das Blatt: dasselbe .krit-blase wie im Filter, hier über die Kachel gelegt,
   unter dem Zeichen und 16px von beiden Rändern. Nie breiter als die Kachel.
   ⛔ Die Kachel ist über `container-type` ein eigener Stapelkontext — ohne
   das z-index an der Kachel mit offenem Blatt malte die NÄCHSTE Kachel ihren
   Grund über ein Blatt, das über die Unterkante ragt. */
.kachel-kompat .krit-blase {
  top: calc(var(--abstand-l) + 8px + 22px + var(--abstand-xs));
  left: var(--abstand-l);
  right: var(--abstand-l);
  z-index: 3;
  width: auto;
  max-width: none;
}
.filament-kachel:has(.krit-blase) { z-index: 5; }

/* In der Kachel füllt die Spule ihre Fläche fast ganz (Vorlage: etwa 59 von
   64px) — mit 8px Polster wäre sie dort nur 48px groß. */
.kachel-bild .bild-platzhalter { padding: var(--abstand-xxs); }

/* ⛔ EINE ZEILE, GEKÜRZT STATT UMGEBROCHEN (22.09.2026).

   Der Name stand in bis zu zwei Zeilen (`-webkit-line-clamp: 2`). Das machte
   jede Kachel einer Reihe verschieden hoch: Die Blöcke darunter standen auf
   verschiedenen Höhen, und wo ein Name kurz war, klaffte die Lücke mitten in
   der Kachel. Jetzt trägt jede Kachel oben genau eine Namenszeile, und der
   Ausgleich entsteht unten.

   ⚠️ Das Auslassungszeichen braucht `min-width: 0`: Ein Flex-Kind wird von
   sich aus nie schmaler als sein Inhalt, und ein `nowrap`-Text drückte die
   Kopfzeile sonst über die Kachel hinaus, statt gekürzt zu werden.
   ⚠️ Der volle Name steht im title (js/kacheln.js) und im aria-label des
   Links — gekürzt ist allein die ANZEIGE. */
.kachel-produktname {
  min-width: 0;
  font-size: var(--title-medium);
  font-weight: 600;
  line-height: var(--zeile-normal);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ⛔ MATERIALKENNZEICHEN UND FARBE — eine Zeile über die WARE (22.09.2026).
   ⚠️ `align-items: center` zieht die Pille auf die Mittellinie des
   Farbnamens; ohne sie stünde sie auf der Grundlinie der Zeile und der
   Farbname 1px höher als der Punkt daneben. */
.kachel-merkmale {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--abstand-s);
  min-width: 0;
}
/* Punkt und Abstand zum Farbnamen wie in der Vorlage (etwa 11 / 10px). */
.kachel-merkmale .kachel-farbpunkt {
  width: 0.75em;
  height: 0.75em;
  margin-right: 0.7em;
}

/* ⚠️ `min-width: 0` plus Kürzung: Ein Farbname wie „Galaxy Black Metallic"
   schöbe die Zeile sonst über die Kachelkante hinaus — dieselbe Falle wie
   beim Produktnamen. */
.kachel-farbe {
  min-width: 0;
  font-size: var(--body-medium);
  color: var(--on-surface-variant);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/**
 * Der Farbpunkt vor dem Farbnamen (PD.kacheln, farbpunktHtml).
 *
 * ⚠️ inline-block statt eines Flex-Kastens am Absatz: Bricht ein langer
 * Farbname um, bleibt der Punkt so in der ersten Zeile beim Wort. Als
 * Flex-Item rutschte er auf die Mitte des gesamten Blocks und stünde dann
 * neben nichts.
 * ⚠️ Die Zeilenhöhe der Kachel darf sich nicht ändern — daher 0.7em, also
 * kleiner als die Zeile, und vertical-align: middle.
 * box-sizing: border-box gilt dokumentweit (basis.css): Der Rand der beiden
 * Sonderfälle wächst nach innen, Punkte mit und ohne Rand sind gleich groß.
 */
.kachel-farbpunkt {
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-right: 0.45em;
  vertical-align: middle;
  border-radius: 50%;
}
/* --outline, nicht --outline-variant: dieselbe Entscheidung wie beim Punkt am
   Filter-Chip am 11.08.2026. Die Trennlinienfarbe stand dort mit 1,95 / 1,73:1
   zu schwach gegen die Fläche, um eine helle Farbprobe abzugrenzen — und genau
   die beiden Fälle soll dieser Rand auffangen. */
.kachel-farbpunkt--rand { border: 1px solid var(--outline); }
/* „Transparent": Umriss ohne Füllung. Kein Ton, also keine Fläche — und
   ausdrücklich kein Grau, das einen behauptete. */
.kachel-farbpunkt--umriss {
  border: 1px solid var(--outline);
  background-color: transparent;
}

/* Oben rechts in einer Reihe; schrumpft nicht und rutscht nie in den
   Textfluss. Über der Klickfläche, damit die Symbole eigenständig wirken. */
/* ⚠️ Seit dem 02.09.2026 im FUSS, neben dem Preis (Bildvorlage 2) — nicht
   mehr oben rechts. Das Polster oben ist damit gegenstandslos; die Größe der
   Knöpfe (36px) ist unverändert. */
/* ⚠️ LAGE WIE IN DER VORLAGE (23.09.2026): Die Symbole stehen an der
   Inhaltskante und tiefer als die Mitte des Preisblocks.
   Die Flächen bleiben 44px; damit das GLYPH an der Kante steht, ragen sie
   12px ins Polster (16px — sie bleiben innerhalb der Kachel) und 8px unter
   den Fuß in die 12px-Lücke vor „Zum Shop" (4px bleiben frei). Ohne Lücke
   nebeneinander: zwei 44px-Ziele liegen damit Kante an Kante, näher geht es
   nicht, ohne dass sie sich überdecken. */
.kachel-symbole {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  align-self: flex-end;
  gap: 0;
  margin-right: calc(-1 * var(--abstand-m));
  margin-bottom: calc(-1 * var(--abstand-s));
}

/* ⛔ 44 × 44px AUF JEDER BREITE (22.09.2026) — dieselbe Zahl wie an der
   Gerätezeile (.dr-aktionen) und die Trefferfläche aus WCAG 2.5.5. Die 36px
   hatten ihren Grund in der alten Anordnung, wo die Symbole oben rechts
   neben dem Namen standen und ihm Breite nahmen. Im Fuß stehen sie neben dem
   Preis, und dort kosten sie in der Höhe nichts: Der Preisblock ist mit
   seiner Nebenzeile ohnehin höher. */
.kachel-symbole .herz-knopf,
.kachel-symbole .vergleich-knopf { width: 44px; height: 44px; }

/* Das Materialkennzeichen — ein Wort, das sagt, worum es geht. Es steht seit
   dem 22.09.2026 in der Merkmalszeile vor der Farbe, nicht mehr oben rechts
   in der Kopfzeile: Dort war es eine eigene Spalte und nahm dem Namen die
   Breite, die er zum Ausschreiben braucht.
   ⚠️ Es ist KEIN Bedienelement und trägt deshalb keinen z-index: Der
   Detaillink der Kachel liegt darüber, ein Klick darauf öffnet also das
   Produkt wie überall sonst auf der Kachelfläche. */
/* ⚠️ Gefüllte Pille in eigener Rolle (23.09.2026, Bildvorlage): 13px fett,
   etwa 27px hoch. Nicht --primary-container — siehe theme.css. */
/* ⚠️ Sie darf schrumpfen und kürzt dann: Ein langer Harztyp war auf der
   229px-Kachel (Tablet, zwei Spalten) 136px breit und lief über die Kante. */
.material-kennzeichen {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 5px 10px;
  border-radius: var(--radius-s);
  background: var(--kennzeichen-flaeche);
  font-family: var(--schrift-mono);
  font-size: var(--body-small);
  font-weight: 700;
  line-height: var(--zeile-normal);
  letter-spacing: 0.04em;
  color: var(--kennzeichen-tinte);
  white-space: nowrap;
}

/* ⛔ DIE ZWEIREIHIGE FASSUNG AUF DEM HANDY IST AM 22.09.2026 ENTFALLEN.

   Sie stammte aus der Zeit, als die Symbole oben rechts neben dem Namen
   standen: Bis zu vier Knöpfe à 36px nahmen der Titelspalte neben dem
   88px-Bild die Breite, und der Name stand Wort für Wort untereinander.
   Seit dem 02.09.2026 stehen sie im FUSS und nehmen dem Namen gar nichts
   mehr; es sind dort genau zwei. Zwei Reihen hätten die Kachel nur höher
   gemacht, ohne irgendetwas zurückzugeben.

   ⚠️ Was der Preis daneben braucht, entscheidet jetzt eine Containerabfrage
   weiter unten (.preis-pro-kg) — nicht die Fensterbreite: Wie breit die
   Kachel ist, hängt an der Spaltenzahl des Rasters. */

/*
 * ⛔ .info-symbol und .warn-symbol sind am 07.08.2026 ENTFALLEN — mit den
 * beiden Knöpfen, die sie trugen (js/kacheln.js). Ihre Regeln stehen hier
 * nicht mehr: Ein Stylesheet, das Klassen beschreibt, die kein Element mehr
 * trägt, liest sich beim nächsten Mal wie ein vorhandenes Bedienelement.
 *
 * Die Warnstufe selbst lebt weiter — sie steht auf der Detailseite und in der
 * Materialübersicht; das Token --warnung-erhoeht wird dort gebraucht und
 * bleibt deshalb in theme.css.
 */

/* Merken-Herz */
.herz-knopf {
  width: 36px; height: 36px;
  border: 0; background: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--on-surface-variant);
}
.herz-knopf svg { width: 20px; height: 20px; }
.herz-knopf[aria-pressed="true"] { color: var(--primary); }

/* Vergleichs-Schalter */
.vergleich-knopf {
  width: 36px; height: 36px;
  border: 0; background: none; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--on-surface-variant);
}
.vergleich-knopf svg { width: 20px; height: 20px; }
.vergleich-knopf[aria-pressed="true"] { color: var(--primary); }

/* Badges stehen jetzt in der Titelspalte unter Name und Farbe, nicht mehr
   als eigene Zeile über die ganze Kachelbreite. */
/* ⚠️ Ohne eigenen Rand seit dem 22.09.2026: Der Abstand zu den Zeilen
   darüber kommt aus der Lücke des Hauptbereichs. Ein eigener Rand käme zu
   ihr hinzu, und die Kachel mit Abzeichen stünde weiter auseinander als die
   ohne — genau die ungleiche Struktur, die der Umbau abstellt. */
.kachel-badges {
  display: flex; flex-wrap: wrap; gap: var(--abstand-xs);
}

/* Inkompatible Filamente bleiben sichtbar, treten aber zurück. Die Deckkraft
   sinkt gezielt auf Bild, Name, Farbe und Preis — NICHT auf die Badges: das
   Badge ist der Grund für die Abstufung und muss voll lesbar bleiben.
   Deshalb einzeln aufgeführt statt auf dem gemeinsamen Elternelement (die
   Badges liegen seit dem Kachel-Umbau innerhalb des Hauptbereichs). */
.filament-kachel--inkompatibel .kachel-bild,
.filament-kachel--inkompatibel .kachel-produktname,
.filament-kachel--inkompatibel .kachel-farbe,
.filament-kachel--inkompatibel .kachel-fuss {
  opacity: 0.62;
}

/* ⚠️ Badges stehen in der Schreibmaschinenschrift (02.09.2026). Sie sind
   Zustandsmarken — „Nicht lieferbar", „Nicht vergleichbar", „Eingeschränkt"
   —, also abgelesene Tatsachen und keine Sätze. In den Vorlagen tragen sie
   dieselbe Schrift wie Temperaturen und Mengen, und genau darin liegt der
   Nutzen: Was gemessen oder abgelesen ist, sieht überall gleich aus. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--abstand-xs);
  padding: var(--abstand-xs) var(--abstand-s);
  border-radius: var(--radius-s);
  font-family: var(--schrift-mono);
  font-size: var(--label-medium);
  font-weight: 500;
}
.badge--fehler { background: var(--error-container); color: var(--on-error-container); }
/* ⛔ `.badge--offen` („noch nicht befüllter Inhalt") ist am 16.09.2026
   entfallen — mit dem Abzeichen „Entwurf" auf /ueber hatte es seinen einzigen
   Leser verloren. Es steht hier als Kommentar, nicht als tote Regel: Ein
   Bausteinname, der „noch nicht befüllt" bedeutet, lädt zur Wiederverwendung
   ein, und der nächste Leser hälte ihn für gepflegt. */
/* „Nicht vergleichbar" ist kein Fehler des Angebots — der Preis stimmt, nur
   sein Kurs fehlt. Deshalb die neutrale Fläche und nicht die Fehlerrolle:
   Sonst läse es sich wie „defektes Angebot". */
/* Eigene Fläche genügt; der frühere Innenrand war eine Umrandung in
   Verkleidung und kam hell nur auf 1,6:1. */
.badge--unvergleichbar {
  background: var(--surface-container-highest);
  color: var(--on-surface-variant);
}

/* Kompatibilitätsstufen. Jede Stufe trägt zusätzlich ein eigenes Symbol und
   ihren Text — die Unterscheidung hängt nie allein an der Farbe. */
.badge svg { width: 16px; height: 16px; flex-shrink: 0; }
.badge--kompatibel { background: var(--gruen-container); color: var(--gruen-inhalt); }
.badge--inkompatibel { background: var(--error-container); color: var(--on-error-container); }
/* Für „eingeschränkt" gibt es keine Container-Rolle im Theme; die
   Warnfarbe trägt hier Text und Rand auf ruhiger Fläche. */
.badge--eingeschraenkt {
  background: var(--surface-container-high);
  color: var(--warnung-erhoeht);
  box-shadow: inset 0 0 0 1px var(--warnung-erhoeht);
}
.badge--ungeprueft {
  background: var(--surface-container-high);
  color: var(--on-surface-variant);
}

/* ⛔ DAS <hr class="kachel-trenner"> IST AM 22.09.2026 ENTFALLEN, und mit ihm
   diese Regel. Es war seit dem Umbau auf Abstände eine Linie ohne Höhe und
   ohne Farbe; sichtbar getrennt hat allein sein Rand, und den trägt jetzt die
   Lücke der Kachel. Ein Stylesheet, das eine Klasse beschreibt, die kein
   Element mehr trägt, liest sich beim nächsten Mal wie ein Baustein. */

/* ⛔ DER FUSS KLEBT AN DER UNTERKANTE (margin-top: auto).

   Das ist die Zusage „alle Kacheln einer Reihe schließen unten auf derselben
   Höhe ab": Eine Rasterreihe macht ihre Kacheln gleich hoch, und der
   Überschuss fällt zwischen den Textblöcken und dem Fuß an — also UNTEN, und
   nicht verteilt auf die Zeilen darüber.

   ⛔ KEIN `flex-wrap` OBERHALB DER SCHWELLE, und das ist eine Entscheidung
   gegen das Naheliegende: Ein Umbruch hängt am INHALT (die Mengenzeile
   „5 kg · 109,99 € gesamt" ist breiter als der Preis darüber), und dann
   stünde in einer Rasterreihe eine Kachel mit Schaltern neben dem Preis und
   die nächste mit Schaltern darunter — gleich hoch, aber verschieden gebaut.
   Stattdessen schrumpft der Preisblock (min-width: 0), und die Mengenzeile
   kürzt sich mit ihrem Auslassungszeichen. Wo auch das nicht reicht,
   entscheidet die Kachelbreite und nicht der Text (Containerabfrage unten). */
/* ⚠️ `padding-top` am Fuß (23.09.2026): Zwischen Textspalte und Preis
   stehen in der Vorlage 24px, zwischen Preisblock und „Zum Shop" 12px — die
   Lücke der Kachel ist 12px, der Rest kommt hier dazu. Der Fuß steht immer
   da; ein eigener Abstand an ihm kann also nicht mit ihm verschwinden. */
.kachel-fuss {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--abstand-s);
  margin-top: auto;
  padding-top: var(--abstand-m);
}

/* Versandmarke und Shopname in einer Zeile (22.09.2026) — beides Angaben
   über DIESES Angebot. Die Farbe steht eine Zeile höher bei der Ware.
   12px zwischen Marke und Shop wie in der Vorlage; bricht die Zeile auf
   schmalen Kacheln um, stehen die Teile 4px untereinander. */
.kachel-herkunft {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--abstand-xs) var(--abstand-m);
  font-size: var(--body-medium);
  color: var(--on-surface-variant);
  min-width: 0;
}
/* ⛔ DIE VERSANDMARKE DER KACHEL: EINE PILLE IN ALLEN DREI LAGEN, AUF JEDER
   BREITE (23.09.2026, Bildvorlage). „Versandfrei" grün, der Aufschlag
   gelblich, „ohne Versandangabe" neutral grau. Die Grundregel in seiten.css
   blendet am Desktop zwei Fassungen aus und nimmt auf dem Handy die Fläche
   weg — für die Tabelle gilt das weiter, hier nicht: Diese Regeln wiegen
   (0,2,0) und schlagen beide.
   ⚠️ `max-width: 100%` mit umbrechendem Text: „ohne Versandangabe" braucht
   bei 14px rund 167px, die Textspalte einer 273px-Kachel hat 157px. */
.kachel-herkunft .versandzeile {
  display: block;
  width: max-content;
  max-width: 100%;
  margin: 0;
  padding: 3px var(--abstand-s);
  border-radius: var(--radius-s);
  font-family: var(--schrift-mono);
  font-size: var(--body-medium);
  line-height: var(--zeile-normal);
}
.kachel-herkunft .versandzeile--frei {
  background: var(--versand-frei-flaeche);
  color: var(--versand-frei-tinte);
}
.kachel-herkunft .versandzeile--auf {
  background: var(--versand-auf-flaeche);
  color: var(--versand-auf-tinte);
}
.kachel-herkunft .versandzeile--unbekannt {
  background: var(--surface-container-highest);
  color: var(--on-surface-variant);
}
.kachel-trennzeichen { color: var(--outline); }

.kachel-anbieter {
  min-width: 0;
  font-size: var(--body-medium);
  font-weight: 600;
  color: var(--primary);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Menge, Rollenpreis und Versandhinweis — abgelesene Werte, deshalb
   Schreibmaschinenschrift (siehe .badge). 14px wie in der Vorlage. */
.kachel-rollendaten {
  font-family: var(--schrift-mono);
  font-size: var(--body-medium);
  color: var(--on-surface-variant);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* preisProKg — das dominante Element der Kachel.
   28px statt 26px: Der Abstand zum Produktnamen (18px) beträgt damit eine
   volle Stufe mehr, und der Preis bleibt auch dann das Erste, was das Auge
   findet, wenn eine Kachel viele Badges trägt.

   ⚠️ In der SERIFENSCHRIFT seit dem 02.09.2026 — dort, wo die Vorlagen jeden
   Preis setzen. Sie trägt hier mehr als Gestaltung: Ihre Ziffern sind
   schmaler und ruhiger als die der serifenlosen Schrift, und ein
   sechsstelliger Kilopreis („129,90 €/kg") passt dadurch auf schmalen
   Kacheln in eine Zeile, die vorher umbrach. */
.preis-pro-kg {
  font-family: var(--schrift-serif);
  font-size: var(--headline-medium);
  font-weight: 700;
  line-height: var(--zeile-eng);
  letter-spacing: -0.01em;
  color: var(--primary);
  white-space: nowrap;
  /* Ziffern gleicher Breite: Untereinanderstehende Preise fluchten damit
     Stelle für Stelle, und ein Preiswechsel beim Umschalten des
     Versandschalters lässt die Kachel nicht springen. */
  font-variant-numeric: tabular-nums;
}

/* ⚠️ AUF DEN SCHMALSTEN GERÄTEN EINE STUFE KLEINER (03.09.2026, mit dem „ab").
   Der Preis steht auf `nowrap` — eine umgebrochene Zahl wäre keine Preisangabe
   mehr —, und im Fuß teilt er sich die Zeile mit Herz und Vergleichsknopf. Bei
   320px Fenster bleiben ihm 157px; gemessen brauchen bei 28px „ab 19,65 €/kg"
   176px, „ab 29,90 CHF/l" 191px und „ab 40,90 CHF/kg" 216px — er lief also
   über die beiden Schalter. Bei 20px passen alle drei (126 / 136 / 154px).
   ⚠️ Lange Fremdwährungspreise liefen hier schon VOR dem „ab" über (ohne es
   braucht „40,90 CHF/kg" 176px in einem 157px-Block); die Stufe behebt beides.
   ⛔ SEIT DEM 22.09.2026 EINE CONTAINERABFRAGE, KEINE FENSTERABFRAGE.

   Die Fensterabfrage stimmte, solange die Schalter 36px breit waren und es
   unter 1024px keine Filterspalte gibt. Mit 44px braucht die Schalterspalte
   92px statt 76px, und damit wird auch die SCHMALSTE DESKTOP-KACHEL zu eng:
   Bei 320px Spaltenbreite (das Minimum von .kachel-liste) bleiben innen
   296px, und „ab 40,90 CHF/kg" braucht bei 28px gemessen 216px — mit
   Schaltern und Lücke 316px. Eine Fensterabfrage sähe das nie, weil dort das
   Fenster 1024px breit ist.

   Maßgeblich ist deshalb die KACHEL.

   ⛔ DIE ABFRAGE MISST DIE INHALTSBREITE, NICHT DIE KACHELBREITE — also die
   Kachel abzüglich Rahmen (4px) und Polster (32px). Bis zum 23.09.2026
   standen die Schwellen hier als Kachelbreiten und wurden als Inhaltsbreiten
   gelesen; mit dem größeren Polster bekam dadurch auch eine 367px-Kachel den
   kleinen Preis, wo die Vorlage bei 371px den großen zeigt.

   Die Rechnung (Stand 23.09.2026): Dem Preis bleibt die Inhaltsbreite
   abzüglich 76px Schalter (2 × 44 ohne Lücke, davon ragen 12px ins Polster)
   und 8px Lücke — also Inhalt minus 84.

     28px, längster Fall „ab 40,90 CHF/kg" = 216px  →  Inhalt ab 300px (Kachel 336)
     20px, derselbe Fall              = 154px  →  Inhalt ab 238px (Kachel 274)

   Beide Schwellen tragen Reserve: Eine Grenze, die auf den Pixel an den
   längsten je gemessenen Preis heranreicht, fällt beim ersten Angebot, das
   einen Zehner mehr kostet. Die große Stufe gilt ab 314px Inhalt, also ab
   350px Kachelbreite (14px Reserve), die kleine ab 254px (16px).
   ⚠️ 350 und nicht 352: Die Mindestbreite des Rasters ist dieselbe Zahl, und
   bei 1600px mit eingeklappter Filterspalte (Liste 1448px) passen vier
   Kacheln erst ab 350px. Wer eine der beiden Zahlen ändert, ändert die
   andere mit (.kachel-liste). */
@container kachel (max-width: 313px) {
  .preis-pro-kg { font-size: var(--title-large); }
}

/* ⛔ UNTER 290px KACHELBREITE (254px Inhalt) STEHEN DIE SCHALTER UNTER DEM
   PREIS.

   Das ist keine Notlösung für einen Sonderfall, sondern der Rest der
   Rechnung darüber: Unterhalb von 274px Kachelbreite passt auch der kleine
   Preis nicht mehr neben zwei 44px-Schalter. Er liefe dann still über die
   Kante oder unter die Schalter — beides sieht man erst bei genau dem einen
   Angebot in genau der einen Währung.

   ⚠️ Es entscheidet die KACHELBREITE, nicht die Textlänge: Damit bricht in
   einer Rasterreihe entweder jede Kachel um oder keine. Genau diese Zusage
   gäbe ein inhaltsgetriebenes `flex-wrap` nicht.

   ⚠️ Eintreten kann das auf zwei Wegen: 320px-Fenster (Kachel 273px) und
   zwei Spalten auf dem Tablet ab 640px (Kachel 231px). Die Schalter bleiben
   dabei rechts — `margin-left: auto`, sonst rückten sie unter dem Preis nach
   links und stünden unter der Mengenzeile statt am Rand. */
@container kachel (max-width: 253px) {
  .kachel-fuss { flex-wrap: wrap; }
  .kachel-symbole { margin-left: auto; }
}

/* preisProKg bleibt das optisch dominante Element; der umgerechnete Wert
   steht als deutlich leisere Nebenzeile darunter und rechtsbündig darunter
   ausgerichtet. Er ist eine Einordnungshilfe, nie die Preisangabe. */
/* ⚠️ LINKSBÜNDIG seit dem 02.09.2026. Der Block stand rechts, solange links
   die Mengenzeile stand; jetzt ist er das erste Element des Fußes, und der
   Preis ist die größte Zahl der Kachel — sie beginnt an derselben Kante wie
   Bild und Name darüber. Rechtsbündig ließe sie gegen nichts fluchten. */
.preis-block {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  min-width: 0;
}

.naeherung {
  display: block;
  font-family: var(--schrift-mono);
  font-size: var(--label-small);
  color: var(--on-surface-variant);
  white-space: nowrap;
  margin-top: var(--abstand-xxs);
}

/* Zusatz am Kilopreis, solange der Versand eingerechnet wird. Gleiche
   Bauform wie die Näherungszeile darüber — beide sind Fußnoten zum Preis
   und sollen nicht miteinander konkurrieren. */
.versand-zusatz {
  display: block;
  font-family: var(--schrift-mono);
  font-size: var(--label-small);
  color: var(--on-surface-variant);
  white-space: nowrap;
  margin-top: var(--abstand-xxs);
}

/* Fehlt die Versandangabe, ist der Wert nicht vergleichbar zustande
   gekommen. Das trägt die Warnfarbe UND den abweichenden Text — nie nur den
   Farbton. */
.versand-zusatz--unbekannt {
  color: var(--warnung-erhoeht);
  font-weight: 500;
}

/* Der angezeigte Preis stammt von einem Angebot, das gerade nicht zu haben
   ist. Gleiche Bauform wie die beiden Fußnoten darüber, aber in der
   Fehlerfarbe wie „Nicht lieferbar" in der Kaufbox — dieselbe Aussage, dieselbe
   Farbe.

   Der Hinweis hängt am PREIS, nicht am Produkt: Er beantwortet, warum hier ein
   Betrag steht, den man heute nicht bezahlen kann. Genutzt von der Kachel und
   von „Weitere Shops" auf der Detailseite. */
.nicht-lieferbar-hinweis {
  display: block;
  font-size: var(--label-small);
  font-weight: 500;
  color: var(--error);
  white-space: nowrap;
  margin-top: var(--abstand-xxs);
}

/* Hinweisband für gemischte Währungen.
   ⚠️ Es hieß hier „dieselbe Sprache wie der Materialhinweis im Vergleich
   (.vergleich-hinweis)" — den gibt es seit dem 04.09.2026 nicht mehr, er ist
   in die Warnpille der Vergleichs-Kopfzeile aufgegangen. Dieses Band bleibt
   unverändert: Es ist die EINE Stelle für die Währungsauskunft, und die steht
   auf mehreren Seiten, nicht nur im Vergleich. */
.waehrungs-hinweis {
  display: flex;
  align-items: flex-start;
  gap: var(--abstand-s);
  background: var(--secondary-container);
  color: var(--on-secondary-container);
  border-radius: var(--radius-m);
  padding: var(--abstand-m);
  font-size: var(--body-medium);
  margin-bottom: var(--abstand-m);
}

/* Ohne Kurse ist die Rangfolge über Währungsgrenzen hinweg nicht belastbar —
   das ist eine Einschränkung der Aussage, kein Nebenhinweis. Deshalb die
   Warnrolle statt der ruhigen Sekundärfläche. */
.waehrungs-hinweis--ohne-kurse {
  background: var(--error-container);
  color: var(--on-error-container);
}

.waehrungs-hinweis svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  /* Auf Höhe der ersten Textzeile, nicht auf Blockmitte. */
  margin-top: var(--abstand-xxs);
}

/* ---------- Zwei Fassungen des Währungshinweises ----------

   js/komponenten.js baut beide ins Markup; sichtbar ist je Fensterbreite
   genau eine. Der Grund steht dort — kurz: <details> lässt sich per CSS nicht
   verlässlich offen erzwingen, und ein Umschalten in JavaScript überstünde
   das Neuzeichnen der Liste beim Filterwechsel nicht.

   Gemessen bei 390px: Das vollständige Band ist 181px hoch. Zusammen mit der
   Werbekennzeichnung (104px) und der Werkzeugzeile (181px) begann die erste
   Kachel erst bei y=738 — 87 % der Bildhöhe eines 844px-Geräts. Zugeklappt
   sind es rund 40px.

   Nicht abgeschnitten war dabei nie etwas: Das Band hat keine feste Höhe,
   kein Vorfahre begrenzt es, und nichts überlagert es (nachgemessen mit
   elementFromPoint an Ober- und Unterkante). Es war schlicht zu lang für den
   Bildschirm. */
.waehrungs-hinweis--klapp { display: none; }

@media (max-width: 639px) {
  .waehrungs-hinweis--voll { display: none; }

  .waehrungs-hinweis--klapp {
    /* <details> ist kein Flex-Kasten — Kopfzeile und Text stehen untereinander. */
    display: block;
    /* Der Innenabstand sitzt an Kopf und Text, nicht am Rahmen: Sonst bliebe
       zugeklappt unter der einen Zeile ein Streifen Fläche stehen. */
    padding: 0;
  }

  .wh-kopf {
    display: flex;
    align-items: center;
    gap: var(--abstand-s);
    padding: var(--abstand-s) var(--abstand-m);
    cursor: pointer;
    /* Das Standarddreieck von <summary> entfällt — die Rolle übernimmt das
       eigene Zeichen rechts, das sich beim Öffnen dreht. Beide Zeilen nötig:
       list-style für die aktuellen Browser, der Pseudo-Marker für ältere. */
    list-style: none;
  }
  .wh-kopf::-webkit-details-marker { display: none; }

  /* Auf Zeilenmitte statt auf der ersten Textzeile — der Kopf IST eine Zeile. */
  .wh-kopf svg { margin-top: 0; }

  /* EINE Zeile, notfalls gekürzt. Die Kürzung ist hier unbedenklich: Der
     vollständige Satz steht unmittelbar darunter, sobald man antippt. */
  .wh-kurz {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .wh-pfeil {
    display: inline-flex;
    flex: 0 0 auto;
    transition: transform var(--dauer-kurz) ease;
  }
  .waehrungs-hinweis--klapp[open] .wh-pfeil { transform: rotate(180deg); }

  .wh-text {
    padding: 0 var(--abstand-m) var(--abstand-m);
    line-height: var(--zeile-normal);
  }
}

/* Über der Klickfläche der Kachel: der Shop-Button behält seine eigene
   Wirkung und öffnet nicht die Detailansicht. */
/* ⚠️ Ohne eigenes Polster seit dem 22.09.2026 — die Kachel polstert einmal,
   und der Abstand nach oben kommt aus ihrer Lücke. Der Knopf steht damit
   über die volle Kachelbreite (knopf--voll) und schließt mit allen anderen
   Blöcken an derselben Kante ab. */
.kachel-shop {
  position: relative;
  z-index: 1;
}
/* ⛔ 44px hoch wie Herz und Vergleich daneben (WCAG 2.5.5). Gesetzt hier und
   nicht an `.knopf`: Dieselbe Klasse trägt jeden Knopf der Seite, und eine
   globale Erhöhung verschöbe Geometrie an Stellen, die niemand angeschaut
   hat — dieselbe Überlegung wie bei `.mz-auswahlleiste .knopf`. */
.kachel-shop .knopf { min-height: 44px; }
/* Beschriftung 16px halbfett wie in der Vorlage. */
.kachel-shop .knopf { font-size: var(--body-large); font-weight: 600; }
/* ⛔ Die Kante ist das dunkelste Grau mit 3:1 — auch auf der überfahrenen
   Kachel (theme.css, --knopf-kante). Die Vorlage setzt 2,3:1, und das trägt
   die Grenze eines Bedienelements nicht. Auf dem Handy ist der Knopf gefüllt;
   die Regel dafür steht in seiten.css, kommt später und gewinnt. */
.kachel-shop .knopf--umrandet { border-color: var(--knopf-kante); }

/* ---------- Lieferbar-Chip (Detailseite) ---------- */

.lieferbar-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--abstand-s);
  padding: var(--abstand-xs) var(--abstand-m);
  border-radius: var(--radius-pille);
  font-size: var(--label-large);
  font-weight: 500;
}
.lieferbar-chip::before {
  content: "";
  width: 8px; height: 8px;
  border-radius: 50%;
  background: currentColor;
}
.lieferbar-chip--ja { background: var(--gruen-container); color: var(--gruen-inhalt); }
.lieferbar-chip--nein { background: var(--error-container); color: var(--on-error-container); }

/* ---------- Bottom-Sheet / Dialog ---------- */

/* ⛔ SOLANGE EIN OVERLAY OFFEN IST, ROLLT DIE SEITE DAHINTER GAR NICHT
   (04.09.2026).

   Vorher tat sie es: Der Schleier liegt zwar über allem, ist aber kein
   Rollbehälter — jede Radumdrehung ging an ihm vorbei ans Dokument. Gemessen
   auf /filamente mit offenem Erweitert-Filter: Seite von y=32 auf y=1400
   gerollt, und nach dem Schließen stand sie dort und nicht mehr da, wo der
   Besucher sie verlassen hatte.

   ⛔ DIE SPERRE GEHÖRT AN DIE WURZEL, NICHT AN DEN SCHLEIER. Ein
   `overscroll-behavior: contain` am Schleier fänge Rad und Finger ab, aber
   weder die Tastatur (Bild ab, Leertaste) noch das Ziehen am Rollbalken.
   `overflow: hidden` an der Wurzel nimmt alle vier Wege auf einmal.
   Der Rollstand überlebt das — `hidden` behält ihn, anders als `clip`, das
   ihn auf 0 zwingen würde und die Seite beim Öffnen nach oben riße.

   ⚠️ DER AUSGLEICH IST PFLICHT, und `scrollbar-gutter: stable` leistet ihn
   NICHT — nachgemessen: Es greift nur bei `auto`/`scroll`, unter `hidden`
   reserviert es nichts. Ohne Ausgleich verschwindet der klassische
   Rollbalken und die Seite dahinter springt um seine Breite zur Seite
   (gemessen 15px bei 900px und 390px Fensterbreite; ab 1024px steht sie bei
   0px, weil die Rollspule den Systembalken ohnehin abschaltet). Die Breite
   mißt js/komponenten.js beim Öffnen und legt sie hier ab — sie hängt am
   Browser, nicht am Entwurf, und darf deshalb keine feste Zahl sein. */
:root[data-modal="offen"] {
  overflow: hidden;
  padding-right: var(--modal-rollbalken, 0px);
}

.schleier-flaeche {
  position: fixed; inset: 0;
  z-index: 60;
  background: var(--schleier);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  animation: schleier-ein var(--dauer-mittel) ease-out;
}
@keyframes schleier-ein { from { opacity: 0; } to { opacity: 1; } }

.sheet {
  width: 100%;
  max-width: 640px;
  max-height: 85vh;
  overflow-y: auto;
  background: var(--surface-container-high);
  border-radius: var(--radius-l) var(--radius-l) 0 0;
  padding: var(--abstand-s) var(--abstand-xl) var(--abstand-xl);
  animation: sheet-hoch var(--dauer-mittel) cubic-bezier(0.2, 0, 0, 1);
}
@keyframes sheet-hoch { from { transform: translateY(24px); opacity: 0.6; } to { transform: none; opacity: 1; } }

.sheet-griff {
  width: 32px; height: 4px;
  border-radius: var(--radius-pille);
  background: var(--outline-variant);
  margin: 0 auto var(--abstand-m);
}

.sheet h2 { font-size: var(--title-large); font-weight: 500; margin-bottom: var(--abstand-l); }

/* Der eigentliche Hebel gegen „alles wirkt gleich wichtig": Zwischen zwei
   Abschnitten steht jetzt --abstand-2xl (40px), innerhalb eines Abschnitts
   höchstens --abstand-l (16px). Vorher war beides 12px, und das Auge konnte
   nicht erkennen, welche Zeilen zusammengehören.
   Die Überschrift wächst dazu von 16px (= Fließtextgröße) auf 18px. */
.sheet-abschnitt { margin-bottom: var(--abstand-2xl); }
.sheet-abschnitt:last-of-type { margin-bottom: 0; }
.sheet-abschnitt h3 { font-size: var(--title-medium); font-weight: 500; margin-bottom: var(--abstand-s); }
.sheet-hinweis { font-size: var(--body-small); color: var(--on-surface-variant); margin-top: var(--abstand-s); }
.sheet-hinweis--akzent { color: var(--primary); }
.sheet-hinweis--block { margin-bottom: var(--abstand-l); }
/* Der Trenner steht zwischen Einfach- und Erweitert-Teil des Filters und
   markiert dort einen echten Wechsel — er bleibt sichtbar, bekommt aber den
   Abschnittsabstand statt des Zeilenabstands. */
.sheet-trenner { border: 0; border-top: 1px solid var(--outline-variant); margin: var(--abstand-2xl) 0; }
.sheet-fuss {
  display: flex;
  justify-content: space-between;
  gap: var(--abstand-s);
  margin-top: var(--abstand-2xl);
}

/* Am Desktop werden Sheets mittig als Dialog gezeigt. */
@media (min-width: 1024px) {
  .schleier-flaeche { align-items: center; }
  .sheet { border-radius: var(--radius-l); max-height: 80vh; }
}

.dialog {
  background: var(--surface-container-high);
  border-radius: var(--radius-l);
  max-width: min(92vw, 400px);
  padding: var(--abstand-xl);
  box-shadow: var(--schatten-schwebend);
}
.dialog h2 { font-size: var(--title-large); margin-bottom: var(--abstand-s); }
.dialog p { font-size: var(--body-medium); color: var(--on-surface-variant); }
.dialog-aktionen {
  display: flex;
  justify-content: flex-end;
  gap: var(--abstand-s);
  margin-top: var(--abstand-xl);
}
.schleier-flaeche--mittig { align-items: center; }

/* ---------- Das einmalige Fenster zu den Beispieldaten (29.09.2026) ----------

   Ein .dialog wie der Bestätigungsdialog, aber mit zwei Absätzen Fließtext —
   er braucht deshalb, was der kurze Dialog nicht braucht:

   ⛔ EINE HÖHENGRENZE UND EIN EIGENER ROLLER. Bei 390px und doppelter
   Grundschrift ist der Text höher als das Fenster; die Seite dahinter ist
   gesperrt (data-modal), also muss der Dialog selbst rollen, sonst wäre der
   Knopf „Verstanden" unerreichbar. 100dvh mit vh-Rückfall für ältere Browser.

   ⚠️ Breiter als der Bestätigungsdialog, aber nie über die Lesebreite — und
   `min(…, 100%)` statt einer blanken Zahl: eine Mindestbreite ist eine
   Untergrenze, auch wenn weniger Platz da ist (CLAUDE.md, „Dateiablage und
   Mindestbreiten").

   ⚠️ Der Knopf ist 44px hoch (WCAG 2.5.5), gesetzt HIER und nicht an .knopf. */
.beispieldaten-fenster {
  max-width: min(92vw, 34rem);
  max-height: calc(100vh - 2 * var(--abstand-l));
  max-height: calc(100dvh - 2 * var(--abstand-l));
  overflow-y: auto;
  overscroll-behavior: contain;
  overflow-wrap: break-word;
}
.beispieldaten-fenster h2 { font-size: var(--headline-small); }
.beispieldaten-fenster p {
  font-size: var(--body-large);
  max-width: var(--lesebreite);
}
.beispieldaten-fenster p + p { margin-top: var(--abstand-m); }
.beispieldaten-fenster .knopf { min-height: 44px; }

/* ---------- Schalter (M3 Switch, vereinfacht) ---------- */

.schalter-zeile {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--abstand-m);
  width: 100%;
  border: 0; background: none; cursor: pointer;
  padding: var(--abstand-xs) 0;
  text-align: left;
  font-size: var(--title-medium);
  font-weight: 500;
}
.schalter-optik {
  flex: 0 0 auto;
  width: 44px; height: 24px;
  border-radius: var(--radius-pille);
  background: var(--surface-variant);
  border: 1px solid var(--outline);
  position: relative;
  transition: background-color var(--dauer-kurz) ease;
}
.schalter-optik::after {
  content: "";
  position: absolute;
  top: 3px; left: 3px;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--outline);
  transition: transform var(--dauer-kurz) ease, background-color var(--dauer-kurz) ease;
}
.schalter-zeile[aria-checked="true"] .schalter-optik { background: var(--primary); border-color: var(--primary); }
.schalter-zeile[aria-checked="true"] .schalter-optik::after { background: var(--on-primary); transform: translateX(20px); }

/* ---------- Schieberegler (Filter, Temperatur, Helligkeit) ----------

   ⛔ EIGENE SPUR STATT accent-color (11.08.2026). Vorher stand hier nur
   `accent-color: var(--primary)`, und alles Übrige zeichnete der Browser.
   Was er zeichnet, ist am Bildschirmabzug gemessen worden:

     hell    Spur #EFEFEF, Rand #B2B2B2   -> Rand 1,95:1 gegen die Karte
     dunkel  Spur #3B3B3B, Rand #858585   -> Rand rund 5:1

   Der helle Regler verfehlte damit die 3:1 aus WCAG 1.4.11, und zwar an der
   einzigen Kante, die sagt, wie weit die Skala reicht. Über accent-color ist
   daran nichts zu ändern: Es färbt Füllung und Griff, nicht die leere Spur.

   ⚠️ Zwei billigere Wege sind ausprobiert und verworfen, beide gemessen:
   `background` auf ::-webkit-slider-runnable-track OHNE appearance:none
   ersetzt die gesamte native Darstellung — übrig blieb ein 16px hoher
   Farbblock ohne Griff und ohne Füllung. Und `box-shadow: inset` auf
   dasselbe Pseudoelement zeichnet den Ring um den 16px hohen Kasten statt um
   die 8px hohe Rille: zwei Striche mit Lücke dazwischen.

   ⚠️ JEDES Vendor-Pseudoelement braucht eine EIGENE Regel. Stünden
   ::-webkit-slider-runnable-track und ::-moz-range-track in einer
   Selektorliste, verwürfen BEIDE Browser die ganze Regel — der Regler wäre
   dann in beiden ungestaltet, ohne dass irgendwo ein Fehler erschiene. */

input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  /* Trefferfläche wie beim nativen Regler; die Spur liegt mittig darin. */
  height: var(--daumen);
  background: none;
  cursor: pointer;
}

/* ⚠️ --daumen steht hier UND als DAUMEN in js/seite-liste.js
   (verbindeReglerWerte). Die Wertblase rechnet ihre Position aus der
   Daumenbreite; laufen die beiden Zahlen auseinander, steht die Blase
   dauerhaft neben dem Griff. Wer eine ändert, ändert beide. */
input[type="range"],
.bereich { --daumen: 20px; --spur: 10px; }

/* Die Füllung ist ein Verlauf über der Spurfarbe, dessen BREITE aus
   --regler-anteil (0…1) kommt; js setzt ihn beim Aufbau und bei jedem
   input-Ereignis. Gerechnet wie die Blase — vom Daumenmittelpunkt aus, nicht
   als glatter Prozentsatz der Spur: Der Griff läuft nicht bis an die Kanten,
   sein Mittelpunkt wandert nur über (Breite − Daumenbreite). Ohne diese
   Korrektur stünde die Füllkante an den Enden bis zu 10px neben dem Griff.

   ⚠️ Fällt js aus, greift der Rückfallwert 0 und die Spur bleibt leer — den
   Wert trägt dann weiterhin der Griff über seine Stellung. Ein Regler ohne
   Füllung ist ablesbar, ein Regler ohne Griff wäre es nicht. */
input[type="range"]::-webkit-slider-runnable-track {
  height: var(--spur);
  border-radius: var(--radius-pille);
  background-color: var(--surface-container-highest);
  background-image: linear-gradient(var(--primary), var(--primary));
  background-repeat: no-repeat;
  background-size:
    calc(var(--daumen) / 2 + var(--regler-anteil, 0) * (100% - var(--daumen))) 100%;
  /* Dieselbe Kante wie bei .balken und .temp-track: 4,69:1 hell,
     5,50:1 dunkel gegen die Kartenfläche. */
  box-shadow: inset 0 0 0 1px var(--outline);
}

input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: var(--daumen);
  height: var(--daumen);
  /* Den 20px-Griff auf der 10px-Spur zentrieren. */
  margin-top: calc((var(--spur) - var(--daumen)) / 2);
  border-radius: 50%;
  background: var(--primary);
  /* Ein Ring in Spurfarbe hält den Griff auch dort ablesbar, wo er auf der
     eigenen Füllung steht — sonst verschmölze Indigo auf Indigo. */
  box-shadow: 0 0 0 2px var(--surface-container-highest);
}

/* Firefox: Die Füllung kann der Browser selbst (::-moz-range-progress), dort
   braucht es --regler-anteil nicht. */
input[type="range"]::-moz-range-track {
  height: var(--spur);
  border-radius: var(--radius-pille);
  background: var(--surface-container-highest);
  box-shadow: inset 0 0 0 1px var(--outline);
}
input[type="range"]::-moz-range-progress {
  height: var(--spur);
  border-radius: var(--radius-pille);
  background: var(--primary);
}
input[type="range"]::-moz-range-thumb {
  width: var(--daumen);
  height: var(--daumen);
  border: 0;
  border-radius: 50%;
  background: var(--primary);
  box-shadow: 0 0 0 2px var(--surface-container-highest);
}

/* ---------- Bereichsregler: zwei Griffe über einer Spur (04.09.2026) ----------

   Rollengewicht und Gebinde. Zwei echte `input[type="range"]` liegen
   deckungsgleich übereinander; gezeichnet wird die Spur DARUNTER, die beiden
   Regler sind durchsichtig gemacht und steuern nur noch ihre Griffe.

   ⛔ EIN NACHGEBAUTES BEDIENELEMENT WÄRE HIER FALSCH. Zwei Regler kosten
   nichts und bringen Tastatur, Vorlesehilfen und den Hochkontrastmodus des
   Systems mit; ein Paar aus <div>-Griffen müßte das alles nachbilden.

   ⚠️ Die Rücknahmen brauchen `input.bereich-griff…`, nicht `.bereich-griff…`
   allein: Die Grundregeln oben stehen an `input[type="range"]` (ein Element
   plus ein Attribut) und wögen sonst schwerer als eine Klasse — die Spur
   bliebe doppelt gezeichnet, einmal je Regler.

   ⚠️ JEDES Vendor-Pseudoelement in einer EIGENEN Regel, aus demselben Grund
   wie oben: In einer gemeinsamen Selektorliste verwürfen beide Browser die
   ganze Regel, und der Regler wäre in beiden ungestaltet. */

.bereich {
  position: relative;
  height: var(--daumen);
  /* Waagerecht ziehen gehört dem Regler, senkrecht rollt die Seite weiter.
     `none` nähme dem Handy das Rollen über dem Regler, `auto` uns das Ziehen. */
  touch-action: pan-y;
}

.bereich-spur {
  position: absolute;
  left: 0; right: 0;
  top: calc((var(--daumen) - var(--spur)) / 2);
  height: var(--spur);
  border-radius: var(--radius-pille);
  background: var(--surface-container-highest);
  /* Dieselbe Kante wie beim einwertigen Regler: 4,69:1 hell, 5,50:1 dunkel. */
  box-shadow: inset 0 0 0 1px var(--outline);
}

/* Gefüllt ist, was ZWISCHEN den Griffen liegt — nicht wie beim einwertigen
   Regler alles bis zum Griff. Die beiden Kanten rechnen vom
   Daumenmittelpunkt aus (--daumen/2 + Anteil · (100% − --daumen)), genau wie
   die Füllung dort und wie wertAusZeiger in js/seite-liste.js: Der Griff
   läuft nicht bis an die Kanten. Ohne die Korrektur stünde die Füllkante an
   den Enden bis zu 10px neben dem Griff. */
.bereich-fuell {
  position: absolute;
  top: 0; bottom: 0;
  left: calc(var(--daumen) / 2 + var(--bereich-von, 0) * (100% - var(--daumen)));
  right: calc(100% - (var(--daumen) / 2 + var(--bereich-bis, 1) * (100% - var(--daumen))));
  border-radius: var(--radius-pille);
  background: var(--primary);
}

input.bereich-griff {
  position: absolute;
  left: 0; top: 0;
  width: 100%;
  margin: 0;
  background: none;
  /* ⛔ Der Regler selbst nimmt keine Zeiger an — sonst fienge der im Markup
     spätere jeden Klick ab, auch die, die dem anderen Griff galten, und
     stünden beide Griffe an derselben Stelle, wäre einer nie wieder zu
     fassen. Das Ziehen führt die Hülle (js/seite-liste.js). */
  pointer-events: none;
}

/* Die eigenen Spuren der beiden Regler verschwinden; sichtbar ist allein
   .bereich-spur darunter. */
input.bereich-griff::-webkit-slider-runnable-track {
  background: none;
  box-shadow: none;
}
input.bereich-griff::-moz-range-track {
  background: none;
  box-shadow: none;
}
/* Firefox füllt bis zum Griff von sich aus — hier füllt .bereich-fuell. */
input.bereich-griff::-moz-range-progress {
  background: none;
}

/* Der Fokusring gehört an den GRIFF, nicht um die ganze Spur: Die beiden
   Regler liegen deckungsgleich übereinander, ein Ring um das Element umrahmte
   den kompletten Balken und sagte nicht, welcher der beiden Griffe gemeint
   ist. */
input.bereich-griff:focus-visible { outline: none; }
input.bereich-griff:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
input.bereich-griff:focus-visible::-moz-range-thumb {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* ⛔ BEIDE WERTE STEHEN HIER, nicht als Blase am Griff. Zwei Blasen
   überlappen, sobald die Griffe nahe beieinanderstehen — in einer 290px
   schmalen Filterspalte ist das der Normalfall. */
.bereich-werte {
  display: block;
  margin-bottom: var(--abstand-xs);
  font-family: var(--schrift-mono);
  font-size: var(--body-medium);
  font-variant-numeric: tabular-nums;
  color: var(--on-surface);
}

/* ---------- Ladeoverlay (LadeOverlay.kt) ----------
   Halbtransparenter schwarzer Schleier, rotierender Dreiviertelkreis.
   Farbe des Kreises: Website-Vorgabe Orange (siehe theme.css --ladekreis). */

.lade-overlay {
  position: fixed; inset: 0;
  z-index: 80;
  background: var(--schleier);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dauer-mittel) ease;
}
.lade-overlay--sichtbar { opacity: 1; pointer-events: auto; }

.ladekreis {
  width: 48px; height: 48px;
  border-radius: 50%;
  /* Dreiviertelkreis: eine Seite bleibt transparent. */
  border: 2px solid var(--ladekreis);
  border-top-color: transparent;
  animation: ladekreis-drehung 1s linear infinite;
}
@keyframes ladekreis-drehung { to { transform: rotate(360deg); } }

/* ---------- Kennwert-Balken (MaterialInfoScreen.BalkenZeile) ---------- */

/* flex-wrap, damit lange Beschriftungen die Zeile nicht aufspannen:
   „Drucker-Anforderungen" plus Balken plus „sehr gering" braucht 267px und
   stand bei 320px Bildschirmbreite über dem Kartenrand. Passt beides nicht
   nebeneinander, rückt der Wert unter die Beschriftung — die Zuordnung
   bleibt eindeutig, weil zwischen zwei Zeilen eine Trennlinie liegt. */
.balken-zeile, .werte-zeile, .sterne-zeile {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--abstand-xxs) var(--abstand-m);
  font-size: var(--body-medium);
}
.balken-zeile dt, .werte-zeile dt, .sterne-zeile dt { color: var(--on-surface-variant); }
.balken-zeile dd, .werte-zeile dd, .sterne-zeile dd { margin: 0; display: flex; align-items: center; gap: var(--abstand-s); }

.balken {
  /* Ausdrücklich block: Als Flex-Element von dd wäre der <span> zwar ohnehin
     blockifiziert, aber diese Rettung hängt am display:flex des Elternteils.
     Explizit gesetzt überlebt die Schiene auch eine Umstellung der Zeile. */
  display: block;
  width: 104px; height: 10px;
  border-radius: var(--radius-pille);
  /* Schiene bewusst surface-container-highest statt outline-variant: das hebt
     den Kontrast jeder Ampelstufe in beiden Modi an.
     Gemessen gegen diese Schiene (WCAG 1.4.11, Ziel 3:1), Stufe 1→5:
       hell   4,5 · 4,2 · 3,6 · 4,5 · 4,9
       dunkel 5,5 · 7,1 · 9,4 · 5,8 · 3,8
     Niedrigster Wert 3,6 (hell, Stufe 3) bzw. 3,8 (dunkel, Stufe 5).
     Die Werte gelten für das abgedunkelte --balken-gelb-fuellung weiter
     unten; mit dem reinen --balken-gelb lag Stufe 3 hell bei 2,8. */
  background: var(--surface-container-highest);
  /* ⛔ Der Ring ist die SKALA, nicht Zierde (11.08.2026). Die Füllung stand
     schon immer über 3:1 gegen die Schiene — was fehlte, war die Schiene
     selbst: Sie steht hell auf Weiß und lag gegen die Kartenfläche bei
     1,03:1. Damit war zwar zu sehen, WIE WEIT der Balken gefüllt ist, aber
     nicht, WOVON — der leere Rest verschwand in der Karte, und „2 von 5"
     sah aus wie ein kurzer Strich ohne Bezug. Ein Balken ohne sichtbares
     Ende ist keine Skala.
     Gemessen: 4,69:1 hell, 5,50:1 dunkel gegen die Kartenfläche.

     ⚠️ Als inset-Schatten und NICHT als border: Ein Rahmen zöge die
     Innenhöhe von 10px auf 8px, und die Füllung (height: 100%) schrumpfte
     mit — der Balken würde dünner, nur weil er eine Kante bekommt. Der
     Schatten liegt innerhalb und kostet kein Layout. Dieselbe Technik wie
     --karten-kante und --feld-kante. */
  box-shadow: inset 0 0 0 1px var(--outline);
  overflow: hidden;
  flex: 0 0 auto;
}
/* display:block ist hier Pflicht, nicht Kosmetik. Die Füllung ist ein <span>
   und damit inline; auf nicht ersetzten Inline-Elementen wirken width und
   height nicht. Ohne diese Zeile blieb die Füllung ein leerer Inline-Kasten
   ohne Ausdehnung — kein Balken, keine Farbe, ganz gleich welche Breite
   JavaScript setzte. Das war die Ursache der leeren Balken. */
.balken-fuellung {
  display: block;
  height: 100%;
  border-radius: var(--radius-pille);
  /* Die Zielbreite steht als Inline-Stil im Markup: Der Balken ist schon
     ohne JavaScript korrekt gefüllt. Die Animation ist nur Verfeinerung —
     das from-Keyframe startet bei 0, das fehlende to-Keyframe ergänzt der
     Browser aus der tatsächlichen Breite. Fällt die Animation aus, bleibt
     die volle Breite stehen; leere Balken kann es dadurch nicht mehr geben.

     Ausgelöst wird das Anwachsen erst, wenn der Balken ins Sichtfeld kommt
     (PD.einblenden.balken setzt die Klasse) — vorher lief es beim Einfügen
     und damit unbemerkt, sobald ein Balken weiter unten auf der Seite stand.
     Ohne JavaScript fehlt die Klasse und der Balken steht sofort auf seiner
     Inline-Breite. */
}
.balken-fuellung--waechst {
  animation: balken-wachsen var(--dauer-mittel) cubic-bezier(0.2, 0, 0, 1);
}
@keyframes balken-wachsen { from { width: 0; } }
@media (prefers-reduced-motion: reduce) {
  .balken-fuellung--waechst { animation: none; }
}

/* Eigenes Gelb NUR für die Balkenfüllung.
   Das reine --balken-gelb erreichte auf der hellen Schiene nur 2,8:1 und
   verfehlte damit die 3:1 aus WCAG 1.4.11. Die Mischung mit --on-surface
   verschiebt den Ton in beiden Themes VOM Untergrund weg: im Hellmodus ist
   --on-surface fast schwarz (dunkler), im Dunkelmodus fast weiß (heller).
   Ein Anteil von 85 % hält den Farbton klar gelb und bringt die Stufen 2–4
   überall über 3:1.
   --stern-gelb bleibt unberührt: Sterne sind Auszeichnung, Balken tragen
   Bedeutung — sie dürfen sich unterscheiden. */
.balken-fuellung { --balken-gelb-fuellung: var(--balken-gelb); }
@supports (background: color-mix(in srgb, red 50%, blue)) {
  .balken-fuellung {
    --balken-gelb-fuellung: color-mix(in srgb, var(--balken-gelb) 85%, var(--on-surface));
  }
}

/* Ampelstufen 1–5 aus den vorhandenen Tokens.
   Für die Zwischenstufen 2 und 4 gibt es KEIN eigenes Token — statt neue
   Hexwerte einzuführen, werden sie mit color-mix aus den vorhandenen
   abgeleitet. Die erste background-Zeile ist der Rückfall für Browser ohne
   color-mix: dort erscheint der jeweils benachbarte Tonwert statt der
   Mischung, nie eine fehlende Farbe. */
.balken-fuellung--stufe-1 { background: var(--balken-gruen); }
.balken-fuellung--stufe-2 {
  background: var(--balken-gruen);
  background: color-mix(in srgb, var(--balken-gruen) 50%, var(--balken-gelb-fuellung));
}
.balken-fuellung--stufe-3 { background: var(--balken-gelb-fuellung); }
.balken-fuellung--stufe-4 {
  background: var(--balken-rot);
  background: color-mix(in srgb, var(--balken-gelb-fuellung) 50%, var(--balken-rot));
}
.balken-fuellung--stufe-5 { background: var(--balken-rot); }

/* „2 / 5" — ein abgelesener Wert, deshalb Schreibmaschinenschrift; ihre
   gleich breiten Ziffern lassen die Werte einer Balkengruppe untereinander
   fluchten. */
.balken-wert {
  font-family: var(--schrift-mono);
  font-weight: 700;
  white-space: nowrap;
}

/* Höchstwert: abgesetzte Zeile plus Klartext-Pille — auffällig auch ohne
   Farbwahrnehmung. Gegenstück zur „Stärke"-Pille der Sternezeilen, hier
   aber in der Fehlerrolle, weil ein hoher Wert auf dieser Skala ungünstig
   ist. */
.balken-zeile--hoechstwert {
  background: var(--surface-container-highest);
  border-radius: var(--radius-s);
  padding: var(--abstand-xs) var(--abstand-s);
}
.balken-pille {
  padding: var(--abstand-xxs) var(--abstand-s);
  border-radius: var(--radius-s);
  background: var(--error-container);
  color: var(--on-error-container);
  font-size: var(--label-small);
  font-weight: 700;
  white-space: nowrap;
}

/* ---------- Sterne (MaterialInfoScreen.BewertungsZeile) ---------- */

.sterne { display: inline-flex; gap: var(--abstand-xxs); }
/* ⚠️ Der LEERE Stern trägt --outline, nicht --outline-variant (11.08.2026).
   Als Trennlinienfarbe lag er bei 1,95:1 hell und 1,73:1 dunkel gegen die
   Kartenfläche — er war damit kaum als Stern zu erkennen, und die Reihe las
   sich als „drei Sterne" statt als „drei von fünf". Jetzt 4,69 / 5,50:1.

   ⛔ Damit ist der Kontrast der REIHE behoben, nicht der Unterschied
   zwischen voll und leer. Der steht hell bei 1,61:1 und ist mit Farbe nicht
   zu retten: Jeder Wert, der den leeren Stern sichtbar macht, rückt ihn auf
   das Gelb zu, und ein dunkleres Gelb macht es schlechter (gemessen 1,35
   bei #A67C00 gegen --outline, 1,10 bei #946E00). Deshalb steht der
   Zahlenwert seit dem 11.08.2026 SICHTBAR daneben — siehe sterneWertHtml in
   js/komponenten.js. Die Sterne sind Auszeichnung, die Zahl ist die
   Auskunft. */
.stern { width: 18px; height: 18px; color: var(--outline); }
.stern--voll { color: var(--stern-gelb); }

.sterne-zeile--staerke {
  background: var(--surface-container-highest);
  border-radius: var(--radius-s);
  padding: var(--abstand-xs) var(--abstand-s);
}
.staerke-pille {
  padding: var(--abstand-xxs) var(--abstand-s);
  border-radius: var(--radius-s);
  background: var(--primary-container);
  color: var(--on-primary-container);
  font-size: var(--label-small);
  font-weight: 700;
}

/* ---------- Snackbar (dezenter Hinweis am unteren Rand) ---------- */

/* Über der Vergleichsleiste, nicht in ihr: Beide erscheinen gleichzeitig,
   sobald etwas gemerkt wird, während eine Vergleichsauswahl steht.

   ⚠️ Der Abstand darf KEINE geschätzte Höhe der Leiste enthalten. Hier stand
   einmal ein aus „80px plus rund 52px" gerechneter Festwert — bei 390px
   Ansichtsbreite bricht die Leiste aber auf 155px um, und die Snackbar lag
   mitten darin: „Gemerkt · Ordner wählen" und die Vergleichsleiste
   übereinandergedruckt und unlesbar. Dieselbe Falle wie bei --kopf-hoehe,
   und dieselbe Antwort: js/app.js misst die Leiste und schreibt
   --vergleichsleiste-hoehe fort; ohne Leiste steht dort 0px. */
.snackbar {
  position: fixed;
  /* ⛔ MITTIG ÜBER left+right+auto, NICHT über left: 50% (18.09.2026).
     Ein fest positioniertes Element mit `left` und `right: auto` bekommt zum
     Schrumpfen nur den Platz RECHTS von `left` — bei left: 50% also die halbe
     Fensterbreite. Gemessen bei 320px: 167px verfügbar, während `max-width`
     294px erlaubte und deshalb nie band. „Gemerkt.“ und der Knopf
     „Ordner wählen“ passen dort nicht nebeneinander; der Knopf schrumpft
     nicht (flex: 0 0 auto), also blieben dem Text 7px — er brach Zeichen für
     Zeichen um (8 Zeilen, Kasten 203px hoch) und stand dadurch nicht mehr am
     unteren Rand, sondern mitten im Bild über den Kacheln.
     Mit beiden Kanten plus `margin-inline: auto` ist der verfügbare Platz die
     volle Fensterbreite abzüglich der Ränder — gemessen 288px bei 320px
     Fenster, und die Leiste ist wieder einzeilig. */
  left: var(--abstand-l);
  right: var(--abstand-l);
  margin-inline: auto;
  width: fit-content;
  bottom: calc(
    var(--nav-hoehe) + var(--abstand-l) +
    var(--vergleichsleiste-hoehe, 0px) + var(--abstand-m)
  );
  z-index: 70;
  max-width: 480px;
  padding: var(--abstand-m) var(--abstand-l);
  border-radius: var(--radius-s);
  background: var(--inverse-surface);
  color: var(--inverse-on-surface);
  font-size: var(--body-medium);
  box-shadow: var(--schatten-schwebend);
  /* ⚠️ Die Bildfolge trägt seit dem 18.09.2026 KEIN translateX mehr — die
     beiden Leisten liegen nicht mehr über einen Transform mittig, sondern
     über ihre Ränder. Mit dem alten translateX(-50%) in jeder Bildstufe
     sprängen sie für die Dauer der Einblendung um ihre halbe Breite nach
     links. Es gibt weiterhin nur EINE Bildfolge für beide. */
  animation: schwebend-einblenden var(--dauer-mittel) ease-out;
  /* Text und wahlweise ein Knopf nebeneinander. */
  display: flex;
  align-items: center;
  gap: var(--abstand-m);
  /* ⚠️ Auffangnetz, falls Text UND Knopf die Höchstbreite sprengen: Dann
     bricht der Knopf in eine zweite Zeile, statt den Text zu zerquetschen.
     Ohne das käme der Fehler von oben bei einer längeren Meldung mit Knopf
     zurück — nur seltener und damit unauffälliger. */
  flex-wrap: wrap;
}
.snackbar-text { flex: 1; min-width: 0; }
/* Der Knopf steht auf der dunklen Leiste und braucht deren Vordergrundfarbe;
   das --primary des Textknopfes wäre dort zu dunkel. */
.snackbar-aktion {
  flex: 0 0 auto;
  color: var(--inverse-on-surface);
}

/* ---------- Vergleichsleiste ----------
   Zeigt eine Vorschau je Auswahl (Obergrenze steht in PD.zustand). Die Reihe
   darf umbrechen, damit die volle Auswahl auf schmalen Fenstern nicht aus dem
   Layout läuft; deshalb auch kein Pillenradius mehr, sondern eine Karte. */

.vergleichsleiste {
  position: fixed;
  /* ⛔ MITTIG ÜBER left+right+auto, NICHT über left: 50% — dieselbe Ursache
     und dieselbe Antwort wie bei der Snackbar (18.09.2026). Mit `left: 50%`
     blieb zum Schrumpfen nur die halbe Fensterbreite: Gemessen stand die
     Leiste auf 320px, 360px und 390px unverändert 214px breit — ihre
     Mindestbreite —, obwohl 288 bis 358px erlaubt waren, und brach deshalb
     auf 155px Höhe um. Auf dem Handy ist das die halbe Bildhöhe für eine
     Leiste, die in zwei Zeilen passt. */
  left: var(--abstand-l);
  right: var(--abstand-l);
  margin-inline: auto;
  width: fit-content;
  bottom: calc(var(--nav-hoehe) + var(--abstand-l));
  z-index: 50;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--abstand-s) var(--abstand-m);
  padding: var(--abstand-s) var(--abstand-m);
  border-radius: var(--radius-l);
  background: var(--inverse-surface);
  color: var(--inverse-on-surface);
  box-shadow: var(--schatten-schwebend);
  font-size: var(--body-medium);
  animation: schwebend-einblenden var(--dauer-mittel) ease-out;
}
.vergleichsleiste .knopf { min-height: 34px; }
.vergleichsleiste .knopf--text { color: var(--inverse-on-surface); }
/* ⚠️ Die frühere Grenze `calc(100vw - --abstand-l * 2)` ist mit den beiden
   Seitenkanten entfallen: Sie sagt jetzt dasselbe, nur schlechter — `100vw`
   schließt den klassischen Rollbalken EIN, das Layout bekommt aber die
   Breite ohne ihn (dieselbe Blink-Eigenheit wie bei der Kopfleisten-Messung
   am 13.08.2026). Die Leiste wäre dadurch um dessen Breite zu breit. */
@media (min-width: 640px) {
  /* Ohne untere Leiste rücken beide nach unten, der Abstand zwischen ihnen
     bleibt derselbe — und er bleibt gemessen, nicht geschätzt. */
  .vergleichsleiste { bottom: var(--abstand-l); }
  .snackbar {
    bottom: calc(var(--abstand-l) + var(--vergleichsleiste-hoehe, 0px) + var(--abstand-m));
  }
}

.vl-auswahl {
  display: flex;
  flex-wrap: wrap;
  gap: var(--abstand-xs);
  list-style: none;
  margin: 0;
  padding: 0;
}

.vl-eintrag {
  display: flex;
  align-items: center;
  gap: var(--abstand-xs);
  padding: var(--abstand-xxs) var(--abstand-xxs) var(--abstand-xxs) var(--abstand-xs);
  border-radius: var(--radius-pille);
  background: color-mix(in srgb, var(--inverse-on-surface) 16%, transparent);
  max-width: 190px;
}

.vl-bild {
  width: 24px; height: 24px;
  flex: 0 0 auto;
  border-radius: var(--radius-pille);
  overflow: hidden;
  background: var(--surface-container-lowest);
}
/* Auch die 24px-Vorschau der Auswahlleiste zeigt ein Produktbild und
   beschneidet es deshalb nicht. Bei dieser Größe geht es weniger ums
   Erkennen als um eine einheitliche Regel: Produktbilder werden nirgends
   angeschnitten. */
.vl-bild img { width: 100%; height: 100%; object-fit: contain; }

.vl-name {
  font-size: var(--label-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vl-entfernen {
  width: 24px; height: 24px;
  flex: 0 0 auto;
  border: 0;
  border-radius: var(--radius-pille);
  background: none;
  color: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.vl-entfernen svg { width: 14px; height: 14px; }
.vl-entfernen:hover { background: color-mix(in srgb, var(--inverse-on-surface) 28%, transparent); }

.vl-zaehler { font-size: var(--label-medium); white-space: nowrap; }

/* ---------- Leerzustände ---------- */

.leerzustand {
  text-align: center;
  padding: var(--abstand-3xl) var(--abstand-l);
  color: var(--on-surface-variant);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--abstand-m);
}
.leerzustand svg { width: 48px; height: 48px; color: var(--primary); }
/* Ein Leerzustand hat genau eine Aussage — die darf auch aussehen wie eine
   Überschrift und nicht wie eine Zeile Fließtext (vorher 16px, also
   Textgröße). */
.leerzustand h2 { font-size: var(--title-large); font-weight: 500; color: var(--on-surface); }
.leerzustand p { font-size: var(--body-large); max-width: 46ch; }

/* Trefferzahl unmittelbar am Suchfeld — ERGÄNZT die Zahl über der Liste.
   Sie schwebt unter dem Feld, damit sie das Layout nicht verschiebt: Ein
   Element, das beim Tippen erscheint und die Liste nach unten drückt, wäre
   beim Lesen störender als hilfreich. */
.such-treffer {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 25;
  margin-top: var(--abstand-xs);
  padding: var(--abstand-xs) var(--abstand-m);
  border-radius: var(--radius-pille);
  background: var(--surface-container-highest);
  color: var(--on-surface-variant);
  font-size: var(--label-large);
  font-weight: 500;
  white-space: nowrap;
  box-shadow: var(--schatten-menue, 0 4px 12px rgba(0,0,0,.2));
  animation: such-treffer-ein var(--dauer-kurz) ease;
}
.such-treffer[hidden] { display: none; }

/* Null Treffer bleibt stehen und trägt die Fehlerfarbe — dieselbe Aussage,
   dieselbe Farbe wie anderswo. Das WORT trägt sie ohnehin. */
.such-treffer--leer { color: var(--error); }

@keyframes such-treffer-ein {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

/* Wer reduzierte Bewegung eingestellt hat, bekommt die Angabe ohne
   Einblendung — sie ist eine Auskunft, kein Effekt. */
@media (prefers-reduced-motion: reduce) {
  .such-treffer { animation: none; }
}

/* ---------- Karten und Felder: Kante im Hellmodus ----------

   ⛔ WER EINE KARTENFLÄCHE TRÄGT, GEHÖRT IN DIESE LISTE. Sie ist die eine
   Stelle, an der --karten-kante ankommt; im Dunkelmodus steht der Wert auf
   `none`, die Regel läuft dort also ins Leere und ändert nichts.

   Warum überhaupt eine Liste und nicht `background: var(--surface-container)`
   als Auswahlkriterium: Das geht in CSS nicht. Und eine Regel auf ALLE
   Elemente mit dieser Fläche wäre falsch — der Punkt im Optionsknopf
   (.sortier-radio::after), die Zellenköpfe der Vergleichstabelle und der
   Bildplatzhalter tragen dieselbe Fläche, sind aber keine Karten. Eine
   Haarlinie um einen 8px-Punkt wäre ein Fleck.

   Die Auswahl folgt einer einzigen Frage: Steht das Element als eigene
   Fläche AUF der Seite? Dann Kante. Liegt es IN etwas? Dann nicht. */
.filament-kachel,
.detail-karte,
.angebot-karte,
.ordner-karte,
.vk-karte,
.produkt-karte,
.material-kachel,
.material-karte,
.antwort-karte,
.vorschlag-karte,
.rechtstext-zeile,
.werbehinweis,
.dr-zeile,
.dr-leer,
.mv-schluss,
.infos-filterzeile,
.filter-sidebar {
  box-shadow: var(--karten-kante);
}

/* ---------- Zustandsebene auf einer EIGENEN Fläche ----------

   ⛔ WER EINE EIGENE FLÄCHE HAT UND AUF HOVER REAGIERT, GEHÖRT IN DIESE
   LISTE. Das ist die zweite der beiden Anwendungsarten aus theme.css: Diese
   Elemente dürfen die Ebene NICHT in `background` setzen, weil das ihre
   Füllung ersetzte und die Seitenfläche durch die Karte scheinen ließe. Sie
   legen sie als background-image darüber.

   ⚠️ Der durchsichtige Verlauf im RUHEZUSTAND ist kein Leerlauf, sondern die
   Bedingung für den Übergang: `background-image` lässt sich zwischen zwei
   Verläufen gleicher Bauart weich rechnen, aus `none` heraus dagegen nur
   hart schalten. Ohne ihn spränge die Kachel, während jeder Chip daneben
   weich wechselt. Kann ein Browser Verläufe nicht interpolieren, schaltet er
   hart — also so, wie es vorher überall war; kaputtgehen kann dabei nichts.

   ⚠️ NICHT über box-shadow, obwohl das kürzer wäre: Alle vier tragen bereits
   --karten-kante als box-shadow, und im Dunkelmodus steht die auf `none`.
   `box-shadow: <ebene>, none` ist ungültig — die Deklaration fiele im
   Dunkelmodus samt Kante weg, ohne Fehlermeldung.

   ⚠️ Und NICHT über ein ::after wie beim Knopf: Eine Kachel trägt Bild und
   Text, ein absolut liegendes Pseudoelement legte sich darüber und tönte
   beides mit ein. Beim Knopf geht es nur, weil dort currentColor über einer
   einzelnen Beschriftung liegt. */
.filament-kachel,
.ordner-karte,
.material-kachel,
.antwort-karte {
  background-image: linear-gradient(transparent, transparent);
  transition:
    background-color var(--dauer-kurz) ease,
    background-image var(--dauer-kurz) ease,
    border-color var(--dauer-kurz) ease;
}
.filament-kachel:hover,
.ordner-karte:hover,
.material-kachel:hover,
.antwort-karte:hover {
  background-image: linear-gradient(var(--zustand-hover), var(--zustand-hover));
}
.antwort-karte:active {
  background-image: linear-gradient(var(--zustand-aktiv), var(--zustand-aktiv));
}

/* Eingabefelder und die Chips daneben: nur die Haarlinie.

   ⚠️ .suchfeld und .chip liegen auf --surface-container-high, also im
   Hellmodus fast auf Papierweiß. Gegen den abgetönten Seitengrund reicht das
   für eine erkennbare Fläche; was fehlte, war die GRENZE — ein Feld muss
   zeigen, wo es anfängt und aufhört, sonst ist es ein heller Fleck.

   ⚠️ .suchfeld steht seit dem 11.08.2026 NICHT mehr in dieser Liste — es
   braucht eine kräftigere Kante als --feld-kante hergibt (siehe dort in
   seiten.css). --feld-kante trägt hell 1,70:1 gegen den Seitengrund; das
   genügt für ein Feld INNERHALB einer Karte, nicht für die große Suchzeile,
   die frei auf der Seitenfläche steht. */
.filter-suchfeld,
.chip,
.ordner-feld input,
.ordner-feld textarea,
/* Das Hex-Feld des Farbkreises hängt an einem Attribut, nicht an einer
   Klasse (js/seite-liste.js) — hier deshalb über den Attributselektor. */
.farbe-frei-hex input {
  box-shadow: var(--feld-kante);
}

/* ---------- Rollleiste der Seite als abrollende Spule ----------

   ⛔ DIE LEISTE DES SYSTEMS WIRD NUR UNTER DIESEM ATTRIBUT VERSTECKT, und
   gesetzt wird es allein von js/rollspule.js — erst, nachdem der Ersatz
   wirklich im Dokument steht. Wer diese Regel je auf `:root` ohne das
   Attribut zieht, nimmt einer Seite ohne Skript die Rollmöglichkeit. Das
   ist der einzige Fehler an dieser Stelle, der die Seite unbedienbar macht.

   Betroffen ist ausschließlich das Dokument. Filterspalte, Handy-Sheet,
   Detail-Seitenspalte und die Klappmenüs rollen weiter mit der Leiste des
   Systems — eine 20px-Spule in einer 420px-Spalte wäre zu klein, und drei
   davon nebeneinander wären Unruhe statt Marke. */
:root[data-rollspule="an"] { scrollbar-width: none; }
:root[data-rollspule="an"]::-webkit-scrollbar { width: 0; height: 0; }

/* Die Bahn liegt genau in dem Streifen, den die Systemleiste räumt (15px
   gemessen). Die Symbole der rechten Hauptleiste enden 17px vor der
   Fensterkante — die Bahn kommt ihnen also nicht ins Gehege, und das
   Seitengerüst musste für sie nicht angefasst werden.

   z-index 46: über der Hauptleiste (45), unter Overlays und Sheets (60). */
.rollspule {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 46;
  width: 16px;
  /* Ohne das übernähme der Browser die Berührung für sein eigenes Rollen,
     und das Ziehen mit dem Finger bliebe wirkungslos. */
  touch-action: none;
  cursor: pointer;
}

/* Ohne Rollweg keine Anzeige — eine Leiste über eine Seite, die nicht
   rollt, wäre eine Aussage über nichts. */
.rollspule--aus { display: none; }

/* Der zurückgelegte Weg als dünne Linie im Filamentstil: von oben
   wachsend, in der Markenfarbe.

   ⚠️ Sie wächst über `transform: scaleY()`, nicht über `height` — eine
   Höhenänderung je Bild wäre Layout, und genau das soll hier nicht
   passieren. transform-origin oben, damit sie von der Seitenoberkante her
   länger wird. */
.rollspule-spur {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 2px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--primary);
  opacity: 0.55;
  transform-origin: top center;
  transform: scaleY(0);
}

/* ---------- Fahrt und Drehung sind ZWEI Elemente ----------

   ⛔ Der äußere Kasten ist genau so hoch wie der WEG, den die Spule
   zurücklegt: die Bahn minus ihren eigenen Durchmesser. Ein `translateY` in
   Prozent bezieht sich auf die eigene Höhe — `translateY(37%)` stellt die
   Spule damit auf 37 % des Weges, und das Skript muss die Bahnhöhe
   überhaupt nicht kennen.

   Vorher rechnete es die Fahrt in Pixeln aus einer zwischengespeicherten
   Bahnhöhe. Wurde die Bahn je in verborgenem Zustand vermessen, stand dort
   dauerhaft 0 — und die Spule bewegte sich nie wieder, ohne jede
   Fehlermeldung. Eine Geometrie, die man nicht speichert, kann nicht
   veralten. Wer die 20px hier ändert, ändert sie auch in js/rollspule.js
   (SPULE): Die beiden Zahlen beschreiben dasselbe Maß. */
.rollspule-fahrt {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 20px;
  transform: translateY(0);
}

/* Die Spule selbst — sie trägt nur noch die DREHUNG. `will-change` macht
   sie zur eigenen Ebene; sie dreht sich in jedem Bild, in dem gerollt
   wird. */
.rollspule-griff {
  position: absolute;
  top: 0;
  left: 50%;
  width: 20px;
  height: 20px;
  margin-left: -10px;
  display: block;
  will-change: transform;
}
.rollspule-griff svg { display: block; }

/* Farben aus den Themerollen — hell wie dunkel sichtbar, ohne einen
   eigenen Wert je Schema. Die Wicklung trägt Indigo wie die Wortmarke,
   der Kern das Cyan des Logos. */
.rs-wicklung {
  fill: var(--surface-container-lowest);
  stroke: var(--primary);
  stroke-width: 3;
}
.rs-kern { fill: none; stroke: var(--secondary); stroke-width: 2; }
.rs-speichen { stroke: var(--primary); stroke-width: 2; stroke-linecap: round; }
.rs-nabe { fill: var(--primary); }

/* Angefasst: etwas kräftiger, damit das Ziehen quittiert wird. */
.rollspule--zieht .rollspule-griff { filter: brightness(1.15); }
.rollspule--zieht .rollspule-spur { opacity: 0.85; }

/* ⚠️ Die Anzeige gibt es nur auf breiten Fenstern — dieselbe Bedingung wie
   in js/rollspule.js (NUR_DESKTOP), und zwar ALLEIN die Breite. Sie steht
   hier ein zweites Mal, weil ein Fenster nach dem Aufbau schmaler werden
   kann: Dann verschwindet die Spule, und die Regel oben gibt die
   Systemleiste frei, weil sie an derselben Bedingung hängt.

   ⛔ KEIN `hover: hover` MEHR (03.09.2026). Bis dahin stand hier zusätzlich
   `, not all and (hover: hover)`. Auf einem Surface mit Touchscreen meldet
   Chromium dauerhaft `hover: none` — auch mit Maus —, und dieser Zweig gab
   die Systemleiste dort wieder frei, selbst wenn das Skript die Spule
   aufgebaut hätte: Leiste und Spule hätten NEBENEINANDER gelegen. Wer hier
   eine Bedingung ergänzt, ergänzt sie in js/rollspule.js — und umgekehrt. */
@media not all and (min-width: 1024px) {
  :root[data-rollspule="an"] { scrollbar-width: auto; }
  :root[data-rollspule="an"]::-webkit-scrollbar { width: auto; height: auto; }
  .rollspule { display: none; }
}

/* Bewegungsreduzierung: Die Spule WANDERT weiter — das ist die Auskunft,
   wo man auf der Seite steht, und die darf nicht wegfallen. Nur die
   Drehung entfällt; js/rollspule.js lässt den rotate-Teil dann weg. */

/* ==================================================================
   ZURÜCK-PFEIL (12.09.2026)

   Oben links auf jeder Seite außer der Startseite, in EINER Zeile mit den
   Brotkrumen, wo es sie gibt. Markup und Ziel: js/komponenten.js
   (zurueckzeileHtml), Rückfallziele je Route: js/router.js (ZURUECK_ZIEL).

   ⛔ ER STEHT IM SEITENINHALT UND NICHT IN DER KOPFLEISTE, und das ist
   gemessen so entschieden (Entscheidung vom 12.09.2026): In der Leiste
   bleiben dem Suchfeld bei 1024px heute 130px und bei 320px liegen 48px
   zwischen Marke und Druckerpille — ein 44px-Knopf nähme beides weg. Er
   verhält sich damit beim Rollen wie die Brotkrumen, mit denen er in einer
   Zeile steht: Er rollt mit dem Inhalt weg und ist beim Hochrollen sofort
   wieder da. Eine eigene Klebe-Mechanik bekommt er NICHT — sie kostete auf
   jeder Seite eine klebende Zeile Höhe und einen weiteren Eintrag in der
   Empfängerliste der gemessenen Kopfhöhe (css/layout.css).
   ================================================================== */

.zurueckzeile {
  display: flex;
  align-items: center;
  gap: var(--abstand-s);
  /* Der Abstand nach unten gehört der ZEILE, nicht den Brotkrumen darin:
     Innerhalb einer Flex-Zeile schöbe deren eigener margin-bottom sie gegen
     den Pfeil daneben nach oben. Die Regel darunter nimmt ihn deshalb
     zurück. */
  margin-bottom: var(--abstand-l);
}

.zurueckzeile .brotkrumen { margin-bottom: 0; }

/* 44 × 44px und damit größer als .symbol-knopf (40px) — auf Ansage vom
   12.09.2026 das Mindestmaß für diese Fläche. Es ist dasselbe Maß wie der
   Burger der Kopfleiste, der im Projekt als Mindestmaß dokumentiert ist, und
   liegt weit über den 24px aus WCAG 2.5.8.

   ⚠️ flex-shrink: 0 ist Pflicht: Als Flex-Kind neben umbrechenden Brotkrumen
   gäbe er sonst Breite ab, und die Trefferfläche schrumpfte mit — unbemerkt,
   weil der Pfeil darin weiter mittig steht. */
.zurueck-knopf {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  /* Der Pfeil soll mit der linken Kante des Inhalts fluchten, nicht mit der
     Kante seiner Trefferfläche. Der negative Einzug holt die Polsterung
     wieder heraus, die das 44px-Quadrat um das 22px-Symbol legt. */
  margin-left: -11px;
  border-radius: var(--radius-pille);
  background: none;
  color: var(--on-surface-variant);
  text-decoration: none;
  transition: background-color var(--dauer-kurz) ease, color var(--dauer-kurz) ease;
}
.zurueck-knopf:hover { background: var(--zustand-hover); color: var(--on-surface); }
.zurueck-knopf:active { background: var(--zustand-aktiv); }
.zurueck-knopf svg { width: 22px; height: 22px; }

/* ⚠️ In einer Flex- oder Rasterspalte mit `gap` ADDIERT sich der Außenabstand
   der Zeile zu dieser Lücke. Genau ein Kopfbereich des Projekts ist so gebaut
   (`.wissen-kopf`, eine Flex-Spalte mit 8px Lücke); dort stünden sonst 32px
   zwischen Pfeil und Kicker, während überall sonst 24px stehen. Wer einen
   weiteren Kopfbereich als Flex-Spalte baut, trägt ihn hier nach. */
.wissen-kopf .zurueckzeile { margin-bottom: 0; }

/* ---------- Affiliate-Schalter: welche der beiden Fassungen gilt ----------

   ⛔ FÜNF DER SECHS STELLEN, DIE ÜBER DIE PROVISION SPRECHEN, TRAGEN BEIDE
   FASSUNGEN IM MARKUP — Fußzeile, Werbeband beider Listen (in beiden Längen),
   der Finanzierungsabschnitt der Startseite, der Vertrauensblock der
   Detailseite und die Zweckzeile auf /ueber. Welche zu sehen ist, entscheidet
   allein dieses Attribut; keine Ansicht kennt den Schalter. Die sechste ist
   die Meta-Beschreibung von /affiliate — ein Attributwert, der sich nicht per
   CSS umschalten lässt; sie liest PD.affiliate.AKTIV unmittelbar.

   Gesetzt wird `data-affiliate` von js/affiliate.js, und zwar im <head> vor
   dem ersten Bild. Die Begründung dafür steht dort; hier zählt die Folge:

   ⛔ GESETZT WIRD NUR `display: none`, NIE EIN POSITIVER WERT. Die sichtbare
   Fassung behält damit ihre natürliche Darstellung — die Fassungen sind mal
   inline (`<span>` im Werbeband), mal Block (`<h2>`, `<p>`), mal
   Listenpunkt. Eine Regel wie `.af-an { display: inline }` machte aus der
   Überschrift der Startseite eine Zeile mitten im Absatz, und zwar lautlos.

   ⛔ DIE VORGABE IST „AUS": Fehlt das Attribut ganz, gilt die Fassung ohne
   Provision. Fehlen kann es nur ohne JavaScript — und dann zeichnet der
   Router keine Angebotsliste, es gibt also keinen „Zum Shop"-Link, der zu
   kennzeichnen wäre. Sichtbar bleibt allein der statische Fußbereich, und für
   den ist „keine Werbelinks" die richtige Auskunft. */
:root:not([data-affiliate="an"]) .af-an { display: none; }
:root[data-affiliate="an"] .af-aus { display: none; }
