/* ============================================================
   polyDesign — col/  (Columns: Bausteine für side- & main-Spalte)
   Baut auf tokens.css + elem.css + mod.css + comp.css auf.
   Raster: Desktop = side-column (240px) + main-column; Tablet/
   Mobile keine side-column (Reflow passiert im sect/-Level).
   Präfix „side-" = gehört in die 240px-Seitenspalte.
   Side-Spalten füllen die Breite ihrer Spalte (Default 240px).
   ============================================================ */

/* ---------- col/side-kapitel-num ----------------------------
   Figma 2039:562 — 240er-Quadrat, Kapitelnummer (h1) rechts oben.
------------------------------------------------------------- */
.col-side-kapitel-num{
  box-sizing:border-box;
  width:100%;            /* Breite = side-column (240px) */
  /* Höhe kommt aus dem Row-Stretch der Section (align-items:stretch) —
     kein height:100%, das würde auf Content-Höhe kollabieren. */
  padding:var(--pad-big);
  /* Fläche + Typo sind IMMER das Hover-/Highlight-Paar des Themes
     (--accent / --accent-ink) — nicht fest polyGreen. Auf green kehrt sich
     das Paar um, die Nummer bleibt damit auf jedem Theme lesbar. */
  background:var(--accent);
  border:var(--linewidth) solid var(--poly-line);
  display:flex; flex-direction:column; align-items:flex-start;
}
.col-side-kapitel-num__num{
  width:100%; text-align:right; margin:0;
  font-family:var(--font-sans); font-weight:700;
  font-size:var(--h1-size); line-height:var(--h1-lh); letter-spacing:var(--h1-ls);
  color:var(--accent-ink);
}
/* Tablet-Ausnahme: ab Tablet nicht mehr 240px, sondern nur so breit wie
   die Zahl (+Padding). Schaltet automatisch am Breakpoint. */
@media (max-width:960px){
  .col-side-kapitel-num{ width:fit-content; }
  .col-side-kapitel-num__num{ width:auto; }
}

/* ---------- col/side-kapitel+links (für Einträge) -----------
   Variante der Kapitel-Nummer: die Zahl sitzt in einem eigenen,
   nur zahlhohen Rahmen (Fläche + Textfarbe aus den polymono-Tokens,
   inline via numColor/numInk). Darunter beliebig viele Links (einzeln
   editierbar, wie side-links). Kein Link → nur der Zahl-Rahmen.
   Reflow (Tablet/Mobile): Zahl + Links wandern unter die letzte
   Main-Reihe (side.pos=bottom), genau wie side-links.
------------------------------------------------------------- */
.col-side-kapitel-links{
  box-sizing:border-box; width:100%;
  display:flex; flex-direction:column; align-items:stretch;
}
.col-side-kapitel-links > * + *{ margin-top:calc(-1 * var(--linewidth)); }  /* Naht Zahl|Links */
.col-side-kapitel-links__num{
  box-sizing:border-box; width:100%;
  padding:var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
  font-family:var(--font-sans); font-weight:700;
  font-size:var(--h1-size); line-height:var(--h1-lh); letter-spacing:var(--h1-ls);
  text-align:right;
  /* Fläche + Textfarbe kommen inline (numColor / numInk) */
}
.col-side-kapitel-links__links{ width:100%; }
.col-side-kapitel-links__links > .nav-item{ width:100%; }
/* Leerer Füll-Rahmen unter den Links: streckt auf die restliche Eintragshöhe,
   damit die Side gerahmt zum Grid passt (wie side-links-vertikal__filler).
   Standalone (ohne Höhe) kollabiert er auf 0 → unsichtbar. */
.col-side-kapitel-links__filler{
  flex:1 1 auto; min-height:0;
  border:var(--linewidth) solid var(--poly-line);
}
/* Reflow-Leiste (Tablet/Mobile): nur die horizontalen Links (die Zahl bleibt in
   der Side bzw. entfällt auf Mobile — siehe module/sect/sect.css, Modus kaplinks). */
.col-kapitel-links-reflow{ display:flex; flex-direction:column; align-items:stretch; }
.col-kapitel-links-reflow > * + *{ margin-top:calc(-1 * var(--linewidth)); }

/* ---------- col/side-zusatzinfo-tags ------------------------
   Figma 2039:574 — rechtsbündig: Trenner (20px) + Label (mono)
   + mod/tags-vertikal (volle Breite). gap padding-medium.
------------------------------------------------------------- */
.col-side-zusatzinfo-tags{
  box-sizing:border-box; width:100%;
  padding:var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
  display:flex; flex-direction:column; align-items:flex-end;
  gap:var(--pad-medium);
}
.col-side-zusatzinfo-tags__trenner{
  width:20px; height:0; border-top:var(--linewidth) solid var(--ink);
}
.col-side-zusatzinfo-tags__label{
  width:100%; text-align:right; margin:0;
  font-family:var(--font-mono); font-weight:500;
  font-size:var(--tag-size); line-height:var(--tag-lh); color:var(--ink);
}
.col-side-zusatzinfo-tags .mod-tags-vertikal{ width:100%; }

/* ---------- col/side-text (freier Text in der Seitenspalte) --
   Schwester von col/text für die 240er-Seitenspalte: freies Markup, aber
   rechtsbündig und mit dem kleinen 20px-Trenner aus side-zusatzinfo-tags —
   die Geometrie (Rahmen, padding-big, gap-medium, Ausrichtung rechts) ist
   bewusst identisch zu col/side-zusatzinfo-tags, damit beide Sides
   nebeneinander gleich sitzen. Ohne Klammer gilt der Tag-Stil.
   Reflow (Tablet/Mobile, pos=top): dieselben Blöcke als volle Zeile ÜBER dem
   Content — dort linksbündig, weil rechtsbündiger Text über die volle
   Seitenbreite schlecht liest. Per Toggle „Auf Tablet/Mobile zeigen"
   abschaltbar; dann fällt die Side ab ≤960 ersatzlos weg.
------------------------------------------------------------- */
.col-side-text{
  box-sizing:border-box; width:100%;
  padding:var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
  display:flex; flex-direction:column; align-items:flex-end;
  gap:var(--pad-medium);
  text-align:right;
}
.col-side-text > *{ margin:0; }
/* Trenner wie in side-zusatzinfo-tags: 20px, Linie in der Ink-Farbe. Der
   gap trägt den Abstand, deshalb keine eigenen Margins. */
.col-side-text__trenner{
  width:20px; height:0; flex:0 0 auto;
  border-top:var(--linewidth) solid var(--ink);
}
/* Reflow-Zeile: volle Breite, linksbündig, Zeilen-Geometrie wie der
   zusatzinfo-Reflow (padding-medium/padding-big). */
.col-side-text--reflow{
  align-items:flex-start; text-align:left;
  padding:var(--pad-medium) var(--pad-big);
  gap:var(--pad-small);
}

/* ---------- col/side-links-vertikal -------------------------
   Figma 2020:804 — vertikale Liste aus elem/nav-item-vertikal
   (volle Breite); Rahmen ergibt die Trennlinien.
------------------------------------------------------------- */
/* Linien: Container = .lines-v (Markup) überlappt die nav-items; kein
   eigener Rahmen — die nav-item-Rahmen bilden den Frame. */
.col-side-links-vertikal{
  box-sizing:border-box; width:100%;
}
.col-side-links-vertikal > .nav-item{ width:100%; }
/* Leerer Füll-Rahmen am Ende: streckt sich auf die restliche Section-Höhe,
   damit die Seitenspalte auch unter der letzten nav-item gerahmt bleibt
   (statt borderloser Leere). Überlappt via .lines-v -1px. Standalone (ohne
   vorgegebene Höhe) kollabiert er auf 0 → unsichtbar. */
.col-side-links-vertikal__filler{
  flex:1 1 auto; min-height:0;
  border:var(--linewidth) solid var(--poly-line);
}

/* ---------- col/links-horizontal ----------------------------
   Figma 2112:10957 — horizontale Reihe aus elem/nav-item-vertikal
   (Inhaltsbreite statt fixe 240px).
------------------------------------------------------------- */
/* Umbrechende Reihe → Linien via gap+Hintergrund (Überlappung zickt bei
   Umbruch). Container-Hintergrund = Linienfarbe scheint durch die 1px-Lücken. */
.col-links-tablet-mobile-horizontal{
  display:flex; flex-wrap:wrap; align-items:stretch;
  gap:var(--linewidth); background:var(--poly-line);
  border:var(--linewidth) solid var(--poly-line);
}
/* Items strecken sich gleichmäßig auf die volle Breite (je 1/N pro Zeile).
   min-width:max-content = nicht unter die Inhaltsbreite schrumpfen → statt-
   dessen umbrechen; die neue Zeile verteilt sich dann wieder gleichmäßig.
   Kein eigener Rahmen (die Lücken sind die Linien), deckender Hintergrund. */
.col-links-tablet-mobile-horizontal > .nav-item{
  flex:1 1 0; min-width:max-content; width:auto; justify-content:center;
  border:0; background:var(--surface);
}
/* Der explizite beige Hintergrund (deckt die Linien-Lücken) überschrieb sonst
   die nav-item-States → Hover/Active hier explizit nachziehen (gelten damit in
   allen Breakpoints, nicht nur Desktop-vertikal). */
.col-links-tablet-mobile-horizontal > .nav-item:hover,
.col-links-tablet-mobile-horizontal > .nav-item.is-hover{ background:var(--poly-green); }
.col-links-tablet-mobile-horizontal > .nav-item.is-active{ background:var(--ink); }
/* Mobile: alle Items gestapelt, je 100%. */
@media (max-width:480px){
  .col-links-tablet-mobile-horizontal > .nav-item{ min-width:100%; }
}

/* ---------- col/kapitel-headline ----------------------------
   Figma 2112:5658 — Card (padding-big) mit H1 + 60px-Trenner +
   Body, gap padding-big. (Wie headline-trenner-text, aber H1.)
------------------------------------------------------------- */
.col-kapitel-headline{
  box-sizing:border-box; width:100%;
  padding:var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
  display:flex; flex-direction:column; align-items:flex-start;
  gap:var(--pad-big);
}
.col-kapitel-headline__h1{
  width:100%; margin:0;
  font-family:var(--font-sans); font-weight:700;
  font-size:var(--h1-size); line-height:var(--h1-lh); letter-spacing:var(--h1-ls);
  color:var(--ink);
}
.col-kapitel-headline__trenner{
  width:60px; height:0; border-top:var(--linewidth) solid var(--ink);
}
.col-kapitel-headline .t-body{ width:100%; }

/* ---------- col/text ----------------------------------------
   Freier Textblock: der Autor tippt sein eigenes Markup
   ([h1] / [h2] / [h3] / [body] / [body-bold] / [button-big] /
   [button] / [tag] / [trenner]); ohne Klammer gilt body.
   Rahmen + padding wie kapitel-headline (per „Rahmen" abschaltbar),
   Grundabstand pad-medium. Der Trenner ist 60px breit und hat
   ringsum pad-big — exakt wie in kapitel-headline.
------------------------------------------------------------- */
.col-text{
  box-sizing:border-box; width:100%;
  padding:var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
  display:flex; flex-direction:column; align-items:flex-start;
  gap:var(--pad-medium);
}
.col-text--plain{ padding:0; border:0; }
.col-text > *{ width:100%; }
/* Zentriert (H & V): Inhalt horizontal mittig (text-align + align-items) und
   vertikal mittig (justify-content — wirkt, sobald das Modul höher als sein
   Inhalt ist, z.B. in einer Grid-/Reihen-Zelle mit height:100%). */
.col-text--center{ align-items:center; justify-content:center; text-align:center; }
.col-text--center .col-text__col{ align-items:center; text-align:center; }
/* Lesebreite (Option „Lesebreite begrenzen"): begrenzt die ZEILENLÄNGE, nicht
   das Modul — der Rahmen bleibt also volle Spaltenbreite und die Linienführung
   unangetastet. Der Wert ist bewusst in PIXEL: ch bezieht sich auf die Schrift
   des jeweiligen Elements, dadurch wurde eine H0 doppelt so breit wie der Body
   (gemessen). Ein fester Wert gilt für alle Schnitte gleich und begrenzt auch
   [trenner-full] auf Textbreite. 560px = gemessene 66 Zeichen bei 18px Body,
   das typografische Optimum. Pro Modul über --measure anpassbar.
   Löst das Ausgangsproblem (Body-Text läuft im Breitbild zu weit), ohne dass
   irgendetwas global umgestellt wird. */
.col-text--measure{ --measure:560px; }
.col-text--measure > *{ max-width:var(--measure); }

/* Mehrspaltiger Textabschnitt: [columns-start]…[columns-end]. N gleich breite
   Spalten (--text-cols), Spaltenabstand = Modul-Padding (--pad-big). Jede
   Spalte stapelt ihre Blöcke in der gewohnten col-text-Rhythmik. Unter 768px
   bricht alles auf eine Spalte um (Body-Text bleibt lesbar). */
.col-text__cols{
  display:grid;
  grid-template-columns:repeat(var(--text-cols, 2), minmax(0, 1fr));
  gap:var(--pad-big);
  align-items:start;
  width:100%;
}
.col-text__col{
  display:flex; flex-direction:column; align-items:flex-start;
  gap:var(--pad-medium); min-width:0;
}
.col-text__col > *{ width:100%; }
@media (max-width:768px){ .col-text__cols{ grid-template-columns:1fr; } }
/* 60px-Linie wie kapitel-headline; die Zusatz-Margins heben den
   Grundabstand (pad-medium) auf pad-big an — oben wie unten. */
.col-text__trenner{
  width:60px; height:0; flex:0 0 auto;
  border-top:var(--linewidth) solid var(--ink);
  margin-top:calc(var(--pad-big) - var(--pad-medium));
}
.col-text > .col-text__trenner{ width:60px; }
/* [trenner-full]: Linie über die volle Breite (statt der 60px-Kurzlinie).
   Nach der 60px-Regel notiert, damit die volle Breite bei gleicher
   Spezifität gewinnt. */
.col-text__trenner--full,
.col-text > .col-text__trenner--full,
.col-text__col > .col-text__trenner--full{ width:100%; }
.col-text__trenner + *{ margin-top:calc(var(--pad-big) - var(--pad-medium)); }
/* Erstes/letztes Element ohne Zusatzabstand zum Rahmen. */
.col-text > .col-text__trenner:first-child{ margin-top:0; }
/* In einer Grid-/Reihen-Zelle liefert das Raster die Außenlinien. */
.poly-cell .col-text{ border:0; height:100%; }

/* ---------- col/kapitel+bento -------------------------------
   Figma 2112:6741 — kapitel-headline (links) + comp/bento-grid
   2×2 (rechts), 2 Spalten. Mobile gestapelt.
------------------------------------------------------------- */
.col-kapitel-bento{
  position:relative;
  display:grid; grid-template-columns:repeat(2, minmax(0,1fr));
  align-items:stretch;   /* Bento füllt die volle Frame-Höhe (= Headline) */
  border:var(--linewidth) solid var(--poly-line);
}
/* (Bento teilt die Frame-Höhe in gleiche Zeilen — jetzt in comp.css global.) */
/* Kein doppelter Strich: Kinder ohne eigenen Rahmen, Frame = Outer-Border,
   Mittellinie = Pseudo-Element (volle Höhe). */
.col-kapitel-bento > .col-kapitel-headline,
.col-kapitel-bento > .comp-bento{ border:0; }
.col-kapitel-bento::after{
  content:""; position:absolute; top:0; bottom:0; left:50%;
  width:var(--linewidth); background:var(--poly-line);
}
@media (max-width:480px){
  .col-kapitel-bento{ grid-template-columns:1fr; }
  .col-kapitel-bento::after{ display:none; }
}

/* ---------- col/kapitel-headline-mobile ---------------------
   Figma 2116:11361 — Mobile-Variante von kapitel-headline.
   Grüner Frame (Nummer /02 mono + H1, Text dunkel), darunter
   separater umrahmter Body-Text (abschaltbar = Element weglassen).
   Keine Trennerlinie. In der Bento-Version kommt darunter die
   mobile comp/bento-grid.
------------------------------------------------------------- */
.col-kapitel-headline-mobile{
  display:flex; flex-direction:column; align-items:flex-start; width:100%;
}
/* Linien: Kopf- und Text-Kasten grenzen Kante-an-Kante → -1px-Überlappung
   (aus zwei Rahmen wird eine Linie). Bei „ohne Text" (nur Kopf) kein Effekt. */
.col-kapitel-headline-mobile > * + *{ margin-top:calc(-1 * var(--linewidth)); }
.col-kapitel-headline-mobile__head{
  box-sizing:border-box; width:100%;
  background:var(--accent);
  border:var(--linewidth) solid var(--poly-line);
  padding:var(--pad-medium-mobile) var(--pad-big-mobile);
  display:flex; flex-direction:column; align-items:flex-start;
}
/* Kopf trägt dasselbe Akzentpaar wie col/side-kapitel-num (Fläche --accent,
   Typo --accent-ink) — er ist die Umbruch-Variante derselben Kapitelnummer. */
.col-kapitel-headline-mobile__num{
  margin:0; font-family:var(--font-mono); font-weight:500;
  font-size:var(--button-big-size); line-height:var(--button-big-lh);
  color:var(--accent-ink);
}
.col-kapitel-headline-mobile__h1{
  margin:0; width:100%;
  font-family:var(--font-sans); font-weight:700;
  font-size:var(--h1-size); line-height:var(--h1-lh); letter-spacing:var(--h1-ls);
  color:var(--accent-ink);
  overflow-wrap:normal;   /* Wörter nicht mitten drin brechen */
}
.col-kapitel-headline-mobile__text{
  box-sizing:border-box; width:100%;
  border:var(--linewidth) solid var(--poly-line);
  padding:var(--pad-big-mobile);
}
.col-kapitel-headline-mobile__text .t-body{ width:100%; }

/* ---------- col/zusatzinfo-tags-tablet-mobile-horizontal ----
   Figma 2116:11336 — Tablet/Mobile-Reflow von side-zusatzinfo-tags:
   Label links + umbrechende Tags rechts, in einer umrahmten Zeile.
------------------------------------------------------------- */
.col-zusatzinfo-tags-tablet-mobile-horizontal{
  display:flex; align-items:flex-start; gap:var(--pad-medium);
  padding:var(--pad-medium) var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
}
.col-zusatzinfo-tags-tablet-mobile-horizontal__label{
  flex:0 0 auto; text-align:right; margin:0;
  font-family:var(--font-mono); font-weight:500;
  font-size:var(--tag-size); line-height:var(--tag-lh); color:var(--ink);
}
.col-zusatzinfo-tags-tablet-mobile-horizontal .mod-tags-horizontal{
  flex:1 1 0; min-width:0; padding-top:0; justify-content:flex-end;
}

/* ---------- col/content-left/right-textblock ----------------
   Figma 2072:3667 / 2071:3012 — col/video + comp/head+body
   nebeneinander. left = Video links (Markup: video zuerst),
   right = Video rechts (Markup: head+body zuerst). Video füllt
   die Text-Höhe. Mobile: gestapelt.
------------------------------------------------------------- */
/* Linien: Rahmen aus den Zellen (video + head-body), die sich um 1px
   überlappen; Container ohne eigenen Rahmen. */
.col-content-left-textblock, .col-content-right-textblock{
  display:flex; align-items:stretch;
}
.col-content-left-textblock > *, .col-content-right-textblock > *{ flex:1 1 0; min-width:0; }
.col-content-left-textblock > * + *, .col-content-right-textblock > * + *{
  margin-left:calc(-1 * var(--linewidth));
}
.col-content-left-textblock > .col-video,
.col-content-right-textblock > .col-video{ aspect-ratio:auto; align-self:stretch; }
@media (max-width:480px){
  .col-content-left-textblock, .col-content-right-textblock{ flex-direction:column; }
  /* Spalte: natürliche Höhe statt Basis 0 (sonst kollabiert das Video). */
  .col-content-left-textblock > *, .col-content-right-textblock > *{ flex:0 0 auto; }
  .col-content-left-textblock > * + *, .col-content-right-textblock > * + *{
    margin-left:0; margin-top:calc(-1 * var(--linewidth));
  }
  .col-content-left-textblock > .col-video,
  .col-content-right-textblock > .col-video{ aspect-ratio:16 / 9; }
  /* right: Video auf Mobile auch oben (wie left). Video nach vorn holen,
     Überlappung an die neue Naht (Textblock zieht 1px hoch). */
  .col-content-right-textblock > .col-video{ order:-1; margin-top:0; }
  .col-content-right-textblock > .comp-head-body{ margin-top:calc(-1 * var(--linewidth)); }
}

/* ---------- col/mind-motion-soul ----------------------------
   Figma 2112:8560 — 3 Zellen (Icon + Wort).
   Desktop (>960): immer 3 nebeneinander, je 1/3 über volle Breite,
     H2, Icon 40px.
   Tablet (481–960): dieselbe Logik, SOLANGE jedes Wort in seine
     Zeile passt (Wörter brechen nie in sich um). Passt eins nicht
     mehr → alle drei untereinander, Rahmen volle Breite. Der
     Umschaltpunkt kommt aus dem Inhalt (Wortlänge × Schrift), nicht
     aus festem px-Wert → `module/col/mind-motion-soul.js` setzt dafür im
     Tablet-Bereich `.is-stacked` (CSS kann Text nicht messen).
   Mobile (≤480): immer untereinander, Rahmen volle Breite, H3,
     Icon 30px.
------------------------------------------------------------- */
.col-mind-motion-soul{
  display:flex; flex-wrap:nowrap;
}
.col-mind-motion-soul__item{
  flex:1 1 0; min-width:0;
  display:flex; align-items:center; gap:var(--pad-medium);
  padding:var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
}
.col-mind-motion-soul__icon{ flex:0 0 auto; width:40px; height:40px; color:var(--ink); display:block; }
.col-mind-motion-soul__icon .poly-ic{ width:100%; height:100%; display:block; }
.col-mind-motion-soul__label{
  min-width:0; overflow:hidden;
  white-space:nowrap; overflow-wrap:normal;   /* Wort bricht nie in sich um */
  font-family:var(--font-sans); font-weight:700;
  font-size:var(--h2-size); line-height:var(--h2-lh); letter-spacing:var(--h2-ls);
  color:var(--ink);
}
/* Linien: benachbarte Zellen überlappen 1px (Reihe = horizontal). */
.col-mind-motion-soul > .col-mind-motion-soul__item + .col-mind-motion-soul__item{
  margin-left:calc(-1 * var(--linewidth));
}
/* Gestapelt (Tablet: vom JS gesetzt, wenn ein Wort nicht passt):
   jede Zelle eine eigene volle Zeile; Überlappung dann vertikal. */
.col-mind-motion-soul.is-stacked{ flex-direction:column; }
.col-mind-motion-soul.is-stacked .col-mind-motion-soul__item{ flex:1 1 auto; }
.col-mind-motion-soul.is-stacked > .col-mind-motion-soul__item + .col-mind-motion-soul__item{
  margin-left:0; margin-top:calc(-1 * var(--linewidth));
}
/* Mobile: immer gestapelt, kleinere Schrift/Icon. */
@media (max-width:480px){
  .col-mind-motion-soul{ flex-direction:column; }
  .col-mind-motion-soul__item{ flex:1 1 auto; padding:var(--pad-big-mobile); }
  .col-mind-motion-soul > .col-mind-motion-soul__item + .col-mind-motion-soul__item{
    margin-left:0; margin-top:calc(-1 * var(--linewidth));
  }
  .col-mind-motion-soul__icon{ width:30px; height:30px; }
  .col-mind-motion-soul__label{ font-size:var(--h3-size); line-height:var(--h3-lh); }
}

/* ---------- nav-split (wiederverwendbar) --------------------
   Waagerecht geteilte Menü-Zelle: zwei gestapelte nav-items, die
   Trennlinie ist ihre geteilte Rahmenkante. Beliebig einsetzbar,
   wo eine Menü-Position in oben/unten geteilt werden soll
   (erste Anwendung: DE/EN in col/nav-horizontal). Die Zelle
   füllt die Höhe ihres Containers; die zwei Items teilen sie je
   zur Hälfte. Für halbhohe Items wird das volle vertikale Padding
   der nav-item--h zurückgenommen.
------------------------------------------------------------- */
.nav-split{
  align-self:stretch;
  display:flex; flex-direction:column;
}
.nav-split > .nav-item--h{
  flex:1 1 0; min-height:0; height:auto;
  padding-top:0; padding-bottom:0;   /* halbe Höhe → kein 40px-Padding */
}

/* ---------- col/nav-horizontal ------------------------------
   Figma 2029:119 — Nav-Leiste (Höhe nav-h). Links ein „Ticker"
   (Status-Text, mono, Typewriter → module/col/nav-horizontal.js), rechts
   die nav-items + geteilte DE/EN-Zelle (nav-split).
   Ab Tablet (≤960) wird's mit voller Nav zu eng → Burger-Menü
   (Icon); der Ticker bleibt. Mobile (≤480): Leiste nav-h-mobile.
   Das Fullscreen-Menü hinter dem Burger kommt auf sect/-Ebene.
------------------------------------------------------------- */
.col-nav-horizontal{
  display:flex; align-items:stretch; justify-content:flex-end;   /* Menü/Burger rechts (Ticker ist ausgelagert → col/ticker) */
  /* box-sizing:border-box (global) würde vom nav-h die 2px Rahmen abziehen →
     Innenhöhe nav-h-2, die nav-h-hohen nav-items würden 2px geclippt und die
     Bar wäre 2px niedriger als eine gerahmte Logo-Zelle. Rahmen deshalb oben-
     drauf: Innenhöhe bleibt exakt nav-h. */
  height:calc(var(--nav-h) + 2 * var(--linewidth));
  border:var(--linewidth) solid var(--poly-line);
  overflow:hidden;
}
@keyframes poly-caret{ 50%{ opacity:0; } }
/* Desktop-Menü (nav-items + DE/EN-Split), rechtsbündig */
.col-nav-horizontal__menu{ flex:0 0 auto; display:flex; align-items:stretch; }
.col-nav-horizontal__menu > .nav-item--h{ flex:0 0 auto; }
/* Linien: Menü-Zellen tragen einseitige Trenner (border-left); der Burger und
   der Außenrahmen (Bar-Frame) bleiben. So jede Linie genau einmal.
   DE/EN-Trenner: border-top am zweiten Split-Item. */
.col-nav-horizontal .nav-item--h{ border:0; }
.col-nav-horizontal__menu > .nav-item--h,
.col-nav-horizontal__menu > .nav-split{
  border-left:var(--linewidth) solid var(--poly-line);
}
.col-nav-horizontal .nav-split > .nav-item--h + .nav-item--h{
  border-top:var(--linewidth) solid var(--poly-line);
}
/* Burger (nur Tablet/Mobile) — Linien stehen frei: keine Rahmen.
   Zwei Icon-Dateien in nativer Größe (keine Skalierung):
   Tablet = burger-menu-tablet.svg (120×120), Mobile = burger-menu.svg (60×60). */
.col-nav-horizontal__burger{
  flex:0 0 auto; align-self:stretch;
  display:none; align-items:center; justify-content:center;
  border:0; background:none; cursor:pointer; padding:0;
  color:var(--ink);   /* Icon-Maske erbt die Farbe */
}
/* native Größe, keine Skalierung; per Breakpoint umgeschaltet.
   (Klassen-Selektoren, damit die Umschaltung nicht von einem
   spezifischeren `…__burger img`-Selektor überstimmt wird.) */
.col-nav-horizontal__burger-tablet,
.col-nav-horizontal__burger-mobile{ display:block; width:var(--nav-h); height:var(--nav-h); line-height:0; }
.col-nav-horizontal__burger .poly-ic{ width:100%; height:100%; display:block; }
@media (max-width:480px){
  .col-nav-horizontal__burger-mobile{ width:var(--nav-h-mobile); height:var(--nav-h-mobile); }
}
.col-nav-horizontal__burger-mobile{ display:none; }   /* Default: Tablet-Icon */
@media (max-width:960px){
  .col-nav-horizontal__menu{ display:none; }
  .col-nav-horizontal__burger{ display:flex; }        /* Tablet: 120×120-Icon */
}
@media (max-width:480px){
  .col-nav-horizontal{ height:calc(var(--nav-h-mobile) + 2 * var(--linewidth)); }
  .col-nav-horizontal__burger-tablet{ display:none; }
  .col-nav-horizontal__burger-mobile{ display:block; }   /* Mobile: 60×60-Icon */
}

/* ---------- col/pattern (Linien-Muster) ---------------------
   Parametrischer repeating-linear-gradient wie die leeren Card-Filler,
   aber modular: Liniendicke/Abstand/Winkel + Vorder-/Hintergrundfarbe
   (aus den Tokens) kommen inline via patternStyle. Kein festes Format —
   das Pattern flext in seinen Rahmen (Zelle/Row/Column/Side) und füllt ihn.
   Grundgröße optional über min-height / min-width (inline via patternStyle),
   damit es auch standalone bzw. in leeren Rahmen sichtbar bleibt. */
/* Rahmen wie jedes andere Modul — in Grid-/Reihen-Zellen nehmen ihn die
   Zell-Regeln (.poly-cell > *, .col-grid__cell > *) ohnehin wieder weg. */
.col-pattern{ border:var(--linewidth) solid var(--poly-line);
  flex:1 1 auto; align-self:stretch; width:100%; height:100%;
  min-height:var(--minh-d, 120px); min-width:var(--minw-d, 0); }
@media (max-width:960px){ .col-pattern{
  min-height:var(--minh-t, var(--minh-d, 120px)); min-width:var(--minw-t, var(--minw-d, 0)); } }
@media (max-width:480px){ .col-pattern{
  min-height:var(--minh-m, var(--minh-t, var(--minh-d, 120px))); min-width:var(--minw-m, var(--minw-t, var(--minw-d, 0))); } }
/* Als Side (Spalte): Rahmen wie die anderen Sides; füllt die Section-Höhe. */
.sect-grid__side > .col-pattern{ min-height:0; }   /* Rahmen kommt aus der Basis-Regel */

/* ---------- col/blog (Container) ------------------------------
   Der Blog-Index: EIN sect-grid — links die Filter-Chips (240er-Side, volle
   Höhe), rechts im Main alle Einträge (Auto-Teaser aus den Beiträgen)
   untereinander mit −1px-Naht. Tablet/Mobile: Side weg, klappbare Leiste
   oben (Standard-sect-grid-Verhalten). Parameter teaserSplit/teaserMin
   werden an jedes col/blog-teaser durchgereicht.
   Der Vorgänger col/einträge bleibt für Bestandsseiten (work) unverändert.
------------------------------------------------------------- */
.col-blog__list{ display:flex; flex-direction:column; }
.col-blog__cell + .col-blog__cell{ margin-top:calc(-1 * var(--linewidth)); }

/* Einträge-Filterzeile mit Side (wie col/collection): Chips links in der
   240er-Side, Main = Schraffur-Füller (die Einträge folgen als eigene
   Zeilen darunter, es gibt hier nichts zu zeigen — leere Fläche = Muster,
   wie überall im System). Ab ≤960 fällt die Side weg und die klappbare
   Leiste (side-reflow) übernimmt — der Füller verschwindet mit, sonst
   stünde ein leerer Schraffur-Block unter der Leiste. */
.col-entries__filtermain{
  box-sizing:border-box; height:100%; min-height:100px;
  border:var(--linewidth) solid var(--poly-line);
}
.col-entries__filterrow .sect-grid__main{ display:flex; }
.col-entries__filterrow .sect-grid__main > .col-entries__filtermain{ flex:1 1 auto; }
@media (max-width:960px){
  .col-entries__filterrow .sect-grid__main{ display:none; }
}

/* ---------- col/blog-teaser ---------------------------------
   Teaser eines Blog-Beitrags auf der Index-Seite (vom Build automatisch
   aus den Beitragsfeldern erzeugt, im Konfigurator auch von Hand setzbar).
   Zwei Spalten 1 : 3 — links Thumbnail, rechts Datum (Tag), Headline (H3),
   Kurzbeschreibung (body) und unten bündig die Tag-Chips.

   Thumbnail-Spalte: aspect-ratio (Default 1/1, Token --bt-ratio als
   CSS-Wert, z.B. 1/1 oder 3/2) gibt die MINDESThöhe der Zeile vor; wird
   die rechte Spalte höher, streckt align-items:stretch die Spalte auf
   volle Höhe und das Bild deckt sie per object-fit:cover, zentriert —
   genau das gewünschte „auf Höhe skalieren". Ohne Bild zeigt die Fläche
   die Standard-Schraffur (poly-hatch).

   Umbruch: flex-wrap wie bei col/columns — die Thumbnail-Spalte hat
   --bt-min (Default 200px) als Mindestbreite, die Textspalte das
   Dreifache; wird der Container schmaler, bricht die Textspalte unter
   das Bild. Ab ≤480 wird immer gestapelt (Trennlinie wechselt von
   links auf oben). Ganze Karte verlinkt (href), Hover: Headline magenta
   — wie das Label der Filter-Leiste.
------------------------------------------------------------- */
.col-blog-teaser{
  box-sizing:border-box; display:flex; flex-wrap:wrap; align-items:stretch;
  width:100%;
  border:var(--linewidth) solid var(--poly-line);
  background:var(--surface);
  text-decoration:none; color:var(--ink);
}
.col-blog-teaser__media{
  /* Feste Spaltenbreite 1/(1+N) über die BASIS (nicht Basis 0 + grow):
     mit Basis 0 macht die aspect-ratio-Box ihre Breite an der gestreckten
     Höhe fest („quadratisch gewinnt") und das 1:N-Verhältnis verrutscht.
     Mit Prozent-Basis ist die Breite definit → das Verhältnis stimmt exakt,
     und aspect-ratio liefert nur noch die MINDESThöhe der Zeile (kurzer
     Text → Bild quadratisch). Wird die Textspalte höher, streckt stretch
     die Spalte und das Bild deckt sie per cover — Breite bleibt 1/(1+N).
     grow:1 lässt die Spalte im umgebrochenen Zustand auf volle Breite
     wachsen (auf einer gemeinsamen Zeile ist kein Platz frei, grow ist
     dort wirkungslos). KEIN min-height:100% — das koppelte gegen die
     auto-Höhe zurück und blähte die Zeile kurz vor dem Umbruch auf. */
  box-sizing:border-box;
  flex:1 1 calc(100% / (1 + var(--bt-split, 3)));
  min-width:var(--bt-min, 200px);
  position:relative;
  aspect-ratio:var(--bt-ratio, 1/1);
  align-self:stretch;
}
.col-blog-teaser__img{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:center; display:block;
}
.col-blog-teaser__body{
  box-sizing:border-box;
  /* Spaltenverhältnis 1 : N über --bt-split (Default 3) — als Parameter am
     col/blog-Modul, das ihn an alle Teaser durchreicht. */
  flex:1 1 calc(100% * var(--bt-split, 3) / (1 + var(--bt-split, 3)));
  min-width:calc(var(--bt-split, 3) * var(--bt-min, 200px));
  display:flex; flex-direction:column; align-items:flex-start;
  padding:var(--pad-big);
  border-left:var(--linewidth) solid var(--poly-line);
}
.col-blog-teaser__datum{ margin-bottom:var(--pad-medium); color:var(--poly-line-strong); }
.col-blog-teaser__headline{ margin:0 0 var(--pad-small); }
.col-blog-teaser__kurz{ margin:0; }
.col-blog-teaser__tags{ margin-top:auto; padding-top:var(--pad-medium); align-self:stretch; }
.col-blog-teaser__tags:empty{ display:none; }
/* Hover: KEIN Flächen-Fill — nur ein 3px-Akzentrahmen um den Eintrag. Als
   ::after-Overlay (nicht outline), damit der Rahmen ÜBER dem absolut
   positionierten Thumbnail/Video liegt und nicht davon verdeckt wird.
   pointer-events:none → Klick bleibt erhalten; kein Layout-Sprung (Overlay).
   Typo und Bild bleiben unverändert. */
.col-blog-teaser[href]{ position:relative; }
.col-blog-teaser[href]::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  border:3px solid transparent; z-index:2;
}
.col-blog-teaser[href]:hover::after{ border-color:var(--accent); }
/* Dasselbe für die Video-Karten in Collection (Portfolio) und Grid-Cards —
   aber NUR bei klickbaren Karten (führen über .col-video__link auf eine
   Overlay-Seite). Karten ohne Overlay (href="#" oder ganz ohne Link) bekommen
   keinen Hover, genau wie wirkungslose Tags. z-index über der Medienfläche. */
.col-collection__cell .comp-video-card,
.col-grid-cards__card .comp-video-card{ position:relative; }
.col-collection__cell .comp-video-card:has(.col-video__link:not([href="#"]))::after,
.col-grid-cards__card .comp-video-card:has(.col-video__link:not([href="#"]))::after{
  content:""; position:absolute; inset:0; pointer-events:none;
  border:3px solid transparent; z-index:3;
}
.col-collection__cell .comp-video-card:has(.col-video__link:not([href="#"])):hover::after,
.col-grid-cards__card .comp-video-card:has(.col-video__link:not([href="#"])):hover::after{
  border-color:var(--accent);
}
@media (max-width:480px){
  .col-blog-teaser__media{ flex:1 0 100%; min-width:0; }
  .col-blog-teaser__body{
    flex:1 0 100%; min-width:0;
    border-left:0; border-top:var(--linewidth) solid var(--poly-line);
  }
}

/* ---------- col/nav-close (Header-Close für Overlays) --------
   Wie col/nav-horizontal aufgebaut (volle Breite, Header-Höhe, Rahmen),
   aber statt Menü/Burger ein rechtsbündiger Close-Button. Icon-Größe =
   Header-Höhe, per Breakpoint umgeschaltet (Desktop/Tablet 120, Mobile 60). */
.col-nav-close{
  display:flex; align-items:stretch; justify-content:flex-end;
  /* Rahmen raus (wie beim Logo gegenüber), nur die Unterkante bleibt — die
     Overlay-Kopfzeile schließt damit genauso ab wie die Nav-Kopfzeile. Die
     Höhe bleibt exakt die der col/nav-horizontal, sonst springt das Logo beim
     Öffnen des Overlays um 2px. */
  height:calc(var(--nav-h) + 2 * var(--linewidth));
  border-bottom:var(--linewidth) solid var(--poly-line);
  overflow:hidden;
}
.col-nav-close__btn{
  flex:0 0 auto; align-self:stretch;
  display:flex; align-items:center; justify-content:center;
  border:0; background:none; cursor:pointer; padding:0; line-height:0;
}
/* Das Kreuz ist INLINE-SVG (kein <img>): nur so erbt es die Theme-Farbe. Vorher
   steckten in icon/close-*.svg eine feste dunkle Fläche und eine feste
   Strichfarbe — auf hellem Grund ein dunkler Klotz. Jetzt zwei Diagonalen in
   currentColor, ohne Fläche. non-scaling-stroke hält den Strich bei jeder
   Größe auf --linewidth, also exakt so stark wie die Rasterlinien daneben. */
.col-nav-close__icon{
  display:block; width:var(--nav-h); height:var(--nav-h);
  color:var(--ink);                      /* nicht erben: die Seite setzt keine Grundfarbe */
  stroke-width:var(--linewidth);
  transition:color .15s;
}
.col-nav-close__btn:hover .col-nav-close__icon{ color:var(--accent); }
@media (max-width:480px){
  .col-nav-close{ height:calc(var(--nav-h-mobile) + 2 * var(--linewidth)); }
  .col-nav-close__icon{ width:var(--nav-h-mobile); height:var(--nav-h-mobile); }
}

/* ---------- col/pfeil-matrix („von poly zu mono") ------------
   Raster aus Link-Pfeilen; die Zellen baut module/col/arrow-matrix.js, weil die
   Spaltenzahl von der Breite abhängt. Geometrie steckt in Variablen:
     --am-cols   Spalten (JS, ggf. reduziert)
     --am-ratio  Zeilenhöhe als Vielfaches der Spaltenbreite (Default .75)
     --am-scale  Pfeilgröße als Anteil der Zelle
     --am-minf   min(1, ratio) — bezieht die Pfeilgröße auf die KLEINERE
                 Zellkante, damit bei flachen Zeilen nichts überläuft
   Kein Rahmen: das Feld ist eine Fläche aus Zeichen, keine Box. Die
   Drehung macht die transform-Transition; ihre Dauer setzt das JS je Phase.
   Mobile (≤480) ist das Modul aus — gestaucht trägt es visuell nicht.
------------------------------------------------------------- */
.col-arrow-matrix{
  display:grid;
  grid-template-columns:repeat(var(--am-cols, 8), 1fr);
  width:100%;
  color:var(--ink);
}
.col-arrow-matrix__cell{
  /* Zelle: Breite aus dem Raster, Höhe = Breite × ratio. */
  aspect-ratio:1 / var(--am-ratio, .75);
  display:flex; align-items:center; justify-content:center;
  min-width:0;
}
.col-arrow-matrix__svg{
  display:block;
  width:calc(var(--am-scale, .5) * var(--am-minf, .75) * 100%);
  height:auto; aspect-ratio:1;
  transform:rotate(0deg);
  transition:transform var(--am-dur, 700ms) cubic-bezier(.45,.05,.3,1);
  /* BEWUSST kein will-change/translateZ: die GPU-Layer-Rasterung zeichnet die
     1px-Linien ausgewaschen und stellenweise gepunktet (gemessen im Vergleich:
     mit will-change deutlich blasser und ungleichmäßig). Ohne Layer-Promotion
     rastert Chromium jeden Winkel sauber — die Transition läuft trotzdem
     flüssig, es sind nur wenige Dutzend kleine SVGs. */
}
.col-arrow-matrix__svg path{
  /* non-scaling-stroke: die Linie bleibt hairline-dünn (= --linewidth wie jede
     andere Linie im System), egal wie groß der Pfeil skaliert ist. */
  stroke:currentColor; stroke-width:var(--linewidth); stroke-miterlimit:10; fill:none;
}
/* Mobile normalerweise aus (zu wenig Fläche, kein Zeiger). Mit dem Schalter
   „Auch auf Mobile zeigen" bleibt es stehen — dort führt ein Tippen die Pfeile,
   statt der Maus (s. module/col/arrow-matrix.js). touch-action:manipulation nimmt dem
   Tippen die 300ms-Verzögerung, ohne das Scrollen zu blockieren. */
@media (max-width:480px){
  .col-arrow-matrix{ display:none; }
  .col-arrow-matrix--mobil{ display:grid; touch-action:manipulation; }
}
@media (prefers-reduced-motion:reduce){
  .col-arrow-matrix__svg{ transition:none; }
}

/* ============================================================
   edit/ — Editorial-Bausteine (Blog, Projektseiten, lange Strecken)

   Haltung: Bilder OHNE Rahmen auf der Fläche, getrennt nur durch Weißraum aus
   den Abstands-Tokens. Die einzige Linie ist optional die Haarlinie über der
   Legende. Zwei gerahmte Bilder nebeneinander lesen als Tabelle — zwei
   rahmenlose als Doppelseite. Genau das nimmt langen Strecken die
   Raster-Lastigkeit, ohne das System zu verlassen: Typo, Linienstärke, Farben
   und Abstände kommen weiter aus den Tokens.
   ============================================================ */

/* ---------- Rahmen & Padding: Variante B ---------------------
   Die edit-Module fügen sich in den bestehenden Gridrahmen ein und erzeugen
   KEINE weiteren Linien zwischen sich: jedes Modul zieht nur die SEITENlinien
   und bringt das Padding mit, oben und unten hat es keine Kante. Damit
   verschmelzen aufeinanderfolgende edit-Module zu EINEM Lauf ohne innere
   Querlinien — die waagerechten Linien kommen weiterhin von den normalen
   Nachbarn (col/text & Co.) und von den Section-Nähten. Die −1px-Nahtmechanik
   des Rasters bleibt vollständig unangetastet.
------------------------------------------------------------- */
.edit-block{
  box-sizing:border-box; width:100%;
  border:var(--linewidth) solid var(--poly-line);
  padding:var(--pad-big);
  background:var(--surface);
}
/* Ein LAUF aus edit-Modulen verschmilzt zu einem Rahmen: der erste behält seine
   obere Kante, der letzte seine untere, dazwischen fällt beides weg. Damit
   entstehen keine Querlinien im Lauf — die Linie zur nächsten Section bzw. zum
   nächsten normalen Modul bleibt aber erhalten (das war vorher der Fehler:
   ohne untere Kante fehlte am Section-Ende die Trennlinie). */
.edit-block + .edit-block{ border-top:0; margin-top:0; padding-top:0; }
.edit-block:has(+ .edit-block){ border-bottom:0; }
/* In einer Grid-/Reihen-Zelle liefert das Raster die Außenlinien. */
.poly-cell .edit-block{ border-left:0; border-right:0; }

/* ---------- Bildfläche + Legende (geteilt: figure/gallery/carousel) -------- */
.edit-fig{ margin:0; min-width:0; display:flex; flex-direction:column; }
.edit-fig__box{
  position:relative; width:100%;
  aspect-ratio:var(--ar-d, 3 / 2);
  overflow:hidden;
  /* Platzhalter: dieselbe Schraffur wie leere Grid-Zellen — aber ohne Rahmen. */
  background-color:var(--surface);
  background-image:repeating-linear-gradient(var(--hatch-angle),
    var(--poly-line) 0 var(--hatch-line),
    var(--surface) var(--hatch-line) calc(var(--hatch-pitch) / 1.41421356));
  background-size:var(--hatch-pitch) var(--hatch-pitch);
}
@media (max-width:960px){ .edit-fig__box{ aspect-ratio:var(--ar-t, var(--ar-d, 3 / 2)); } }
@media (max-width:480px){ .edit-fig__box{ aspect-ratio:var(--ar-m, var(--ar-t, var(--ar-d, 3 / 2))); } }
.edit-fig__img{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; display:block;
}
.edit-fig__img--contain{ object-fit:contain; }
/* Legende: Mono im Tag-Stil (wie das Datum im Blog-Teaser) — ruhige Nebenstimme. */
.edit-fig__cap{
  margin-top:var(--pad-small);
  font-family:var(--font-mono); font-weight:500;
  font-size:var(--tag-size); line-height:var(--tag-lh);
  letter-spacing:var(--tag-ls); color:var(--poly-line-strong);
}
.edit-fig--linie .edit-fig__cap{
  border-top:var(--linewidth) solid var(--poly-line);
  padding-top:var(--pad-small);
}

/* ---------- edit/text — Fließtext im editorial-Lauf ----------
   Gleicher Parser und dieselbe Markup-Sprache wie col/text, aber als edit-block:
   kein eigener Kasten, keine Linie zu den Nachbar-edit-Modulen. */
.edit-text{ display:flex; flex-direction:column; align-items:flex-start; gap:var(--pad-medium); }
.edit-text > *{ width:100%; }
/* Ausnahme: der kurze [trenner] ist 60px breit — die Regel „alle Kinder auf
   volle Breite" hat ihn hier über die ganze Spalte gezogen und damit von
   [trenner-full] ununterscheidbar gemacht. Gilt genauso im Text neben dem Bild. */
.edit-text > .col-text__trenner,
.edit-figtext__text > .col-text__trenner{ width:60px; }
.edit-text > .col-text__trenner--full,
.edit-figtext__text > .col-text__trenner--full{ width:100%; }
.edit-text--center{ align-items:center; justify-content:center; text-align:center; }
.edit-text--center .col-text__col{ align-items:center; text-align:center; }
.edit-text--measure{ --measure:560px; }
.edit-text--measure > *{ max-width:var(--measure); }

/* ---------- edit/figure — ein Bild ---------- */
.edit-figure{ width:100%; }
/* „nur Textbreite": begrenzt das BILD, nicht das Modul — sonst zöge die rechte
   Rahmenlinie mit nach innen statt außen zu bleiben. */
.edit-figure--text > .edit-fig{ max-width:var(--measure, 560px); }

/* ---------- edit/figure-text — Bild neben Text ----------
   Prozent-Basis statt flex-basis:0 → das Verhältnis 1:N ist EXAKT (dieselbe
   Lösung wie beim Blog-Teaser, wo eine 0-Basis es verzogen hat). Unter --ft-min
   bricht der Text unter das Bild. */
.edit-figtext{ display:flex; flex-wrap:wrap; gap:var(--pad-big); align-items:flex-start; }
.edit-figtext__media{
  flex:1 1 calc((100% - var(--pad-big)) / (1 + var(--ft-split, 1)));
  min-width:var(--ft-min, 260px);
}
.edit-figtext__text{
  flex:1 1 calc((100% - var(--pad-big)) * var(--ft-split, 1) / (1 + var(--ft-split, 1)));
  min-width:var(--ft-min, 260px);
  display:flex; flex-direction:column; align-items:flex-start; gap:var(--pad-medium);
}
.edit-figtext__text > *{ width:100%; }
/* Bild rechts: nur die visuelle Reihenfolge dreht sich, die Quellordnung
   (Bild zuerst) bleibt — im gestapelten Zustand steht das Bild dann oben. */
.edit-figtext--rechts .edit-figtext__media{ order:2; }

/* Gleiche Höhe in einer Zeile: hat ein Bild ein anderes Format, ragte es vorher
   heraus. Jetzt streckt sich jede Figure auf die Zeilenhöhe, die Bildfläche
   wächst mit (aspect-ratio liefert nur noch die MINDESThöhe) und das Bild deckt
   per cover ab — dieselbe Lösung wie beim Blog-Teaser. */
.edit-gal--reihe > .edit-fig,
.edit-gal--versatz > .edit-fig,
.edit-car__track > .edit-fig{ align-self:stretch; }
.edit-gal--reihe > .edit-fig > .edit-fig__box,
.edit-gal--versatz > .edit-fig > .edit-fig__box,
.edit-gal--raster > .edit-fig > .edit-fig__box,
.edit-car__track > .edit-fig > .edit-fig__box{ flex:1 1 auto; }
/* Format „original": das Bild bestimmt die Höhe, also läuft es im Fluss mit
   statt absolut zu liegen — und wird nicht beschnitten. Das gilt nur für
   edit/figure und edit/figure-text: in einer REIHE widerspricht „original"
   der gleichen Höhe (das Bild bestimmt die Höhe ODER die Reihe tut es, nicht
   beides), deshalb ersetzt das Prepare es in Galerie und Karussell durch das
   Modulformat — dort kommt --orig nie im Markup an. */
.edit-fig__box--orig{ aspect-ratio:auto; }
.edit-fig__box--orig > .edit-fig__img{
  position:static; width:100%; height:auto; object-fit:fill;
}
/* ---------- Klickbares Medium: Hover-Linie + Eckwinkel ----------
   Der Knopf liegt ueber der ganzen Bildflaeche und ist dreierlei in einem:
   Klickziel (auch per Tastatur), Hover-Rahmen und Zeichen. Der Rahmen ist
   derselbe wie beim Blog-Teaser und bei den Video-Karten — 3px Akzent als
   Overlay, kein Flaechen-Fill, kein Layout-Sprung.
   Die vier Eckwinkel fahren beim Hover aus der Mitte nach aussen: die Geste
   IST die Aussage („wird groesser"). Ohne Hover sind sie unsichtbar, also
   bleibt das Bild im Ruhezustand voellig ruhig. */
.edit-fig__zoom{
  all:unset; box-sizing:border-box; cursor:pointer;
  position:absolute; inset:0; z-index:2;
  display:flex; align-items:center; justify-content:center;
  border:3px solid transparent;
  background-color:transparent;
  transition:border-color .15s, background-color .15s;
}
/* Beim Überfahren legt sich Dunkelblau zu 60% über das Medium. Bewusst NICHT
   --surface (das wäre im Hellmodus beige): der grüne Winkel und der helle
   Rahmen brauchen einen dunklen Grund, sonst verschwinden die Haarlinien in
   hellen oder unruhigen Bildern. Also in BEIDEN Modi derselbe dunkelblaue
   Schleier. Der Blog-Teaser und die Collection bleiben ausdrücklich
   unangetastet (eigene Hover-Regeln weiter oben). */
.edit-fig__zoom:hover,
.edit-fig__zoom:focus-visible{
  border-color:var(--accent);
  background-color:color-mix(in srgb, var(--poly-dark) 60%, transparent);
}
.edit-fig__ecken{
  display:block; width:44px; height:44px; color:var(--accent);
  opacity:0; transform:scale(.82);
  transition:opacity .15s, transform .15s var(--ease, ease-out);
}
.edit-fig__ecken svg{ display:block; width:100%; height:100%; stroke-width:var(--linewidth); }
.edit-fig__zoom:hover .edit-fig__ecken,
.edit-fig__zoom:focus-visible .edit-fig__ecken{ opacity:1; transform:scale(1); }
@media (prefers-reduced-motion:reduce){
  .edit-fig__ecken{ transition:opacity .15s; transform:none; }
  .edit-fig__zoom:hover .edit-fig__ecken{ transform:none; }
}
/* Play-Dreieck steht DAUERHAFT auf Videos — nur so sieht man in einer Reihe,
   was Bild und was Video ist. Ruhig in der Linienfarbe, beim Hover Akzent. */
.edit-fig__play{
  position:absolute; left:50%; top:50%; z-index:1;
  width:44px; height:44px; margin:-22px 0 0 -22px;
  color:var(--poly-line-strong); pointer-events:none;
  transition:color .15s;
}
.edit-fig__play svg{ display:block; width:100%; height:100%; stroke-width:var(--linewidth); }
.edit-fig__box:hover .edit-fig__play{ color:var(--accent); }
/* Video + Zoom zusammen: das Dreieck bleibt in der Mitte, die Winkel legen
   sich als Rahmen darum — deshalb ist der Winkelkasten groesser als 44. */
.edit-fig__box:has(.edit-fig__play) .edit-fig__ecken{ width:74px; height:74px; }

/* Format „original": das Verhältnis kommt aus der Datei und wird von
   module/col/edit-gallery.js nachgetragen. Bis dahin gilt die Vorgabe — ohne dieses
   Skript sieht die Reihe also aus wie mit 3:2, nicht kaputt. */
.edit-fig__box--auto{ aspect-ratio:var(--ar-d, 3 / 2); }

/* Legenden-Platzhalter: hat ein Bild der Reihe eine Legende und das nächste
   nicht, wäre dessen Bildfläche genau um die Legendenzeile HÖHER — die Bilder
   stünden nicht mehr auf einer Kante. Der Platzhalter hält die Zeile frei
   (unsichtbar, also auch ohne Haarlinie). */
.edit-fig__cap--leer{ visibility:hidden; }
.edit-fig__cap--leer::before{ content:"\00a0"; }

/* ---------- edit/gallery — reihe / raster / versatz ---------- */
.edit-gal{ --gal-gap:var(--pad-big); }
/* reihe: JUSTIERTE Zeile — alle Bilder gleich hoch, die BREITE folgt dem
   Bildformat. Vorher teilte sich die Reihe die Breite und die Höhen mussten per
   Streckung angeglichen werden (also beschnitten); ein 4:5 neben einem 16:9
   brachte das sichtbar durcheinander. Jetzt rechnet die Zeile andersherum:
   Wächst jedes Bild proportional zu seinem Seitenverhältnis, ist die Höhe aller
   Bilder rechnerisch identisch (Breite/Verhältnis = konstant) — ohne Beschnitt,
   auch bei „ganz sichtbar". Das Verhältnis steht als zwei ganze Zahlen im
   Markup (--arw/--arh), damit Python und JS zeichengleich bleiben; die Division
   macht CSS. Ein Gewicht (--w) multipliziert nur die Breite; wer eins setzt,
   will bewusst ein größeres Bild und nimmt den Beschnitt in Kauf (die Streckung
   weiter unten fängt die Höhe dann wieder ein).
   flex-basis:0 statt einer Prozentbasis — die Breite soll allein aus dem
   Verhältnis kommen. Umbrochen wird weiterhin an --gal-min. */
.edit-gal--reihe{ display:flex; flex-wrap:wrap; gap:var(--gal-gap); align-items:flex-start; }
/* WARUM DIE BASIS AM VERHÄLTNIS HÄNGT und nicht bei 0 steht — das war ein
   echter Fehler, und zwar einer, der nur in einem BESTIMMTEN Breitenfenster
   auftrat und deshalb lange durchrutschte:

   Vorher stand hier `flex-basis:0` plus `min-width:var(--gal-min,240px)`. Die
   Rechnung „Breite proportional zum Verhältnis → alle Höhen gleich" stimmt aber
   nur, solange NICHTS klemmt. Sobald das schmalste Bild auf die Mindestbreite
   auflief, hielt es dort — und das breite bekam den ganzen Rest. Gemessen an
   einer 4:5 neben einer 16:9: bei 1500px war das Kasten-Verhältnis sauber 1.78,
   bei 1000px 1.48, bei 800px 1.61, bei 700px 1.28, bei 560px 0.81. Der Kasten
   behielt die Zeilenhöhe der schmalen Figur, das Bild passte nicht mehr hinein
   und stand mit „ganz sichtbar" (contain) in bis zu 164px Schraffur.

   Jetzt hängen BASIS und WACHSTUM am selben Verhältnis. Dann gilt in beide
   Richtungen: Breite = Verhältnis × (Basis-Anteil + Rest-Anteil), also bleibt
   Breite/Verhältnis — die Höhe — für alle gleich. Auch beim SCHRUMPFEN, denn
   der Schrumpf-Anteil richtet sich nach der Basis, und die ist ja proportional.
   `--gal-min` ist damit kein Riegel mehr, sondern der Umbruchpunkt: passen die
   Basisbreiten nicht mehr nebeneinander, bricht die Zeile um — statt eine Figur
   festzuhalten und die Rechnung zu zerstören. Umbrechen ist die ehrliche
   Antwort auf „zu eng", Klemmen ist es nicht.

   Das `* 2 / 3` ist die Bezugsgröße: `--gal-min` gilt für ein 3:2-Bild, das
   Standardformat des Moduls. Ohne diesen Bezug wäre die Basis eines 16:9 gleich
   der vollen 240px × 1.78 = 427 — drei solche Bilder brächen dann schon bei
   1500px um, obwohl sie bequem nebeneinander passen (gemessen). Mit dem Bezug
   liegt der Umbruch fast genau dort, wo er vorher lag.

   `--ar` steht einmal hier und wird in den Breakpoints nur neu belegt; vorher
   musste die Formel dreimal getippt werden, und die Basis hätte man dabei
   zwangsläufig vergessen. */
.edit-gal--reihe > .edit-fig{
  --ar:calc(var(--arw, 3) / var(--arh, 2));
  flex-grow:calc(var(--w, 1) * var(--ar));
  flex-shrink:1;
  flex-basis:calc(var(--gal-min, 240px) * var(--ar) * 2 / 3);
  min-width:0;
}
@media (max-width:960px){
  .edit-gal--reihe > .edit-fig{
    --ar:calc(var(--arw-t, var(--arw, 3)) / var(--arh-t, var(--arh, 2)));
  }
}
@media (max-width:480px){
  .edit-gal--reihe > .edit-fig{
    --ar:calc(var(--arw-m, var(--arw-t, var(--arw, 3)))
            / var(--arh-m, var(--arh-t, var(--arh, 2))));
    /* Auf dem Handy stapeln, nicht schrumpfen. Vorher erledigte das die
       Mindestbreite von selbst (zwei Bilder à 240px passen nicht in 400px).
       Ohne sie liefen zwei Bilder nebeneinander auf 130 und 288 Pixel — beides
       zu klein, um noch etwas zu zeigen. Volle Breite pro Bild ist hier die
       richtige Antwort; `--gal-min` gilt ab Tablet. */
    flex-basis:100%;
  }
}
/* raster: auto-fit — die Spaltenzahl ergibt sich aus der Breite, ohne Breakpoint. */
.edit-gal--raster{
  display:grid; gap:var(--gal-gap);
  grid-template-columns:repeat(auto-fit, minmax(var(--gal-min, 200px), 1fr));
}
/* versatz: zweites Bild aus der Achse — der eine Moment, der eine Seite
   gestaltet statt gerastert aussehen lässt. Unter 768 wieder gerade. */
.edit-gal--versatz{ display:flex; flex-wrap:wrap; gap:var(--gal-gap); align-items:flex-start; }
.edit-gal--versatz > .edit-fig{
  flex:1 1 calc((100% - var(--gal-gap)) / 2); min-width:var(--gal-min, 240px);
}
.edit-gal--versatz > .edit-fig:nth-child(2){ margin-top:var(--gal-offset, 90px); }
@media (max-width:768px){ .edit-gal--versatz > .edit-fig:nth-child(2){ margin-top:0; } }

/* ---------- edit/carousel — nativer scroll-snap ----------
   Kein Slider-Framework: overflow-x + scroll-snap. Wischen auf Touch und
   Trackpad funktioniert dadurch von Haus aus; module/col/edit-carousel.js liefert nur
   Pfeile, Zähler und Aktiv-Zustände. Der Anschnitt des nächsten Bildes erzählt
   „es geht weiter" — deshalb braucht es keinen Punkte-Pager. */
.edit-car{ --gal-gap:var(--pad-medium); position:relative; }
.edit-car__track{
  display:flex; gap:var(--gal-gap);
  overflow-x:auto; overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;
  scrollbar-width:none;
}
.edit-car__track::-webkit-scrollbar{ display:none; }
/* Schieben mit der Maus: Cursor erst, wenn es etwas zu schieben gibt (Klasse
   kommt aus edit-carousel.js). Beim Ziehen ist der Snap aus — sonst zöge der
   Browser die Strecke gegen die Hand zurück — und die Bilder nehmen keine
   Klicks an, damit aus dem Schieben kein Bildklick wird. */
.edit-car__track.is-grab{ cursor:grab; }
.edit-car__track.is-drag{ cursor:grabbing; scroll-snap-type:none; user-select:none; }
.edit-car__track.is-drag .edit-fig{ pointer-events:none; }
.edit-car__track img{ -webkit-user-drag:none; }
.edit-car__track > .edit-fig{
  flex:0 0 var(--gal-slide, 72%); scroll-snap-align:start;
}
@media (max-width:480px){ .edit-car__track > .edit-fig{ flex-basis:88%; } }
/* ---------- Blätter-Leiste (Balken · Zähler · Pfeile) --------
   Gemeinsam für edit/carousel (scrollt) und col/grid-cards (blättert) — beide
   sollen sich gleich anfassen. Der native Scrollbalken des Karussells ist
   verborgen; diese Leiste zeigt Position UND Gesamtlänge, auch beim freien
   Wischen. Verhalten: module/col/pager.js.
------------------------------------------------------------- */
.poly-pager[hidden]{ display:none; }
.poly-pager__bar{
  position:relative; height:var(--linewidth); margin-top:var(--pad-small);
  background:var(--poly-line);
  touch-action:none;
}
.poly-pager__fill{
  position:absolute; top:0; left:0; height:100%;
  width:100%; background:var(--poly-line-strong);
  transition:transform .12s linear;
  transform-origin:left center;
}
/* Der Balken ist anfassbar — eine 1px-Linie kann man aber nicht greifen.
   Das ::after legt eine unsichtbare Grifffläche von ±9px darüber (es malt nach
   dem Kind, liegt also über dem Fortschritt und fängt den Zeiger). */
.poly-pager__bar.is-grab{ cursor:grab; }
.poly-pager__bar.is-grab::after{
  content:""; position:absolute; left:0; right:0; top:-9px; bottom:-9px;
}
.poly-pager__bar.is-drag{ cursor:grabbing; }
.poly-pager__bar.is-drag .poly-pager__fill{ transition:none; }
.poly-pager__ctrl{
  display:flex; align-items:center; gap:var(--pad-small);
  margin-top:var(--pad-small);
}
.poly-pager__count{
  font-family:var(--font-mono); font-weight:500; font-size:var(--tag-size);
  letter-spacing:var(--tag-ls); color:var(--poly-line-strong);
  margin-right:auto;
}
/* Pfeile: dasselbe Link-Icon wie überall, der „zurück"-Pfeil gespiegelt.
   Hover auf das Akzentpaar — gleiche Sprache wie Tags und Filter-Reset. */
.poly-pager__nav{
  all:unset; box-sizing:border-box; cursor:pointer;
  width:35px; height:35px; flex:0 0 auto;
  display:inline-flex; align-items:center; justify-content:center;
  border:var(--linewidth) solid var(--poly-line-strong);
  color:var(--poly-line-strong);
  transition:color .15s, border-color .15s, background-color .15s;
}
.poly-pager__nav svg{ width:14px; height:14px; display:block; }
.poly-pager__nav--prev svg{ transform:rotate(180deg); }
.poly-pager__nav:hover{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent); }
.poly-pager__nav[disabled]{ opacity:.35; cursor:default; }
.poly-pager__nav[disabled]:hover{
  background:none; color:var(--poly-line-strong); border-color:var(--poly-line-strong);
}
/* ---- Punkte-Fassung (col/grid-cards) ----
   Hier wird geblättert, nicht gescrollt: statt Balken und Zähler ein Punkt je
   Seite, dazwischen. Die Leiste wird dabei wieder zum gerahmten Streifen wie
   früher der page-indicator, mit −1px-Naht ans Gitter; die Pfeile sitzen links
   und rechts außen (der „zurück"-Pfeil per order, ohne Markup-Umbau). */
.poly-pager--punkte .poly-pager__bar,
.poly-pager--punkte .poly-pager__count{ display:none; }
.poly-pager--punkte .poly-pager__ctrl{
  margin-top:calc(-1 * var(--linewidth));
  border:var(--linewidth) solid var(--poly-line);
  padding:var(--pad-medium) 0;      /* seitlich KEIN Abstand: die Pfeile sitzen
                                       an der Rahmenkante, wie Zellen im Raster */
  gap:var(--pad-medium);
  /* Kantenlänge der Pfeil-Quadrate = Innenhöhe des Streifens. Die steht fest:
     der Punkt aus elem/page-indicator (10px) plus der Innenabstand oben und
     unten. Ausgerechnet statt aspect-ratio, weil ein Flex-Element seine Breite
     bestimmt, BEVOR die Höhe durch stretch feststeht — aspect-ratio bliebe
     wirkungslos (gemessen: 15px breit statt 40). */
  --pager-quad:calc(2 * var(--pad-medium) + 10px);
}
/* Die Pfeile sind QUADRATE, die den Streifen ganz ausfüllen: align-self:stretch
   gibt die Höhe (Innenmaß des Streifens), aspect-ratio daraus die Breite. Eine
   eigene Linie zeichnen sie nur nach INNEN — oben, unten und außen steht schon
   der Rahmen des Streifens, ein zweiter Strich daneben wäre eine doppelte
   Haarlinie. Die Linien bleiben immer auf --poly-line, auch beim Überfahren:
   der grüne Wechsel passiert in der Fläche. */
.poly-pager--punkte .poly-pager__nav{
  width:var(--pager-quad); height:var(--pager-quad); align-self:center;
  margin-block:calc(-1 * var(--pad-medium));   /* bis an die Innenkante */
  border:0;
  color:var(--poly-line);
}
.poly-pager--punkte .poly-pager__nav--prev{
  order:-1; margin-left:0;
  border-right:var(--linewidth) solid var(--poly-line);
}
.poly-pager--punkte .poly-pager__nav--next{
  margin-right:0;
  border-left:var(--linewidth) solid var(--poly-line);
}
.poly-pager--punkte .poly-pager__nav:hover{
  background:var(--accent); color:var(--accent-ink); border-color:var(--poly-line);
}
/* Kein sichtbarer Deaktiviert-Zustand: ob es links oder rechts noch weitergeht,
   muss der Streifen nicht anzeigen — er sieht immer gleich aus. */
.poly-pager--punkte .poly-pager__nav[disabled]{ opacity:1; }
.poly-pager--punkte .poly-pager__nav[disabled]:hover{
  background:none; color:var(--poly-line); border-color:var(--poly-line);
}
.poly-pager__punkte{
  display:none; flex:1 1 auto; min-width:0;
  align-items:center; justify-content:center; gap:var(--pad-medium);
}
.poly-pager--punkte .poly-pager__punkte{ display:flex; }
.poly-pager--punkte .page-indicator__dot{ cursor:pointer; }

/* Der Teilen-Knopf braucht keine eigene Flaeche mehr: die Rueckmeldung
   („Kopiert") steht zwei Sekunden lang IN der Beschriftung (module/col/share.js).
   Das spart eine Schicht, verschiebt keine Reihe und laesst den Blick da,
   wo geklickt wurde. Die Klasse is-kopiert bleibt als Anfasser, falls der
   Zustand einmal eigens ausgezeichnet werden soll.
------------------------------------------------------------- */

/* ---------- Medien-Overlay (module/col/media-overlay.js) ------------
   Nur zwei Teile: die Schließen-Leiste (dasselbe col/nav-close wie auf den
   Overlay-Seiten) und darunter die Bühne mit dem Medium in der Mitte.
   Einpassung: Originalgröße ist die Obergrenze (width/height auto), das
   Fenster die andere (max-width/max-height 100%) — hoch- wie querformatige
   Medien passen damit ohne Zutun. Die Bühne hat dank flex:1 in einer
   fixierten Höhe ein definiertes Maß, sonst griffe max-height:100% nicht.
------------------------------------------------------------- */
.poly-medov{
  position:fixed; inset:0; z-index:900;
  display:flex; flex-direction:column;
  background:var(--surface);
}
html.poly-medov-an, html.poly-medov-an body{ overflow:hidden; }
.poly-medov__leiste{ flex:0 0 auto; border-bottom:var(--linewidth) solid var(--poly-line); }
.poly-medov__buehne{
  flex:1 1 auto; min-height:0;
  display:flex; align-items:center; justify-content:center;
  padding:var(--pad-big);
}
.poly-medov__bild, .poly-medov__video{
  display:block; width:auto; height:auto;
  max-width:100%; max-height:100%;
}
/* Der FrameRate-Player hat keine eigene Größe. Die Breite kommt deshalb aus
   der verfügbaren Höhe (Fenster minus Leiste minus Bühnen-Padding) — so bleibt
   16:9 erhalten, ohne dass eine der beiden Achsen überläuft. */
.poly-medov__embedbox{
  --medov-h:calc(100dvh - var(--nav-h) - 2 * var(--linewidth) - 2 * var(--pad-big));
  width:min(100%, calc(var(--medov-h) * 16 / 9));
  aspect-ratio:16 / 9;
}
/* Fläche wie beim Embed im Raster und im Overlay-Player: die eingebettete
   Seite ist durchsichtig, ohne eigene Fläche stünde das Video hier auf Beige. */
.poly-medov__embed{ display:block; width:100%; height:100%; border:0; background:var(--poly-dark); }
@media (max-width:480px){
  .poly-medov__embedbox{
    --medov-h:calc(100dvh - var(--nav-h-mobile) - 2 * var(--linewidth) - 2 * var(--pad-big));
  }
}

/* ---------- col/laufband (durchlaufender Schriftzug) ---------
   Ein Modul wie jedes andere im Raster: eigener Rahmen, Innenabstand aus den
   Vorgaben, Fläche und Typo wählbar (dieselben Variablen-Namen wie beim
   big-button, damit die Auswahl im Konfigurator gleich heißt und gleich wirkt).

   Die Bewegung ist eine reine Transform-Animation über die halbe Spur — das
   läuft auf dem Compositor und braucht kein Layout je Bild. Weg und Dauer
   setzt module/col/laufband.js, weil beides von der gemessenen Textbreite abhängt
   (eingestellt wird Tempo in px/s, nicht Sekunden — sonst hetzte langer Text
   und kurzer schliche). Ohne das Skript fehlt die Klasse is-laeuft und der
   Satz steht still: lesbar, nur ruhig.
------------------------------------------------------------- */
.col-laufband{
  --lb-pad:var(--pad-big);
  box-sizing:border-box; width:100%; overflow:hidden;
  padding:var(--lb-pad) 0;
  border:var(--linewidth) solid var(--poly-line);
  background:var(--lb-surface, var(--surface));
  color:var(--lb-ink, var(--ink));
}
.col-laufband__spur{
  display:flex; width:max-content;
  will-change:auto;                 /* s. Kommentar unten */
}
.col-laufband__satz{
  display:block; white-space:pre;
  padding-inline-end:var(--pad-big); /* Luft zwischen den Durchläufen */
  color:inherit;
}
/* Erst mit is-laeuft (also nach dem Vermessen) bewegt sich etwas — sonst
   liefe die Animation kurz gegen einen Weg von 0 und zuckte. */
.col-laufband.is-laeuft .col-laufband__spur{
  animation:poly-laufband var(--lb-dauer, 20s) linear infinite;
}
.col-laufband--rechts.is-laeuft .col-laufband__spur{ animation-direction:reverse; }
/* „Beim Überfahren anhalten": ohne JS, allein über :hover. Der Schalter im
   Konfigurator setzt die Klasse am Modul. */
.col-laufband--halt:hover .col-laufband__spur{ animation-play-state:paused; }
@keyframes poly-laufband{
  from{ transform:translate3d(0,0,0); }
  to{ transform:translate3d(calc(-1 * var(--lb-weg, 0px)),0,0); }
}
/* Bewegung reduzieren: der Satz steht still (das Skript baut dann gar nicht
   erst auf). Die Regel doppelt das ab, falls die Klasse doch gesetzt wurde. */
@media (prefers-reduced-motion:reduce){
  .col-laufband .col-laufband__spur{ animation:none; }
}
/* In einer Grid-/Reihen-Zelle liefert das Raster die Außenlinien. */
.poly-cell .col-laufband{ border:0; }

/* ---------- col/ticker -------------------------------------
   Eigenständiger Ticker (aus nav-horizontal ausgelagert): Tickertext
   linksbündig, übliches Padding, mono. Blinkender Cursor via ::after
   (Keyframe poly-caret). Text tippt module/col/ticker.js in das <p>.
------------------------------------------------------------- */
.col-ticker{
  display:flex; align-items:center; min-width:0;   /* zwingt den nowrap-Text nicht auf Container-Breite */
  padding:var(--pad-medium) var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
  overflow:hidden;
}
.col-ticker p{
  max-width:100%; margin:0; white-space:nowrap; overflow:hidden;
  font-family:var(--font-mono); font-weight:500;
  font-size:var(--button-size); line-height:var(--button-lh); color:var(--ink);
}
.col-ticker p::after{ content:"|"; margin-left:2px; animation:poly-caret 1s steps(1) infinite; }
@media (max-width:480px){ .col-ticker{ padding:var(--pad-medium-mobile) var(--pad-big-mobile); } }
/* In der Side: feste Breite (240) füllen und umbrechen statt die Spalte zu dehnen.
   Ausnahme: oben & unten padding-big. */
.sect-grid__side .col-ticker{ align-items:flex-start; overflow:hidden;
  padding-top:var(--pad-big); padding-bottom:var(--pad-big); }
.sect-grid__side .col-ticker p{ white-space:normal; overflow-wrap:anywhere; }

/* ---------- col/side-empty (leere Side, nur Rahmen) --------- */
.col-side-empty{ border:var(--linewidth) solid var(--poly-line); }

/* ---------- col/side-polylogo-nav-vertikal ------------------
   Figma 2029:557 — Seitenspalte (240px): Logo (nav-h hoch) +
   col/side-links-vertikal + leerer Füll-Rahmen, der die restliche
   Section-Höhe auffüllt. Desktop-only (Tablet/Mobile keine Side).
------------------------------------------------------------- */
/* Linien: Container = .lines-v (Markup) überlappt Logo + Menü + Füller;
   kein eigener Rahmen. Logo bekommt einen Rahmen (bildet den oberen
   Frame-Teil). Das Menü ist selbst ein .lines-v. */
.col-side-polylogo-nav-vertikal{
  box-sizing:border-box; width:100%; height:100%;
}
.col-side-polylogo-nav-vertikal__logo{
  box-sizing:border-box; width:100%; flex:0 0 auto;
  /* Basis (z.B. Footer): kein umlaufender Rahmen, nur die Unterkante — das
     Logo steht frei über den gerahmten Zeilen darunter. In der Kopfzeile
     kommen oben/links dazu, siehe die headbar-Regel weiter unten. */
  border-bottom:var(--linewidth) solid var(--poly-line);
  line-height:0;   /* kein Inline-Whitespace */
}
/* Kopfzeile: EIN Rahmen um die ganze Bar statt zwei aneinanderstoßender
   Kästen. Das Logo liefert oben/links/unten, die Nav (bzw. der Close-Bereich)
   rechts/oben/unten — die gemeinsame Kante zwischen beiden entfällt, es gibt
   also keinen Trenner mehr zwischen Logo und Menü. */
.sect-grid--headbar .col-side-polylogo-nav-vertikal__logo{
  /* Rechte Kante als TRANSPARENTER Rahmen statt gar keinem: das SVG skaliert
     auf width:100% der Zelle, und nur mit beiden seitlichen Rahmen bleibt die
     Innenbreite 238 → Logohöhe glatt 119 → Zellhöhe 121. Ohne rechte Kante
     wären es 239 → 119,5 und die Unterkante der Bar läge auf einem halben
     Pixel (weiche Linie bei DPR 1). */
  border:var(--linewidth) solid var(--poly-line);
  border-right-color:transparent;
}
.sect-grid--headbar .col-nav-horizontal{ border-left:0; }
.sect-grid--headbar .col-nav-close{
  border-top:var(--linewidth) solid var(--poly-line);
  border-right:var(--linewidth) solid var(--poly-line);
}
.col-side-polylogo-nav-vertikal__logo img{ width:100%; height:auto; display:block; }
/* Klickbares Logo: Anker füllt die Zelle, SVG erbt die Ink-Farbe (kein Link-Blau). */
.logo-link{ display:block; line-height:0; color:inherit; text-decoration:none; }
/* Nur Desktop: SVG füllt den 240er-Kasten — sonst hält die feste width="240"
   die Side auf 242 (2px-Versatz zu den Sections darunter). Logo skaliert dabei
   ~2px schmaler (unauffällig). Tablet/Mobile behalten die feste col-polymono-
   Breite (240 / Mobile 120×60), damit der Mobile-Logo-Switch stimmt. */
@media (min-width:961px){
  .col-side-polylogo-nav-vertikal__logo svg{ width:100%; height:auto; display:block; }
}
.col-side-polylogo-nav-vertikal .col-side-links-vertikal{ width:100%; }
.col-side-polylogo-nav-vertikal__filler{
  flex:1 1 auto; min-height:0;
  border:var(--linewidth) solid var(--poly-line);
}

/* ---------- col/header --------------------------------------
   Figma 2029:71 — großer Kopf-Block (padding-big, gap-medium):
   60px-Freiraum, H2, mod/datum-headline-body (schmaler Intro),
   volle Trennerlinie, mod/subtext-2cols, Fließtext.
   Ohne Sticky (das ist sect/-Ebene).
------------------------------------------------------------- */
.col-header{
  box-sizing:border-box; width:100%;
  padding:var(--pad-big);
  border:var(--linewidth) solid var(--poly-line);
  display:flex; flex-direction:column; align-items:flex-start;
  gap:var(--pad-medium);
}
.col-header__h2{ width:100%; }
.col-header .mod-datum-headline-body{ width:100%; }   /* Body full-width (war 320px) */
.col-header__trenner{
  width:100%; height:0;
  border-top:var(--linewidth) solid var(--ink);   /* Trenner = polyDarkBlue */
}
.col-header .mod-subtext-2cols{ width:100%; }
.col-header__body{ width:100%; }
@media (max-width:480px){
  .col-header{ padding:var(--pad-big-mobile); }
}

/* ---------- col/contact-form --------------------------------
   Figma 2112:6400 (vereinfacht) — eine Spalte: das Formular
   (Name+Position, E-Mail, Projektart-Select volle Breite, Nachricht als
   wachsende Textarea), darunter bündig der Absenden-Button (col/button-big
   über die volle Breite). Der frühere Intro-Block (Headline + Text) ist
   raus — der wird aus eigenen Modulen davor gesetzt.
------------------------------------------------------------- */
/* Der Kasten IST das <form> (nicht ein div darin): der Absenden-Knopf sitzt
   als bündiger unterer Abschluss außerhalb des gepolsterten Feldblocks, und
   ein Knopf muss in seinem Formular stehen. Das form-Element selbst bringt in
   manchen Browsern einen unteren Außenabstand mit — hier weg. */
.col-contact-form{
  display:flex; flex-direction:column;
  margin:0;
  border:var(--linewidth) solid var(--poly-line);
}
.col-contact-form__form{
  box-sizing:border-box;
  padding:var(--pad-big);
  display:flex; flex-direction:column; align-items:stretch;
  gap:var(--pad-small);
}
.col-contact-form__row2{
  display:grid; grid-template-columns:1fr 1fr; column-gap:var(--pad-small);
}
.col-contact-form .mod-form-select{ width:100%; }   /* Select + Popup volle Breite */
/* Absenden = col/button-big über die volle Breite, bündig als unterer
   Abschluss des Formular-Kastens: er liegt AUSSERHALB des gepolsterten
   Formulars und legt seinen Rahmen per margin:-1px deckungsgleich auf den
   Container-Rahmen (links/rechts/unten) — kein Abstand darunter, keine
   Doppellinie. */
.col-contact-form > .col-contact-form__submit{
  /* width:auto statt der 100% aus .btn--big — sonst rechnet der Button 100%
     der Innenbreite und die negativen Margins ziehen ihn nicht bis auf die
     Container-Rahmen. Mit auto greift align-items:stretch der Spalte. */
  width:auto;
  /* Nur seitlich auf die Container-Rahmen legen. Unten KEINE Naht mehr: unter
     dem Button folgt die Rechtszeile, die Button-Unterkante ist deren
     Trennlinie. */
  margin:0 calc(-1 * var(--linewidth));
  /* Es ist jetzt ein echtes <button type="submit"> statt eines <a href="#">.
     Ein Klick soll ja auch etwas abschicken — und die Tastatur (Enter im
     Feld) soll dasselbe tun. Safari und Firefox geben Knöpfen sonst eine
     eigene Grundoptik (Radius, Verlauf), die den .btn-Stil an den Rändern
     übermalt.

     KEIN `font:inherit` hier. Das stand einen Tag lang da, um die
     System-Schrift des Knopfes abzuräumen — und hat den Knopf prompt in die
     Serifenschrift des Dokuments gesetzt: die Regel ist spezifischer als
     `.btn` und schlug damit auch dessen `font-family:var(--font-mono)`. Nötig
     war sie ohnehin nie: was der Browser von sich aus mitbringt, verliert
     gegen JEDE Regel aus unseren Dateien, und `.btn` setzt Familie, Größe,
     Schnitt und Laufweite bereits vollständig. */
  -webkit-appearance:none; appearance:none; border-radius:0;
}
/* Während des Sendens: nicht klickbar, aber sichtbar dasselbe Element —
   kein Ausgrauen. Der Knopftext sagt bereits, was los ist. */
.col-contact-form__submit[disabled]{ cursor:progress; }
.col-contact-form__submit[disabled]:hover{
  background:var(--btn-surface, var(--surface)); color:var(--btn-ink, var(--ink));
  border-color:var(--poly-line);
}

/* Spam-Falle: für Menschen unerreichbar, für Automaten verlockend. Ihre
   Position steht INLINE im Markup und NICHT hier — sie darf unter keinen
   Umständen sichtbar werden, auch nicht bei einem Besucher, der noch ein altes
   Stylesheet im Cache hat. Genau das ist passiert: ein weißes Extra-Feld
   mitten im Formular. Was hier stünde, wäre eine Zusage, die eine Datei
   einhalten muss, die vielleicht gar nicht ankommt.

   Sichtbarkeit generell: Meldung und Danke-Block schalten über das
   hidden-Attribut bzw. Inline-Stil (module/mod/form-send.js). Das Stylesheet macht
   nur noch die Optik — Farbe, Abstand —, nicht das Ob. */

/* Meldung (Pflichtfelder / Versand ging schief). */
.col-contact-form__meldung{ margin:0; color:var(--poly-magenta); }

/* Fehlendes Pflichtfeld: die Linie unter dem Feld und sein Label wechseln
   auf polyMagenta. Kein Symbol, kein Kasten — die Zeile ist die Auszeichnung,
   genau wie im Ruhezustand die Linie das Feld bildet. */
.col-contact-form .mod-form-text.is-fehlt{ border-bottom-color:var(--poly-magenta); }
.col-contact-form .mod-form-text.is-fehlt .mod-form-text__label{ color:var(--poly-magenta); }

/* Nach dem Absenden bleibt der Kasten stehen, sein Inhalt wird ersetzt: die
   Felder, der Knopf und die Rechtszeile treten ab, der Dank tritt an ihre
   Stelle. So kann niemand zweimal abschicken, und die Section behält ihren
   Platz im Raster. Das Umschalten macht module/mod/form-send.js (hidden + Inline),
   hier steht nur, wie der Dank aussieht, wenn er da ist. */
.col-contact-form__danke{
  box-sizing:border-box; padding:var(--pad-big);
}
.col-contact-form__danke p{ margin:0; }
/* Rechtszeile unter dem Button: eigene Zeile im Kasten, Tag-Typo, Geometrie
   wie die Filter-/Ticker-Zeilen (pad-medium/pad-big). */
.col-contact-form__legal{
  box-sizing:border-box; width:100%;
  padding:var(--pad-medium) var(--pad-big);
  color:var(--ink);
}
@media (max-width:480px){
  .col-contact-form__form{  padding:var(--pad-big-mobile); }
  .col-contact-form__row2{ grid-template-columns:1fr; }
  .col-contact-form__legal{ padding:var(--pad-medium) var(--pad-big); }
  .col-contact-form__danke{  padding:var(--pad-big-mobile); }
}

/* ---------- col/button-big ----------------------------------
   Figma 2039:994 — vormals elem/button-big. Als Main-Baustein zu
   col/ gewandert (füllt immer die volle Column-Breite). Baut auf der
   Basis `.btn` (elem/) auf: Rahmen polyDarkBlue_line, px=padding-big,
   py=padding-medium, gap=padding-medium, Typo=button-big, Icon 10px.
   States identisch (Hover grün).
------------------------------------------------------------- */
.btn--big{
  display:flex; width:100%;   /* füllt immer die volle Frame-/Column-Breite */
  /* Feste Höhe (border-box, inkl. Rahmen). 60 minus eine Linienbreite: im
     Stapel teilt sich der Button seine Unterkante per −1px-Naht mit dem
     Nachbarn, die gezeichnete Zeile ist also 1px höher als der Kasten. */
  height:calc(60px - var(--linewidth));
  padding:0 var(--pad-big);
  gap:var(--pad-medium);
  border-color:var(--poly-line);
  font-size:var(--button-big-size); line-height:var(--button-big-lh);
  /* Farben je Instanz übersteuerbar (Inline-Vars aus dem Template):
     --btn-surface/--btn-ink für den Ruhezustand, --btn-h-surface/--btn-h-ink
     für Hover. Ohne Angabe steht der Button auf der FLÄCHE seiner Umgebung
     (--surface) — er hebt sich dann nur durch seine Linien ab und folgt Theme
     und Modus. Vorher war es --poly-line, also ein grauer Kasten, der auf
     jeder Section als Fremdkörper saß. Hover bleibt das Akzentpaar. */
  background:var(--btn-surface, var(--surface));
  color:var(--btn-ink, var(--ink));
}
.btn--big .link-arrow{ width:10px; height:10px; }
/* Schalter (Sprache, Light/Dark) statt Link: kein Pfeil-Icon — der Pfeil
   verspricht „es geht woanders hin". Das Template lässt ihn bei is_switch weg,
   die Klasse bleibt für den Fall, dass ein Icon anders hereinkommt. Ohne Icon
   ist die Beschriftung allein im Flex-Container, also zentriert wie gehabt. */
.btn--big.btn--switch .link-arrow{ display:none; }
/* Aktiver Schalter: dieselbe Auszeichnung wie ein aktiver Menüeintrag
   (`.nav-item.is-active`) — Akzentpaar des Themes. Gilt für den laufenden
   Modus (module/col/mode.js setzt is-active zur Laufzeit) und für die laufende
   Sprache (build.py/Werkbank setzen `active` beim Auflösen des Links).
   Der Rahmen bleibt wie beim Hover auf der Linienfarbe: der Big Button teilt
   seine Kanten im Raster, ein mitgefärbter Rahmen wäre ein halbes Rechteck. */
.btn--big.is-active{
  background:var(--accent);
  color:var(--accent-ink);
  border-color:var(--poly-line);
}
/* Hover: nur die Fläche (grün) + Typo — der Rahmen bleibt polyDarkBlue_line.
   Als Main-Baustein sitzt der Big Button meist in einer Grid-/Reihen-Zelle,
   wo das Raster die Linien liefert und der eigene Rahmen (teilweise) entfällt.
   Ein mitgefärbter Rahmen highlightet dort nur die verbliebenen Kanten — also
   ein halbes Rechteck. Deshalb abweichend von .btn kein Rahmen-Signal. */
.btn--big:hover, .btn--big.is-hover{
  background:var(--btn-h-surface, var(--accent));
  color:var(--btn-h-ink, var(--accent-ink));
  border-color:var(--poly-line);
}

/* ---------- col/grid-cards ----------------------------------
   Generisches, paginierbares Karten-Grid (Portfolio, Testimonials …).
   Desktop/Tablet (gleich): Grid cols×rows, Linien via gap+Hintergrund
   (wie bento). Mehr Karten als Zellen → Blättern über die gemeinsame
   Blätter-Leiste (.poly-pager, Balken/Zähler/Pfeile — dieselbe wie beim
   edit/carousel); weniger Karten als Zellen → leere Filler-Zellen. Mobile:
   eine swipebare Reihe (scroll-snap), Karten 90% breit (nächste lugt an),
   die Leiste zählt dort Karten statt Seiten. Karten sind randlos (das Gitter
   bilden gap+bg), die interne Video|Info-Naht bleibt als Video-Unterkante.
   Spalten = min(data-cols, was bei data-min Kartenbreite passt) — Rows
   (data-rows) bleiben immer gleich; leere Zellen werden schraffiert.
   Verhalten: module/col/grid-cards.js.
------------------------------------------------------------- */
.col-grid-cards{ display:flex; flex-direction:column; }
.col-grid-cards > * + *{ margin-top:calc(-1 * var(--linewidth)); }   /* Grid|Pager-Naht */
.col-grid-cards__grid{
  display:grid;
  grid-template-columns:repeat(var(--cols,3), minmax(0,1fr));
  grid-auto-rows:1fr;   /* alle Zeilen gleich hoch → Filler-Zeilen kollabieren nicht
                           (sonst Höhensprung + doppelte Linie auf der letzten Seite) */
  gap:var(--linewidth);
  background:var(--poly-line);
  border:var(--linewidth) solid var(--poly-line);
}
/* Karte = Grid-Zelle: füllt die Zelle (für unten-verankerte Elemente wie
   info-card__zusatz / testimonial-Person). */
.col-grid-cards__card{ background:var(--surface); min-width:0; display:flex; }
.col-grid-cards__card > *{ flex:1 1 auto; min-width:0; }
/* Leere Zelle: die Standard-Schraffur (Definition + Begründung der Kachelung
   in tokens.css, .poly-hatch). */
.col-grid-cards__filler{ min-width:0; }
/* Karten randlos (Gitter = gap+bg). video-card: interne Naht = Video-Unterkante.
   info-card / testimonial-card: nur äußeren Rahmen weg (keine interne Linie). */
.col-grid-cards__card .comp-video-card > * + *,
.col-grid-cards__card .comp-image-card > * + *{ margin-top:0; }
.col-grid-cards__card .comp-video-card .col-video,
.col-grid-cards__card .comp-image-card .col-image{
  border:0; border-bottom:var(--linewidth) solid var(--poly-line);
}
.col-grid-cards__card .comp-video-card .comp-content-info,
.col-grid-cards__card .comp-image-card .comp-content-info,
.col-grid-cards__card .comp-info-card,
.col-grid-cards__card .comp-testimonial-card{ border:0; }
/* Die Blätter-Leiste ist KEIN gerahmter Kasten mehr, sondern steht frei unter
   dem Gitter — deshalb hier keine −1px-Naht (die gilt für gerahmte Nachbarn). */
.col-grid-cards > .poly-pager{ margin-top:0; }
/* Mobile: swipebare Reihe (scroll-snap), Karten 90% (Anschnitt) */
@media (max-width:480px){
  .col-grid-cards__grid{
    display:flex; position:relative;
    grid-template-columns:none;
    overflow-x:auto; scroll-snap-type:x mandatory;
    -webkit-overflow-scrolling:touch;
  }
  .col-grid-cards__grid > .col-grid-cards__card{
    flex:0 0 90%; scroll-snap-align:start;
  }
  /* Option „Mobile untereinander" (data-mstack): kein Swipe/Pager, alle Karten
     fortlaufend gestapelt (1 Spalte), Filler weg. */
  .col-grid-cards__grid[data-mstack]{
    display:grid; grid-template-columns:1fr; grid-auto-rows:auto;
    overflow-x:visible; scroll-snap-type:none;
  }
  .col-grid-cards__grid[data-mstack] > .col-grid-cards__card{ flex:none; scroll-snap-align:none; }
  .col-grid-cards__grid[data-mstack] > .col-grid-cards__filler{ display:none; }
  /* Leiste entfällt bei Mobile-Stack (grid-cards.js blendet sie zusätzlich aus). */
  .col-grid-cards__grid[data-mstack] ~ .poly-pager{ display:none; }
}

/* ---------- col/polymono (Logo) -----------------------------
   Figma 2024:899 — vormals elem/polymono. Als col-Komponente (Main-
   Baustein / Grid-Zelle / Side-Logo). Einzelversion dunkel, 240×120;
   Mobile 120×60. In Kontexten mit voller Breite (Grid-Zelle, Footer-
   Mobile) auf width:100% übersteuern. SVG-Asset bleibt in elem/.
------------------------------------------------------------- */
.col-polymono{ display:block; width:240px; height:auto; color:var(--logo-ink, var(--ink)); }  /* Inline-SVG erbt via fill:currentColor; --logo-ink macht es im Dark-Modus grün (tokens.css) */
@media (max-width:480px){ .col-polymono{ width:120px; } }   /* Mobile 120×60 */

/* ---------- col/grid ----------------------------------------
   Generisches, NICHT scrollendes Grid (z.B. Footer 2×2 big-buttons).
   Linien wie grid-cards via gap+Hintergrund, aber ohne Paginierung/
   Carousel: Spalten = min(--cols, was bei --min Zellbreite passt),
   der Rest bricht klassisch um / stapelt nach unten (Mobile 1 Spalte).
   Reines CSS (kein JS). Zellen randlos (Gitter = gap+bg).
   Der minmax-Trick deckelt bei --cols (Zell-Min = 1/cols der Breite),
   greift aber auf --min zurück, sobald es enger wird → Umbruch.
------------------------------------------------------------- */
.col-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(
    max(var(--min, 240px),
        calc((100% - (var(--cols, 2) - 1) * var(--linewidth)) / var(--cols, 2))
    ), 1fr));
  grid-auto-rows:1fr;
  gap:var(--linewidth);
  background:var(--poly-line);
  border:var(--linewidth) solid var(--poly-line);
}
.col-grid__cell{ background:var(--surface); min-width:0; display:flex; }
/* Leere Zelle (Rest der letzten Reihe, via module/col/grid-fill.js): dieselbe
   Standard-Schraffur wie grid-cards (siehe tokens.css). */
.col-grid__filler{ min-width:0; }
/* Beliebiges col-Modul als Zelle: äußerer Rahmen weg (Gitter = gap+bg),
   States/Hover bleiben. Zelle wird gefüllt. */
.col-grid__cell > *{ flex:1 1 auto; min-width:0; border:0; }
.col-grid__cell .col-polymono{ width:100%; }
/* comp-Karten im Grid: interne Rahmen (Video/Info) weg → keine Doppellinien;
   video-card behält die Video|Info-Naht als Video-Unterkante. */
.col-grid__cell .col-video,
.col-grid__cell .col-image,
.col-grid__cell .comp-content-info{ border:0; }
.col-grid__cell .comp-video-card .col-video,
.col-grid__cell .comp-image-card .col-image{ border-bottom:var(--linewidth) solid var(--poly-line); }

/* ---------- col/columns (freies Spalten-Grid) ----------------
   Bis zu 4 frei wählbare Module nebeneinander, jede Zelle mit
   eigener min-width (inline). Umbruch, sobald eine Zelle unter
   ihre min-width fiele (flex-wrap); je Zeile füllen die Zellen
   gleichmäßig (flex-grow). Borderlinien: gap = Linienbreite auf
   linienfarbenem Grund → aus zwei Rahmen wird eine 1px-Linie;
   Container-Rahmen = äußerer Frame. Innenrahmen der Module weg,
   damit keine Doppellinien entstehen.
------------------------------------------------------------- */
.col-columns{
  display:flex; flex-wrap:wrap;
  gap:var(--linewidth);
  background:var(--poly-line);
  border:var(--linewidth) solid var(--poly-line);
}
/* col/rows — vertikales Pendant zu col/columns: Zellen stapeln untereinander,
   jede volle Breite, optional min-height je Zelle. Gleiche Border-Logik.
   Zusammen mit col/columns lassen sich beliebige Bento-Raster verschachteln. */
.col-rows{
  display:grid;   /* Zeilen via grid-template-rows: minmax(min, w·fr) (inline) */
  gap:var(--linewidth);
  background:var(--poly-line);
  border:var(--linewidth) solid var(--poly-line);
  flex:1 1 auto;   /* als Flex-Item im Parent die Frame-Höhe füllen */
}
/* Reihen-/Spalten-Zellen setzen flex-grow (Anteil) inline via Template. */
/* gemeinsame Zelle (columns + rows) */
.poly-cell{ background:var(--surface); display:flex; align-items:stretch; min-width:0; }
/* Anteil und Mindestbreite je Spalten-Zelle kommen als Variablen aus dem
   Template (--cellw / --cellmin), nicht mehr als harte Inline-Werte — nur so
   lassen sie sich per Media Query übersteuern (Inline-Styles gewinnen sonst
   gegen jede Regel, und !important kommt im System sonst nirgends vor). */
.col-columns__cell{ flex:var(--cellw, 1) 1 0; min-width:var(--cellmin, 0px); }
/* Mobile: Mindestbreite ignorieren und jede Zelle über die volle Breite —
   sonst schiebt ein min-Wert oberhalb der Viewportbreite das Raster seitlich
   aus dem Bild. */
@media (max-width:480px){
  .col-columns__cell{ min-width:0; flex:1 0 100%; }
}
.poly-cell > *{ flex:1 1 auto; min-width:0; align-self:stretch; border:0; }
/* Modul-Rahmen in der Zelle weg (das Grid liefert die Außenlinien) — sonst
   Doppellinien am Zellenrand. Interne Nähte werden gezielt einseitig zurückgesetzt. */
.poly-cell .col-kapitel-headline,
.poly-cell .col-mind-motion-soul__item,
.poly-cell .col-video,
.poly-cell .col-image,
.poly-cell .comp-content-info,
.poly-cell .comp-info-card,
.poly-cell .comp-testimonial-card,
.poly-cell .comp-video-card,
.poly-cell .comp-image-card,
.poly-cell .comp-video-feature,
.poly-cell .col-content-left-textblock,
.poly-cell .col-content-right-textblock{ border:0; }
/* verschachtelte Grids in einer Zelle: eigener Rahmen weg (Zell-Gap liefert Linien) */
.poly-cell > .col-columns,
.poly-cell > .col-rows{ border:0; }
/* horizontale Composites: eine interne Naht als border-left, −1px-Überlappung aus */
.poly-cell .comp-video-feature > * + *,
.poly-cell .col-content-left-textblock > * + *,
.poly-cell .col-content-right-textblock > * + *{
  border-left:var(--linewidth) solid var(--poly-line); margin-left:0;
}
/* vertikale Composites (video-/image-card): interne Naht als border-top */
.poly-cell .comp-video-card > * + *,
.poly-cell .comp-image-card > * + *{
  border-top:var(--linewidth) solid var(--poly-line); margin-top:0;
}
/* mind-motion-soul: interne Trenner zwischen den Items */
.poly-cell .col-mind-motion-soul__item + .col-mind-motion-soul__item{
  border-left:var(--linewidth) solid var(--poly-line);
}
/* kapitel-headline-mobile (Mobile-Variante via mswap): Kopf/Text sind eigene
   umrahmte Boxen mit −1px-Naht → in der Zelle Außenrahmen weg, eine Naht als
   border-top zurück. */
.poly-cell .col-kapitel-headline-mobile__head,
.poly-cell .col-kapitel-headline-mobile__text{ border:0; }
.poly-cell .col-kapitel-headline-mobile > * + *{
  border-top:var(--linewidth) solid var(--poly-line); margin-top:0;
}
/* kapitel-headline soll die Zellenhöhe füllen */
.poly-cell .col-kapitel-headline,
.poly-cell .col-kapitel-headline-mobile{ height:100%; }

/* ---------- col/video ---------------------------------------
   Figma 2035:280 — vormals col/video. Als eigenständiger Main-Baustein
   zu col/ gewandert (z.B. Showreel-Hero, volle Breite; auch als Zelle in
   comp/video-card, comp/video-feature, col/content-textblocks). Grauer
   Platzhalter (proxy), 1px Rahmen, Default 16:9; weitere Formate per
   Modifier (Notion-Befehl). Ein <video>/<img>/<iframe> als erstes Kind
   füllt die Fläche. link-boxed-Badge oben rechts, optional.
------------------------------------------------------------- */
.col-video, .col-image{
  position:relative; width:100%;
  aspect-ratio:var(--ar-d, 16 / 9);
  /* Platzhalter (solange kein Video/Bild geladen ist): Standard-Schraffur
     statt der grauen proxy-Fläche — dieselbe Kachel wie die leeren
     Grid-Zellen, siehe tokens.css. */
  background-color:var(--surface);
  background-image:repeating-linear-gradient(var(--hatch-angle),
    var(--poly-line) 0 var(--hatch-line),
    var(--surface) var(--hatch-line) calc(var(--hatch-pitch) / 1.41421356));
  background-size:var(--hatch-pitch) var(--hatch-pitch);
  border:var(--linewidth) solid var(--poly-line);
  overflow:hidden;
}
/* Seitenverhältnis pro Breakpoint: --ar-d/t/m via Inline-Style, mit Fallback
   Tablet→Desktop, Mobile→Tablet→Desktop. Gilt für Video- und Bild-Frame. */
@media (max-width:960px){ .col-video, .col-image{ aspect-ratio:var(--ar-t, var(--ar-d, 16 / 9)); } }
@media (max-width:480px){ .col-video, .col-image{ aspect-ratio:var(--ar-m, var(--ar-t, var(--ar-d, 16 / 9))); } }
/* Das Medium steht 1px ueber den Rahmen hinaus (overflow:hidden schneidet
   es ab; bei object-fit:cover ist der Millimeter unsichtbar). Grund: Video-
   und Bildflaechen werden vom Browser auf ganze Geraetepixel gerastert, der
   Rahmen aber nicht — bei krummen Breiten (Spalten, Prozent, Retina) bleibt
   links/rechts ein 1px-Spalt, durch den die Schraffur des Platzhalters
   scheint (gemeldet an col/video, mit Bild). Der Ueberstand deckt ihn. */
.col-video > video, .col-video > img, .col-video > iframe,
.col-image > img{
  position:absolute; inset:-1px; width:calc(100% + 2px); height:calc(100% + 2px);
  object-fit:cover; border:0; display:block;
}
/* FrameRate-Embed: eigene FLÄCHE, sonst scheint die Schraffur durch.
   Ein iframe füllt zwar den Rahmen — sein INHALT aber nicht unbedingt: der
   FrameRate-Player hält sein eigenes Format und lässt an den Rändern ein paar
   Pixel frei (unten dazu die Bedienleiste). Und weil die eingebettete Seite
   keinen eigenen Hintergrund malt, ist das iframe an diesen Stellen
   DURCHSICHTIG — man sieht den Elternhintergrund, hier also den Platzhalter.
   Genau das war zu sehen: gestrichelte Ränder links, rechts und unten.
   `object-fit:cover` hilft nicht, das gilt für iframes nicht.
   Dieselbe Fläche wie im Overlay-Player (.col-video-player__embed):
   polyDarkBlue, unabhängig vom Modus — ein Video sitzt auf Dunkel, nicht auf
   Beige. Die Schraffur bleibt am Rahmen: solange nichts geladen ist, ist sie
   ja die richtige Antwort. */
.col-video__embed{ background:var(--poly-dark); }

/* ---- FrameRate: Klick zum Laden ---------------------------------------
   Solange nicht geklickt wurde, steht hier KEIN iframe, sondern ein Knopf.
   Warum, steht ausführlich in build/gen_templates.py (FR_LADEN): ein iframe
   mit src lädt beim Seitenaufruf, und damit ginge ungefragt eine Verbindung
   in die USA raus. Die Seite ist sonst messbar kontaktfrei — das bleibt so.

   Das Standbild (falls vorhanden) liegt als eigenes Bild darunter. Es trägt
   bewusst NICHT .col-video__poster: das gehört dem lokalen Video und wird von
   module/col/media.js beim Hover ausgeblendet — hier soll es liegen bleiben. */
.col-fr-standbild{
  position:absolute; inset:-1px; width:calc(100% + 2px); height:calc(100% + 2px);   /* Ueberstand, s. .col-video > video */
  object-fit:cover; display:block; z-index:0;
}
.col-video-player .col-fr-standbild{ inset:0; width:100%; height:100%; object-fit:contain; background:var(--poly-dark); }
/* Der Knopf füllt die ganze Fläche — man muss nicht das kleine Dreieck
   treffen. Sichtbar ist nur das Dreieck; der Hinweistext sass bis zum 19.08.
   darunter und ist auf Wunsch raus, er lebt als aria-label weiter.
   Der leichte dunkle Schleier bleibt: er hebt das Dreieck von jedem Standbild
   ab. Ohne Standbild liegt darunter die Schraffur, die ohnehin „hier ist noch
   nichts geladen" heißt. */
.col-fr-laden{
  position:absolute; inset:0; z-index:2;
  display:flex; align-items:center; justify-content:center;
  width:100%; height:100%; margin:0; padding:0;
  border:0; border-radius:0;
  -webkit-appearance:none; appearance:none;
  background:color-mix(in srgb, var(--poly-dark) 28%, transparent);
  color:var(--poly-beige); cursor:pointer;
  transition:background .15s, color .15s;
}
.col-fr-laden:hover{ background:color-mix(in srgb, var(--poly-dark) 45%, transparent); }
.col-fr-laden__play{ display:block; width:44px; height:44px; flex:0 0 auto; }
.col-fr-laden__play svg{ display:block; width:100%; height:100%; stroke-width:var(--linewidth); }
.col-fr-laden:hover .col-fr-laden__play{ color:var(--accent); }
@media (prefers-reduced-motion:reduce){ .col-fr-laden{ transition:none; } }
/* Poster-Overlay: liegt über dem Video (Idle = Standbild). module/col/media.js blendet
   es bei Hover aus (nach 0,5s) und beim Verlassen sofort wieder ein — so bleibt
   der Thumbnail auch nach dem ersten Abspielen erhalten. */
.col-video__poster{ z-index:1; }
.col-image__media--contain{ object-fit:contain; }
/* Format-Varianten (col-video--<ratio>) — Inhalt füllt via object-fit:cover
   und wird passend vertikal/horizontal beschnitten. */
.col-video--16-9{ aspect-ratio:16 / 9; }
.col-video--1-1{ aspect-ratio:1 / 1; }
.col-video--4-5{ aspect-ratio:4 / 5; }
.col-video--9-16{ aspect-ratio:9 / 16; }
.col-video--4-3{ aspect-ratio:4 / 3; }
.col-video--3-2{ aspect-ratio:3 / 2; }
.col-video--21-9{ aspect-ratio:21 / 9; }
/* Beliebiges Format alternativ per Inline-Style: style="aspect-ratio:5/4" */
.col-video__link, .col-image__link{
  position:absolute; top:0; right:0; z-index:2;
  width:20px; height:20px; display:block; line-height:0;   /* Desktop/Tablet */
}
@media (max-width:480px){ .col-video__link, .col-image__link{ width:15px; height:15px; } }  /* Mobile */
.col-video__link svg, .col-video__link img,
.col-image__link svg, .col-image__link img{ display:block; width:100%; height:100%; }

/* ============================================================
   col/collection — datengetriebene Karten-Sammlung mit Facetten-
   Filter (PLUS/MINUS). Filter-Bar (auto aus Karten-Tags) + Grid
   (Optik von col/grid-cards wiederverwendet). Filterung clientseitig
   (module/col/collection.js). Skaliert bis zu kategorisierten Tags.
   ============================================================ */
.col-collection{ display:flex; flex-direction:column; }
/* Filter|Grid-Naht: Unterkante der Filter-Leiste verschmilzt mit der Grid-
   Oberkante zu einer Linie (System-Muster wie grid-cards Grid|Pager). */
.col-collection > * + *{ margin-top:calc(-1 * var(--linewidth)); }
/* Filter-Leiste = eigener gerahmter Column-Container. Sie besteht aus zwei
   Teilen: der Klapp-Leiste (nur <=960 sichtbar) und der Chip-Reihe. Auf Desktop
   ist die Leiste aus und die Chips stehen wie bisher als umbrechende Reihe mit
   System-Padding (pad-big/pad-medium, wie col/ticker) im Rahmen. */
.col-collection__filter{
  box-sizing:border-box; width:100%;
  display:flex; flex-direction:column; align-items:stretch;
  border:var(--linewidth) solid var(--poly-line);
}
.coll-filter__chips{
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--pad-small);
  padding:var(--pad-medium) var(--pad-big);
}
@media (max-width:480px){ .coll-filter__chips{ padding:var(--pad-medium-mobile) var(--pad-big-mobile); gap:8px; } }
/* Klapp-Leiste: erst ab Tablet sichtbar. Zeile in Ticker-Geometrie
   (pad-medium/pad-big), Mono-Typo wie ein Tag-Label, Icon links, Chevron
   rechts. Zugeklappt gestartet — die Chips haengen an .is-open. */
.coll-filter__bar{ display:none; }
@media (max-width:960px){
  .coll-filter__bar{
    display:flex; align-items:center; gap:var(--pad-small);
    width:100%; box-sizing:border-box;
    padding:var(--pad-medium) var(--pad-big);
    background:none; border:0; cursor:pointer; text-align:left;
    font-family:var(--font-mono); font-weight:500;
    font-size:var(--button-size); line-height:var(--button-lh);
    letter-spacing:var(--button-ls); color:var(--ink);
  }
  /* Label hugt, der Zaehler steht direkt dahinter — und DER schiebt den
     Chevron nach rechts (margin-right:auto). Vorher streckte sich das Label
     ueber die ganze Zeile; die Zahl waere dann am rechten Rand gelandet, weit
     weg von dem Wort, auf das sie sich bezieht. */
  .coll-filter__label{ flex:0 1 auto; min-width:0; }
  /* Zaehler: wie viele Ergebnisse gerade uebrig sind. Steht NUR, wenn ein
     Filter gesetzt ist (col/collection.js) — zugeklappt sieht man der Leiste
     sonst nicht an, dass gefiltert wird, und die halb leere Seite darunter
     wirkt wie ein Fehler. Farben gedreht: Flaeche in --ink, Schrift in
     --surface. Damit liest sie sich als Marke, nicht als weiteres Wort. */
  .coll-filter__zahl{
    flex:0 0 auto; margin-right:auto;
    display:inline-block; min-width:1em; text-align:center;
    padding:1px 6px;
    background:var(--ink); color:var(--surface);
    font-size:var(--tag-size); line-height:var(--tag-lh);
    letter-spacing:var(--tag-ls); font-weight:500;
  }
  .coll-filter__zahl[hidden]{ display:none; }
  /* Ohne Zahl muss trotzdem etwas den Chevron nach rechts schieben. */
  .coll-filter__zahl[hidden] + .coll-filter__chev{ margin-left:auto; }
  .coll-filter__ic, .coll-filter__chev{
    flex:0 0 auto; display:block; width:16px; height:16px; line-height:0;
    color:var(--ink);
  }
  .coll-filter__ic svg, .coll-filter__chev svg{ display:block; width:100%; height:100%; }
  .coll-filter__chev{ transition:transform .15s; }
  .coll-filter__bar[aria-expanded="true"] .coll-filter__chev{ transform:rotate(180deg); }
  .coll-filter__bar:hover .coll-filter__label{ color:var(--poly-magenta); }
  /* Zugeklappt: Chips weg. Aufgeklappt: Chips unter der Leiste, getrennt durch
     eine Linie (die Leiste selbst hat keinen eigenen Rahmen). */
  .col-collection__filter > .coll-filter__chips{ display:none; }
  /* Aufgeklappt gehen Balken und Chips zu EINEM Kasten zusammen — ohne
     Trennlinie darunter. */
  .col-collection__filter.is-open > .coll-filter__chips{ display:flex; }
}
/* Chip = elem/tag (Rahmen polyDarkBlue_line) + Zähler; Padding folgt der Tag-
   Spec und skaliert am Mobile-Breakpoint mit. PLUS grün (ODER), MINUS magenta
   (NICHT). Reset trägt .tag (gleicher Rahmen/Padding), aber NICHT .coll-tag,
   damit das Filter-JS ihn nicht als Tag greift. */
.coll-tag, .coll-reset{
  gap:6px; padding:8px var(--pad-small);
  border:var(--linewidth) solid var(--poly-line);
}
@media (max-width:480px){ .coll-tag, .coll-reset{ padding:6px var(--pad-small); } }
.coll-tag__n{ font-size:var(--tag-size); opacity:.55; font-variant-numeric:tabular-nums; }
.coll-tag.is-plus{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent-ink); }
.coll-tag.is-plus .coll-tag__n{ opacity:.7; }
.coll-tag.is-minus{ background:var(--poly-magenta); color:var(--poly-beige); border-color:var(--poly-dark); }
.coll-tag.is-minus .coll-tag__n{ opacity:.8; }
/* Reset: sieht aus wie ein Tag; Hover = Magenta-Signal fürs Zurücksetzen. */
.coll-reset{ color:var(--ink); }
.coll-reset:hover, .coll-reset:focus-visible{ background:var(--poly-magenta); color:var(--poly-beige); border-color:var(--poly-dark); }
/* Grid erbt die col-grid-cards-Optik; nur der Pager entfällt.
   Anders als grid-cards ist die Spaltenzahl NICHT fix: --cols ist die
   Obergrenze, --min die Mindestbreite einer Zelle — wird es enger, bricht
   das Grid auf weniger Spalten um (derselbe minmax-Trick wie col/grid).
   Nur oberhalb des Mobile-Breakpoints: ab ≤480 gilt weiter die
   Swipe-/Stack-Regel aus dem grid-cards-Block. */
@media (min-width:481px){
  .col-collection__grid{
    grid-template-columns:repeat(auto-fill, minmax(
      max(var(--min, 300px),
          calc((100% - (var(--cols, 3) - 1) * var(--linewidth)) / var(--cols, 3))
      ), 1fr));
  }
}

/* ---------- col/side-collection-filter (Desktop-Side) -------
   Die Filter-Chips als 240er-Seitenspalte NEBEN dem Karten-Grid — Optik und
   Geometrie von elem/nav-item-vertikal: volle Spaltenbreite, rechtsbündig,
   Mono-Typo (button), Padding pad-medium / links pad-big, Abstand Text→Zahl
   = pad-small (derselbe wie Label→Pfeil beim nav-item). Der letzte Chip
   („Filter Reset") sieht genauso aus.
   Linien: der Container trägt den Frame, jeder Chip seinen eigenen Rahmen —
   die Chips liegen per margin:-1px deckungsgleich auf dem Container-Rahmen
   bzw. auf dem Rahmen des Vorgängers, aus zwei Linien wird also eine. Kein
   Füll-Rahmen nötig: unterhalb des letzten Chips läuft der Container-Frame
   bis zur Zeilenunterkante weiter (die Side streckt über
   `.sect-grid__side > *`), und ein leerer Füll-Rahmen könnte hier gar nicht
   erst das bekannte 1px-Artefakt erzeugen.
   Die Collection rendert die Spalte selbst (eigene .sect-grid-Zeile), damit
   die Chips im .col-collection bleiben und module/col/collection.js sie ohne
   Sonderregel findet. Ab ≤960 fällt die Side über die Basis-Regel in
   sect.css weg; die klappbare Leiste (Reflow-Partner) übernimmt oben.
------------------------------------------------------------- */
.col-side-collection-filter{
  box-sizing:border-box; width:100%;
  display:flex; flex-direction:column; align-items:stretch;
  border:var(--linewidth) solid var(--poly-line);
}
.col-side-collection-filter > .tag{
  width:auto;                      /* Breite kommt aus align-items:stretch */
  justify-content:flex-end; gap:var(--pad-small); text-align:right;
  padding:var(--pad-medium) var(--pad-medium) var(--pad-medium) var(--pad-big);
  font-size:var(--button-size); line-height:var(--button-lh);
  /* Rahmen auf den Container- bzw. Vorgänger-Rahmen legen (eine Linie). */
  margin:calc(-1 * var(--linewidth)) calc(-1 * var(--linewidth)) 0;
}
/* States: NUR Typo und Fläche, der Rahmen bleibt immer polyDarkBlue_line.
   Grund: die Chip-Rahmen liegen deckungsgleich auf dem Container- bzw.
   Vorgänger-Rahmen — eine mitgefärbte Chip-Linie ergäbe an den geteilten
   Kanten ein halbes Rechteck (mal liegt die Chip-Linie oben, mal die des
   Nachbarn). Deshalb hier abweichend von .tag/.coll-tag kein Rahmen-Signal. */
.col-side-collection-filter > .tag,
.col-side-collection-filter > .tag:hover,
.col-side-collection-filter > .tag.is-hover,
.col-side-collection-filter > .coll-tag.is-plus,
.col-side-collection-filter > .coll-tag.is-minus{ border-color:var(--poly-line); }

/* Filter-Zeile der Collection: eigene .sect-grid im Modul. Keine eigenen
   Regeln nötig — Side|Main-Naht, Side-Wegfall ≤960 und der Reflow-Partner
   kommen aus sect.css. */

/* ============================================================
   col/einträge — Sammlung zusammengesetzter Einträge (mehrere
   Module + Tags) mit derselben PLUS/MINUS-Filter-Engine wie
   col/collection. Kein Karten-Grid: jeder Eintrag ist eine eigene
   .sect-grid-Zeile (Side pro Eintrag + Main-Stack), die Filter-Zeile
   ebenso (Side nur keine/leer/pattern). Kein Padding zwischen
   Einträgen/Modulen — die −1px-Nähte (.col-collection > *+*,
   .sect-grid, .sect-grid__main) legen benachbarte Rahmen zu EINER
   Linie zusammen (keine Doppellinien).
   ============================================================ */
/* Filter-Zeile trägt .sect-grid → Side|Filter-Naht + Reflow gratis.
   Weitere Regeln nicht nötig: Stapel-Nähte kommen aus .col-collection
   und .sect-grid (siehe sect.css). */

/* ============================================================
   col/video-player — voller HTML5-Player fürs Video-Overlay
   (native controls, Ton an, kein Autoplay). Ratio-Rahmen wie
   col/video; Video wird eingepasst (contain, nichts abschneiden).
   ============================================================ */
.col-video-player{
  position:relative; width:100%;
  aspect-ratio:var(--ar-d, 16/9);
  border:var(--linewidth) solid var(--poly-line);
  overflow:hidden; background:var(--poly-dark);
}
@media (max-width:960px){ .col-video-player{ aspect-ratio:var(--ar-t, var(--ar-d, 16/9)); } }
@media (max-width:480px){ .col-video-player{ aspect-ratio:var(--ar-m, var(--ar-t, var(--ar-d, 16/9))); } }
.col-video-player__media{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:contain; background:var(--poly-dark); display:block;
}
/* FrameRate-Embed (iframe) füllt den Player-Rahmen wie das Video-Element;
   im col-video-Rahmen greift schon die generische „> iframe"-Regel oben. */
/* In einer Zelle (col/columns, col/rows) NICHT strecken. Die Zelle ist so hoch
   wie die höchste Spalte daneben, und eine so entstandene feste Höhe sticht
   aspect-ratio aus: der Rahmen wird zu hoch, und weil ein Player sein Bild
   nicht beschneiden darf (object-fit:contain), liegt das Video mit schwarzen
   Balken darin (gemessen: 1.66 statt 1.78 bei 16:9). Oben ausgerichtet behält
   er sein Format; was in der Zelle übrig bleibt, ist einfach Fläche.
   col/video und col/image sind ausdrücklich NICHT gemeint — die füllen mit
   object-fit:cover und sollen sich strecken. */
.poly-cell > .col-video-player{ align-self:flex-start; }
.col-video-player__embed{
  position:absolute; inset:0; width:100%; height:100%;
  border:0; display:block; background:var(--poly-dark);
}

/* ============================================================
   col/projekt-videoliste — Videowahl unter dem Overlay-Player.
   Gefüllt von module/col/overlay.js, NUR wenn das Projekt mehr als ein Video
   trägt; sonst bleibt der Rahmen versteckt (hidden im Markup) und die Seite
   sieht aus wie ohne das Modul. Daumen in fester Höhe, Breite aus 16:9;
   horizontal wischbar wie edit/carousel — der Anschnitt des nächsten Daumens
   ist der Hinweis „es geht weiter", deshalb kein Pager und kein Scrollbalken.
   ============================================================ */
.col-projekt-videoliste{
  padding:var(--pad-medium);
  border:var(--linewidth) solid var(--poly-line);
  background:var(--surface);
}
.col-projekt-videoliste[hidden]{ display:none; }
/* Die SPUR scrollt; der Rahmen darum steht. Darunter die Blätter-Leiste
   (p-pager, module/col/pager.js) — dieselbe wie beim edit/carousel. Sie ist
   nötig, weil der native Scrollbalken verborgen ist: Trackpad und Finger
   wischen, aber mit der Maus kam man am Desktop an abgeschnittene Daumen
   nicht heran (gemeldet). pager.js blendet sie aus, sobald alles passt. */
.col-projekt-videoliste__spur{
  display:flex; align-items:flex-start; gap:var(--pad-small);
  overflow-x:auto; overscroll-behavior-x:contain;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
}
.col-projekt-videoliste__spur::-webkit-scrollbar{ display:none; }
.col-projekt-videoliste .poly-pager{ margin-top:var(--pad-small); }
.col-projekt-videoliste__thumb{
  flex:0 0 auto; width:160px;
  margin:0; padding:0; border:var(--linewidth) solid var(--poly-line);
  border-radius:0; -webkit-appearance:none; appearance:none;
  background:var(--surface); color:var(--ink); cursor:pointer;
  display:block; text-align:left;
}
@media (max-width:480px){ .col-projekt-videoliste__thumb{ width:120px; } }
/* Bildfläche: FESTE Höhe (160×90 = 16:9) statt aspect-ratio — ein Bild, das
   noch lädt oder fehlt, hält mit aspect-ratio seine Höhe nicht (gemessen:
   der Kasten fiel auf die Höhe des Kaputtbild-Zeichens zusammen und die
   Leiste sprang beim Laden). Mit fester Höhe steht die Leiste vom ersten
   Frame an ruhig; cover beschneidet abweichende Formate. Ohne Bild bleibt
   die Schraffur — dieselbe Kachel wie überall, wo noch nichts geladen ist. */
.col-projekt-videoliste__bild,
.col-projekt-videoliste__thumb::before{ height:90px; }
@media (max-width:480px){
  .col-projekt-videoliste__bild,
  .col-projekt-videoliste__thumb::before{ height:68px; }
}
.col-projekt-videoliste__bild{
  display:block; width:100%; object-fit:cover;
  background:var(--poly-dark);
}
.col-projekt-videoliste__thumb:not(:has(.col-projekt-videoliste__bild))::before{
  content:""; display:block; width:100%;
  background-color:var(--surface);
  background-image:repeating-linear-gradient(var(--hatch-angle),
    var(--poly-line) 0 var(--hatch-line),
    var(--surface) var(--hatch-line) calc(var(--hatch-pitch) / 1.41421356));
  background-size:var(--hatch-pitch) var(--hatch-pitch);
}
.col-projekt-videoliste__label--leer{ visibility:hidden; }
.col-projekt-videoliste__label{
  display:block; padding:4px 6px;
  font-family:var(--font-mono); font-weight:500;
  font-size:var(--tag-size); line-height:var(--tag-lh); letter-spacing:var(--tag-ls);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
/* Zustände: Hover eine Haarlinie Akzent, das GEWÄHLTE Video drei — dieselbe
   Sprache wie der Hover-Rahmen auf klickbaren Medien (edit-fig__zoom).
   Outline statt inset-Schatten und statt Rahmenwechsel: ein inset box-shadow
   läge UNTER dem Bild (Kinder malen über inset-Schatten), ein dickerer border
   liesse die Leiste beim Wechsel springen. Outline malt über den Inhalt und
   kostet kein Layout; negativ versetzt liegt es innen. */
.col-projekt-videoliste__thumb:hover,
.col-projekt-videoliste__thumb:focus-visible{
  outline:var(--linewidth) solid var(--accent);
  outline-offset:calc(-1 * var(--linewidth));
}
.col-projekt-videoliste__thumb.is-active{
  outline:3px solid var(--accent);
  outline-offset:-3px;
}

/* ============================================================
   Overlay-Wipe, Weg 1: View Transitions über den Seitenwechsel.

   Warum überhaupt: ein Overlay ist eine eigene SEITE. Beim Navigieren wird das
   alte Dokument abgebaut und das neue gemalt — dazwischen ist zwangsläufig ein
   Moment leerer Seitengrund zu sehen, VOR dem Wipe. Genau das war der Punkt
   „direkt background leer, dann wiped der Inhalt rein": maskieren allein kann
   das nicht lösen, weil es nichts gibt, worüber maskiert werden könnte.

   Cross-Document View Transitions lösen es an der Wurzel: der Browser hält ein
   Bild der ALTEN Seite fest und lässt die neue darüber laufen. Das Overlay
   baut damit über der Collection auf, aus der man kommt — kein leerer
   Zwischenzustand.

   `navigation:auto` gilt für alle Navigationen der Seite; die Voreinstellung
   darunter nimmt beiden Schnappschüssen ihre Animation, ein normaler
   Seitenwechsel bleibt also ein harter Schnitt wie bisher. Nur die
   Overlay-Fälle bekommen eine Bewegung:
   • AUF  — erkannt am `col/nav-close` der neuen Seite: die alte Seite bleibt
            liegen, die neue wird von links nach rechts freigelegt.
   • ZU   — die neue Seite (Collection) weiß nicht, woher man kommt. Deshalb
            markiert module/col/overlay.js den Wechsel beim Verlassen der
            Overlay-Seite per `pageswap` mit dem Typ `poly-ov-close`; hier wird
            darauf gematcht. Die alte Seite (das Overlay) liegt dann OBEN und
            wischt rechts→links weg — derselbe Weg rückwärts.

   Bei „Bewegung reduzieren" wird das Ganze abgeschaltet, nicht nur die Dauer
   genullt: dann bleibt der harte Schnitt.
   ============================================================ */
@view-transition{ navigation:auto; }
@media (prefers-reduced-motion:reduce){ @view-transition{ navigation:none; } }

/* `mix-blend-mode:normal` ist WICHTIG und kein Aufräumen: die UA-Vorgaben für
   ::view-transition-old/new setzen `plus-lighter`, damit sich die beiden
   Schnappschüsse in der Standard-Überblendung sauber addieren. Mit eigenen
   Animationen liegen aber beide Bilder deckend übereinander — dann addiert
   plus-lighter zwei Flächen und die Farbe verschiebt sich sichtbar (die
   Overlay-Fläche bekommt einen anderen Ton, je nachdem was darunter liegt).
   Deckende Schnappschüsse gehören normal übereinander. */
::view-transition-old(root), ::view-transition-new(root){
  animation:none; mix-blend-mode:normal;
}

html:has(.col-nav-close)::view-transition-new(root){
  animation:poly-ov-in var(--ov-dur) var(--ov-ease) both;
}
html:active-view-transition-type(poly-ov-close)::view-transition-old(root){
  animation:poly-ov-out var(--ov-dur-out) var(--ov-ease) both;
  z-index:2;                              /* Overlay liegt beim Schließen oben */
}
html:active-view-transition-type(poly-ov-close)::view-transition-new(root){
  animation:none; z-index:1;
}
@keyframes poly-ov-in {
  from{ clip-path:inset(0 100% 0 0) } to{ clip-path:inset(0 0 0 0) }
}
@keyframes poly-ov-out{
  from{ clip-path:inset(0 0 0 0) } to{ clip-path:inset(0 100% 0 0) }
}

/* ============================================================
   Overlay-Wipe, Weg 2 (Rückfallebene) — Aufbau links→rechts, Abbau
   rechts→links, im Dokument selbst.

   Greift nur in Browsern OHNE Cross-Document View Transitions: das
   Boot-Skript (build.OV_BOOT) setzt `poly-ov-js` genau dann. Dort bleibt der
   kurze leere Zwischenzustand technisch bestehen — mehr ist ohne
   Seitenschnappschuss nicht möglich.

   Overlay-Seiten sind erkennbar daran,
   dass sie col/nav-close tragen — deshalb `body:has(.col-nav-close)`. Ohne
   :has()-Unterstützung greift die Regel nicht: dann erscheint die Seite wie
   bisher ohne Bewegung. Genauso ohne JS oder bei „Bewegung reduzieren" — die
   Gate-Klasse `poly-ov-js` setzt das Boot-Skript im <head> nur, wenn beides
   passt. Ohne diese Gate-Kopplung wäre der Startzustand (zugeklappt) das
   Letzte, was man sieht.

   Bewegt wird eine clip-path-Kante, nicht der Inhalt: der steht still und wird
   freigelegt. Zu ist exakt derselbe Weg rückwärts — die Kante läuft von rechts
   zurück nach links.

   Nach dem Aufbau wird die Maske GANZ entfernt (`done` → clip-path:none), weil
   clip-path einen Containing Block bildet und damit position:sticky der
   Kopfleiste beeinflussen könnte. Während der kurzen Bewegung ist das
   unkritisch, dauerhaft will man es nicht. Beim Schließen setzt
   module/col/overlay.js kurz auf den offenen Wert zurück (sonst ist der Übergang von
   `none` nicht interpolierbar) und dann auf `out`.
   ============================================================ */
html.poly-ov-js body:has(.col-nav-close){
  clip-path:inset(0 100% 0 0);            /* Start: noch nichts freigelegt */
  /* Die Fläche der Overlay-Seite muss MIT maskiert werden. Das Seitengerüst
     setzt den Theme-Grund auf html UND body; der von html liegt außerhalb der
     Maske und wäre sofort da — bei theme-dark liefe der Wipe unsichtbar dunkel
     auf dunkel. Also: html bekommt für die Dauer der Bewegung die neutrale
     Leinwand, body behält die Theme-Fläche und wird freigelegt.
     min-height, damit die Fläche des body auch bei kurzem Inhalt den ganzen
     Bildschirm deckt — sonst blieb darunter die Leinwand stehen. dvh wegen der
     ein- und ausfahrenden Safari-Leisten; die vh-Zeile ist der Rückfall. */
  min-height:100vh;
  min-height:100dvh;
}
html.poly-ov-js:has(.col-nav-close){ background:var(--canvas); }
/* Nach der Bewegung wieder der Theme-Grund auf html — sonst schimmert beim
   Überscrollen die Leinwand durch. */
html.poly-ov-js[data-ov-anim="done"]:has(.col-nav-close){ background:var(--surface); }
html.poly-ov-js[data-ov-anim="in"] body:has(.col-nav-close){
  clip-path:inset(0 0 0 0);
  transition:clip-path var(--ov-dur) var(--ov-ease);
}
html.poly-ov-js[data-ov-anim="done"] body:has(.col-nav-close){
  clip-path:none; transition:none;
}
html.poly-ov-js[data-ov-anim="out"] body:has(.col-nav-close){
  clip-path:inset(0 100% 0 0);
  transition:clip-path var(--ov-dur-out) var(--ov-ease);
}

/* ============================================================
   col/projekt-overlay — Layout fürs Video-Overlay: Player oben,
   darunter Headline, Kurzbeschreibung, Detailtext, Credits
   (Rolle + Namen darunter) und Tags. Auf der Overlay-Seite füllt
   module/col/overlay.js die data-ov-Hooks aus der angeklickten Card;
   rendert aber auch statisch (Werkbank/Vorschau).
   ============================================================ */
.col-project-overlay{ box-sizing:border-box; width:100%; display:flex; flex-direction:column; }
.col-project-overlay > * + *{ margin-top:calc(-1 * var(--linewidth)); }   /* Player|Info-Naht */
.col-project-overlay__info{
  box-sizing:border-box; width:100%;
  border:var(--linewidth) solid var(--poly-line);
  padding:var(--pad-big);
  display:flex; flex-direction:column; gap:var(--pad-medium);
}
.col-project-overlay__headline{
  margin:0; font-family:var(--font-sans); font-weight:700;
  font-size:var(--h1-size); line-height:var(--h1-lh); letter-spacing:var(--h1-ls); color:var(--ink);
}
.col-project-overlay__kurz{
  margin:0; font-family:var(--font-sans); font-weight:600;
  font-size:var(--body-bold-size); line-height:var(--body-bold-lh); color:var(--ink);
}
.col-project-overlay__detail{
  font-family:var(--font-sans); font-size:var(--body-size); line-height:var(--body-lh);
  color:var(--ink); white-space:pre-line;   /* Absätze aus dem Text erhalten */
}
.col-project-overlay__credit{
  display:flex; flex-direction:column;
  font-family:var(--font-sans); font-size:var(--body-size); line-height:var(--body-lh); color:var(--ink);
}
.col-project-overlay__role{ font-weight:600; }
.col-project-overlay__role::after{ content:":"; }
.col-project-overlay__name{ display:block; }
.col-project-overlay__tags{ margin-top:var(--pad-small); }
.col-project-overlay__tags:empty, .col-project-overlay__credits:empty{ display:none; }

/* ============================================================
   col/projekt-info — Textelement fürs Video-Overlay (Player ist
   separat, col/video-player). Übernimmt die Portfolio-Texte:
   H2 = Titel (headline), H1 = Kurzbeschreibung (body), Trennstrich
   über volle Breite, darunter zweispaltig [Detailtext | Credits],
   ganz unten die Tags. module/col/overlay.js füllt die data-ov-Hooks aus
   dem per ?p=<id> gewählten Projekt; rendert aber auch statisch.
   ============================================================ */
.col-projekt-info{
  box-sizing:border-box; width:100%;
  border:var(--linewidth) solid var(--poly-line);
  padding:var(--pad-big);
  display:flex; flex-direction:column; align-items:flex-start;
  gap:var(--pad-medium);
}
.col-projekt-info__titel{
  width:100%; margin:0; font-family:var(--font-sans); font-weight:700;
  font-size:var(--h2-size); line-height:var(--h2-lh); letter-spacing:var(--h2-ls); color:var(--ink);
}
/* Subline eine Stufe unter dem Titel: H3 statt H1. Der Titel (H2) bleibt die
   lauteste Stimme des Blocks — vorher war die Subline größer als er. */
.col-projekt-info__kurz{
  width:100%; margin:0; font-family:var(--font-sans); font-weight:700;
  font-size:var(--h3-size); line-height:var(--h3-lh); letter-spacing:var(--h3-ls); color:var(--ink);
}
.col-projekt-info__divider{
  width:100%; height:0;
  border-top:var(--linewidth) solid var(--ink);   /* Trennstrich = theme-abhängig (--ink) */
}
.col-projekt-info__cols{
  width:100%; display:grid; grid-template-columns:1fr 1fr;
  column-gap:var(--pad-big); row-gap:var(--pad-medium);
}
@media (max-width:640px){ .col-projekt-info__cols{ grid-template-columns:1fr; } }
/* Schalter „Einspaltig" am Modul: Detailtext und Credits laufen untereinander
   statt nebeneinander. Schmal ist der Block ohnehin einspaltig — die Option
   wirkt also auf Desktop und Tablet. */
.col-projekt-info--einspaltig .col-projekt-info__cols{ grid-template-columns:1fr; }
.col-projekt-info__detail{
  font-family:var(--font-sans); font-size:var(--body-size); line-height:var(--body-lh);
  color:var(--ink); white-space:pre-line;   /* Absätze aus dem Text erhalten */
}
.col-projekt-info__credits{ display:flex; flex-direction:column; gap:var(--pad-small); }
.col-projekt-info__credit{
  display:flex; flex-direction:column;
  font-family:var(--font-sans); font-size:var(--body-size); line-height:var(--body-lh); color:var(--ink);
}
.col-projekt-info__role{ font-weight:600; }
.col-projekt-info__role::after{ content:":"; }
.col-projekt-info__name{ display:block; }
.col-projekt-info__tags{ width:100%; margin-top:var(--pad-small); }
.col-projekt-info__titel:empty, .col-projekt-info__kurz:empty,
.col-projekt-info__tags:empty, .col-projekt-info__credits:empty{ display:none; }
