/*
 * Kampagnenfigur — portable Grundgestaltung.
 *
 * Reines CSS, kein Build-Schritt, keine Projektabhaengigkeit.
 *
 * ZWEI BETRIEBSARTEN
 *
 *   Vorgabe    Das Element steht IM TEXTFLUSS, an der Stelle, an der es in
 *              der Spalte eingesetzt wurde — ein ganz normales
 *              Inhaltselement: Figur links, Text und Schaltflaeche daneben,
 *              alles sofort sichtbar.
 *
 *   overlay    Das Element loest sich aus dem Fluss und sitzt fest am
 *              unteren Bildschirmrand. Ab 768px als Figur in einer Ecke mit
 *              Sprechblase auf Klick, darunter als Leiste ueber die volle
 *              Breite.
 *
 * Die Betriebsart waehlt die REDAKTION JE ELEMENT im Feld "Darstellung".
 * Der Server kennt sie damit schon beim Rendern und gibt die Klasse
 * `is-overlay` gleich im Markup mit — entscheidend, denn solange sie erst
 * vom Skript kam, stand eine schwebende Figur bis dahin sichtbar und in
 * voller Hoehe im Fluss und blendete dann weg.
 *
 * JE ELEMENT, weil eine Seite beides tragen kann: eine Figur im
 * Fusszeilen-Slide als Overlay und eine zweite im Textfluss. Jedes Element
 * traegt dafuer eine eigene ID und eine eigene Klasse.
 *
 * WO die schwebende Figur sitzt — Abstand zum Rand, Groesse, ab wieviel
 * Scrollen sie erscheint — bleibt Sache des PROJEKTS und kommt weiterhin
 * ueber CSS-Variablen. Das kennt nur das Projekt: was am unteren Rand sonst
 * liegt, weiss die Extension nicht.
 *
 * FARBEN UND MASSE STEHEN AUSSCHLIESSLICH ALS `var(--x, fallback)` IM
 * GEBRAUCH — sie werden hier NIRGENDS definiert. Diese Datei wird ueber
 * `f:asset.css` geladen und kann NACH dem Projekt-Stylesheet landen; eine
 * Definition wuerde die Projektwerte dann ueberschreiben statt von ihnen
 * ueberschrieben zu werden. Vollstaendige Liste in der README.md.
 */

/* ========================================== Vorgabe: im Textfluss ======= */

.cb-campaign-figure {
  display: flex;
  /*
   * IM FLUSS stehen Figur und Text mit der OBERKANTE auf einer Linie. Mittig
   * zentriert haengt die Figur bei laengerem Text in der Mitte des Absatzes
   * und verliert den Bezug zu dessen Anfang.
   *
   * Die beiden schwebenden Darstellungen setzen das selbst anders: am
   * Bildschirmrand `flex-end`, in der schmalen Leiste `center`.
   */
  align-items: flex-start;
  flex-wrap: wrap;
  gap: var(--cb-campaign-figure-gap, 1rem);
  /* Eigener vertikaler Rhythmus: der Block bringt kein Projektlayout mit und
     saesse sonst ohne jeden Abstand zwischen den Nachbarelementen. */
  margin-block: var(--cb-campaign-figure-flow-spacing, 2.5rem);
}

.cb-campaign-figure-trigger {
  padding: 0;
  margin: 0;
  border: 0;
  background: none;
  cursor: pointer;
  line-height: 0;
  flex: none;
}

.cb-campaign-figure-visual {
  display: block;
  width: var(--cb-campaign-figure-flow-size, 110px);
}

.cb-campaign-figure-visual svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/*
 * Im Fluss ist die Sprechblase keine Blase, sondern der Inhalt neben der
 * Figur — es gibt nichts aufzuklappen, der Text steht ohnehin da.
 */
.cb-campaign-figure-panel {
  display: flex;
  /*
   * Text UEBER der Schaltflaeche. Nebeneinander sieht bei kurzem Text
   * ordentlich aus, bei laengerem entsteht eine weite Luecke zwischen beiden
   * — untereinander bleibt der Bezug erhalten, unabhaengig von der
   * Textlaenge. Nur in der schmalen Leiste stehen sie nebeneinander, dort
   * ist die Hoehe knapp.
   */
  flex-direction: column;
  align-items: flex-start;
  flex: 0 1 auto;
  min-width: 0;
  gap: var(--cb-campaign-figure-panel-gap, .75rem);
  position: relative;
}

.cb-campaign-figure-text {
  margin: 0;
  font-size: var(--cb-campaign-figure-text-size, 1rem);
  line-height: 1.45;
  /*
   * NICHT wachsen lassen. Mit `flex: 1 1 …` dehnt sich der Text ueber die
   * ganze Zeile und schiebt die Schaltflaeche an den aeussersten Rand — sie
   * steht dann ohne sichtbaren Bezug zum Text. Stattdessen waechst der Text
   * nur bis zu einer lesbaren Zeilenlaenge und die Schaltflaeche folgt
   * unmittelbar.
   */
  flex: 0 1 auto;
  max-width: var(--cb-campaign-figure-text-width, 46ch);
}

/* Der Kurztext gehoert nur in die Leiste. */
.cb-campaign-figure-text-bar {
  display: none;
}

/*
 * DIE SCHALTFLAECHE TRAEGT IM TEMPLATE `btn btn-primary`.
 *
 * Damit erbt sie in jedem Bootstrap-Projekt die dortige Buttongestaltung —
 * Farbe, Innenabstand, Radius, Hover, Fokus — ohne dass irgendetwas erneut
 * definiert werden muesste. Das Projekt pflegt seine Buttons an einer
 * Stelle, und diese hier gehoert einfach dazu.
 *
 * DIE EIGENE OPTIK STEHT DESHALB IN `:where()` UND HAT NULL SPEZIFITAET.
 * Jede Projektregel gewinnt gegen sie, auch eine sehr allgemeine, und auch
 * wenn dieses Stylesheet spaeter geladen wird — bei gleicher Spezifitaet
 * entschiede sonst die Ladereihenfolge, und die Extension wuerde dem Projekt
 * seine Buttons ueberschreiben.
 *
 * Es ist dasselbe Prinzip wie beim Rueckfallwert in `var(--x, wert)`, nur
 * fuer Spezifitaet statt fuer Werte: greifen soll es nur dort, wo sonst
 * nichts steht — etwa in einer Installation ohne Bootstrap.
 */
:where(.cb-campaign-figure-cta) {
  background: var(--cb-campaign-figure-cta-bg, #e60005);
  color: var(--cb-campaign-figure-cta-color, #fff);
  border-radius: var(--cb-campaign-figure-cta-radius, 6px);
  padding: var(--cb-campaign-figure-cta-padding, .6rem 1.1rem);
  text-decoration: none;
  font-weight: 600;
}

/*
 * MINDESTGROESSE FUER BERUEHRUNG — und die Zentrierung, die dazugehoert.
 *
 * Diese Regel steht BEWUSST NICHT in `:where()`. Die Optik oben soll jede
 * Projektregel schlagen koennen; die Beruehrflaeche ist dagegen eine
 * Zusicherung der Extension und darf nicht halb verloren gehen.
 *
 * Genau das ist am 05.10.2026 passiert und war im Frontend zu sehen: aus
 * `:where()` setzte sich `min-height` durch (niemand sonst setzt es), die
 * danebenstehende Zentrierung aber nicht — gegen `.btn { display: inline-block }`
 * des Projekts hat Null Spezifitaet keine Chance. Ohne Flex-Zentrierung faellt
 * die ueberschuessige Hoehe vollstaendig UNTER die Zeile: gemessen 7,6px ueber
 * der Beschriftung, 17,4px darunter. Hoehe und Zentrierung gehoeren deshalb in
 * dieselbe Regel.
 */
.cb-campaign-figure-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
}

:where(.cb-campaign-figure-cta:hover, .cb-campaign-figure-cta:focus) {
  background: var(--cb-campaign-figure-cta-bg-hover, #c30004);
  color: var(--cb-campaign-figure-cta-color, #fff);
}

/* Das Layoutverhalten bleibt an der eigenen Klasse — es ist Sache dieses
   Bausteins, nicht des Buttonsystems. */
.cb-campaign-figure-cta {
  flex: none;
}

/* Im Fluss gibt es nichts wegzuklicken — das Element steht in der Seite. */
.cb-campaign-figure-close {
  display: none;
}

/*
 * Fokus niemals entfernen. Der Rahmen sitzt bewusst aussen, damit er auf der
 * Figur selbst nicht verschluckt wird.
 */
.cb-campaign-figure-trigger:focus-visible,
.cb-campaign-figure-cta:focus-visible,
.cb-campaign-figure-close:focus-visible {
  outline: 3px solid var(--cb-campaign-figure-focus, #16324f);
  outline-offset: 3px;
}

/* Beschriftungen, die nur Hilfsmittel brauchen. Bewusst nicht Bootstraps
   `.visually-hidden` — der Block soll ohne Bootstrap funktionieren. */
.cb-campaign-figure-trigger-label,
.cb-campaign-figure-close-label {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ================= overlay: fest am unteren Bildschirmrand ============== */


.cb-campaign-figure.is-overlay {
  /*
   * Die Betriebsart steht im Markup, weil der Server sie aus dem Feld
   * "Darstellung" kennt. Hier wird sie fuer das Skript lesbar gemacht: es
   * fragt `--cb-campaign-figure-mode` ab und muss dafuer weder eine Klasse
   * noch ein Attribut kennen. EINE Quelle - das Feld -, zwei Verbraucher.
   */
  --cb-campaign-figure-mode: overlay;

  position: fixed;
  margin: 0;
  z-index: var(--cb-campaign-figure-z, 1030);
  align-items: flex-end;
  flex-wrap: nowrap;
  gap: var(--cb-campaign-figure-overlay-gap, .75rem);

  /*
   * Das Element landet im Seitenlayout haeufig in einer Grid-Zeile, und
   * Bootstrap setzt dort `.row > * { width: 100% }`. Das gilt auch fuer ein
   * `position: fixed`-Element: der Container waere bildschirmbreit und wuerde
   * als unsichtbare Flaeche Klicks auf alles darunter abfangen.
   */
  width: max-content;
  max-width: 100%;

  /* Zweite Absicherung, unabhaengig von der Breite. */
  pointer-events: none;

  /*
   * ERSCHEINEN NACH DEM SCROLLEN. Wieviel gescrollt sein muss, sagt das
   * Projekt ueber `--cb-campaign-figure-reveal-at` (z. B. `10%`); ohne
   * Angabe erscheint die Figur sofort. Auf Seiten, die zu kurz zum Scrollen
   * sind, ebenfalls sofort — sonst waere sie dort nie zu sehen.
   *
   * KEIN Uebergang auf `bottom`: das Ausweichen vor der Fusszeile wird je
   * Bild neu gerechnet, und waehrend einer Animation liefert
   * getBoundingClientRect() die Zwischenposition statt der Zielposition —
   * der selbstkorrigierende Schub zaehlt sich dann auf, bis die Figur oben
   * aus dem Bild laeuft.
   */
  opacity: 0;
  transform: translateY(14px);
  /*
   * `visibility` mit DERSELBEN Dauer wie die Deckkraft, nicht kuerzer.
   * CSS schaltet sie als Stufe erst am Ende der Dauer - mit .3s gegen .45s
   * war die Figur mitten im Ausblenden auf einen Schlag weg (gemessen
   * Deckkraft 0,21 im Moment des Umschaltens).
   */
  transition: opacity .45s ease-out, transform .45s ease-out, visibility .45s ease-out;
}

.cb-campaign-figure.is-overlay.is-revealed {
  opacity: 1;
  transform: none;
}

/* Bedienbar erst, wenn sie auch sichtbar ist — eine unsichtbare Figur darf
   weder anklickbar noch im Tabulator-Lauf sein. */
.cb-campaign-figure.is-overlay.is-revealed .cb-campaign-figure-trigger,
.cb-campaign-figure.is-overlay.is-revealed .cb-campaign-figure-panel {
  pointer-events: auto;
}

/* Vom Besucher weggeklickt. `visibility` gehoert dazu — ohne sie bliebe ein
   unsichtbares Element anklickbar und im Tabulator-Lauf. */
.cb-campaign-figure.is-overlay.is-hidden,
.cb-campaign-figure.is-overlay.is-dismissed {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

.cb-campaign-figure.is-overlay .cb-campaign-figure-close {
  display: block;
  position: absolute;
  width: 44px;
  height: 44px;
  border: 0;
  background: none;
  cursor: pointer;
  color: inherit;
}

.cb-campaign-figure.is-overlay .cb-campaign-figure-close::before,
.cb-campaign-figure.is-overlay .cb-campaign-figure-close::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 2px;
  background: currentColor;
}

.cb-campaign-figure.is-overlay .cb-campaign-figure-close::before { transform: translate(-50%, -50%) rotate(45deg); }
.cb-campaign-figure.is-overlay .cb-campaign-figure-close::after  { transform: translate(-50%, -50%) rotate(-45deg); }

/* ------------------------------------- overlay ab 768px: Figur in der Ecke */

@media (min-width: 768px) {
  .cb-campaign-figure.is-overlay {
    /*
     * VORGABE 0: die Figur steht auf der Unterkante des Bildschirms. Bewusst
     * neutral — wo genau sie sitzt, haengt von den anderen fest
     * positionierten Elementen des Projekts ab.
     *
     * Kein Schub mehr vom Skript: vor der Fusszeile wird nicht ausgewichen,
     * sondern hinausgefahren.
     *
     * `--…-trim` ebenfalls vom Skript: der LEERE STREIFEN am unteren Rand
     * der Zeichnung. Die viewBox ist bei allen Figuren 197 Einheiten hoch,
     * die Zeichnung endet aber frueher - beim Rentier bei 195, beim Hasen
     * bei 195, beim Weihnachtsmann bei 190, bei der Sonne bei 180. Dieser
     * Rest ist leeres Bild und sah als Luft unter der Figur aus: beim
     * Rentier 2 von 197 Einheiten, bei 260 px Figurgroesse also 2,6 px.
     * Genau so gemeldet am 05.10.2026 ("circa 3 Pixel").
     *
     * Abgezogen statt in die Zeichnungen hineingerechnet: ein getrimmter
     * viewBox je Figur waere sechsmal von Hand zu messen und bei der
     * siebten Figur wieder vergessen. Das Skript misst ihn aus der
     * Zeichnung selbst, einmal und bei jeder Groessenaenderung.
     */
    bottom: calc(
      var(--cb-campaign-figure-offset-bottom, 0px)
      - var(--cb-campaign-figure-trim, 0px)
    );
    max-width: calc(100vw - 2 * var(--cb-campaign-figure-offset-side, 0px));
  }

  .cb-campaign-figure.is-overlay.cb-campaign-figure-corner-left {
    left: var(--cb-campaign-figure-offset-side, 0px);
    flex-direction: row;
  }

  .cb-campaign-figure.is-overlay.cb-campaign-figure-corner-right {
    right: var(--cb-campaign-figure-offset-side, 0px);
    flex-direction: row-reverse;
  }

  /*
   * DIE LUECKE ZWISCHEN FIGUR UND BLASE GEHOERT ZUM ZIEL.
   *
   * Die Grundlage setzt `pointer-events: none` an der ganzen Figur, damit
   * ihr Kasten keine Klicks auf die Seite darunter abfaengt. Am Desktop
   * sitzt zwischen Figur und Sprechblase aber ein Abstand, und der gehoert
   * zu eben diesem Kasten. Blieb er durchlaessig, galt der Zeiger dort als
   * "ausserhalb der Figur": wer langsam von der Figur zur Blase fuhr,
   * verlor sie auf halbem Weg und kam weder an die Schaltflaeche noch an
   * das Kreuz. Am 05.10.2026 gemeldet.
   *
   * Ungefaehrlich, weil `width: max-content` den Kasten genau auf Figur,
   * Abstand und Blase begrenzt - er ist nicht bildschirmbreit. Und nur im
   * eingeblendeten Zustand: vorher soll die unsichtbare Figur nichts
   * abfangen. Der Mobil-Block macht es seit jeher genauso.
   *
   * WCAG 2.1, 1.4.13 verlangt genau das unter "hoverable": was bei Hover
   * erscheint, muss mit dem Zeiger erreichbar bleiben.
   */
  .cb-campaign-figure.is-overlay.is-revealed {
    pointer-events: auto;
  }

  /*
   * EIN WEG, ZWEI RICHTUNGEN: die Aussenposition.
   *
   * Unterhalb der Bildschirmkante steht die Figur zweimal - bevor sie zum
   * ersten Mal erscheint, und nachdem sie weggeklickt wurde. Beide Male
   * dieselbe Stelle, deshalb dieselbe Rechnung.
   *
   * 100 % ist die eigene Hoehe der Grafik. Sie sitzt mit ihrer gezeichneten
   * Unterkante auf der Bildschirmkante; eine ganze Hoehe tiefer ist sie
   * vollstaendig heraus. Der Randabstand des Projekts kommt dazu, falls es
   * die Figur hoeher setzt - sonst bliebe genau dieser Rest im Bild stehen.
   *
   * VERSCHOBEN WIRD DIE GRAFIK, nicht der ganze Kasten: `transform` tastet
   * die Groesse des Kastens nicht an, Position und Groesse der Sprechblase
   * bleiben also unberuehrt.
   */
  .cb-campaign-figure.is-overlay .cb-campaign-figure-visual {
    width: var(--cb-campaign-figure-size, 130px);
    transform: translateY(calc(100% + var(--cb-campaign-figure-offset-bottom, 0px)));
    /* Gleiche Dauer wie Deckkraft und Sichtbarkeit der Figur: Hereinfahren
       und Erscheinen sind EINE Bewegung, nicht zwei. */
    transition: transform .45s ease;
  }

  /*
   * HEREINFAHREN STATT ERSCHEINEN, Kundenwunsch vom 05.10.2026.
   *
   * Die Figur blendet nicht an Ort und Stelle ein, sondern faehrt beim
   * Ueberschreiten der Schwelle von unten aus dem nicht sichtbaren Bereich
   * herauf - und blendet dabei ein.
   *
   * Der kurze Weg von 14 px, den die Grundregel weiter oben vorgibt, bleibt
   * fuer die LEISTE stehen: sie laeuft ueber die volle Breite und sollte
   * nicht ueber den halben Bildschirm fahren. Deshalb wird der Kasten hier
   * ruhiggestellt und der Weg allein von der Grafik gegangen; zwei
   * gleichzeitige Verschiebungen auf verschiedenen Elementen waeren sonst
   * wieder zwei Bezugspunkte fuer dieselbe Bewegung.
   */
  .cb-campaign-figure.is-overlay,
  .cb-campaign-figure.is-overlay.is-revealed {
    transform: none;
  }

  .cb-campaign-figure.is-overlay.is-revealed .cb-campaign-figure-visual {
    transform: none;
  }

  /*
   * WIEDER HINAUS - zurueck auf die Aussenposition.
   *
   * Zwei Anlaesse, derselbe Weg rueckwaerts:
   *   `is-dismissed`  der Besucher hat das Kreuz geklickt
   *   `is-hidden`     die Fusszeile hat die Figur erreicht (s. Skript)
   *
   * Die Figur faehrt um ihre volle Hoehe nach unten und wird dabei
   * durchsichtig; `visibility: hidden` an der Figur (weiter oben) nimmt sie
   * auch aus dem Tabulator-Lauf.
   *
   * Steht NACH der Erscheinen-Regel, weil beide dieselbe Spezifitaet haben
   * - weggeklickt muss gewinnen, auch wenn die Figur eingeblendet ist.
   *
   * Zwischenstufen ("nur die Ohren schauen heraus") gibt es nicht mehr.
   * Die Vorgabe lautet: gesamt aus dem Bild, gesamt nicht mehr sichtbar.
   */
  .cb-campaign-figure.is-overlay.is-dismissed .cb-campaign-figure-visual,
  .cb-campaign-figure.is-overlay.is-hidden .cb-campaign-figure-visual {
    transform: translateY(calc(100% + var(--cb-campaign-figure-offset-bottom, 0px)));
  }

  /* Hier IST die Sprechblase eine Blase — und zunaechst zu. */
  .cb-campaign-figure.is-overlay .cb-campaign-figure-panel {
    display: none;
    flex: none;
    /*
     * ETWAS HOEHER ALS DIE FIGUR. Die Blase sitzt sonst mit ihrer
     * Unterkante genau auf der Bildschirmkante, weil `align-items:
     * flex-end` sie mit der Figur ausrichtet - sie klebt dann unten.
     * Kundenwunsch vom 05.10.2026: zuerst "mindestens 1rem", dann 4rem.
     *
     * Der Zipfel wandert mit, er sitzt relativ zur Blase. Er zeigt damit
     * 1rem hoeher auf die Figur, was bei 260 px Figurhoehe weiterhin im
     * Kopfbereich liegt.
     *
     * `--…-trim` kommt dazu, weil der Kasten um genau diesen Betrag unter
     * die Bildschirmkante reicht (der leere Streifen unter der Zeichnung,
     * s. oben). Ohne ihn waere der sichtbare Abstand um ihn zu klein -
     * gemessen 14 statt 17 px. Der Angabe soll man ansehen koennen, was
     * man bekommt: 1rem ueber der Kante heisst 1rem ueber der Kante.
     */
    margin-bottom: calc(
      var(--cb-campaign-figure-panel-lift, 4rem)
      + var(--cb-campaign-figure-trim, 0px)
    );
    background: var(--cb-campaign-figure-panel-bg, #fff);
    border: 1px solid var(--cb-campaign-figure-panel-border, #dcd6cc);
    border-radius: var(--cb-campaign-figure-panel-radius, 10px);
    box-shadow: var(--cb-campaign-figure-panel-shadow, 0 6px 24px rgba(0, 0, 0, .18));
    /*
     * INNENABSTAND LINKS UND RECHTS GLEICH, und zwar beide so breit wie
     * das Kreuz. Rechts haelt er dessen Flaeche frei, links sorgt derselbe
     * Wert dafuer, dass die Mitte auch wirklich die Mitte ist.
     *
     * Vorher standen hier 1rem links und 2,75rem rechts. Der Inhalt sass
     * damit sichtbar aus der Mitte - am 26.10.2026 vom Kunden als "links
     * zum Rentier mehr Abstand als rechts zum X" gemeldet. Dieselbe
     * Ueberlegung gilt in der Leiste schon laenger; hier fehlte sie.
     */
    padding: var(--cb-campaign-figure-panel-padding, 1rem 2.75rem);
    max-width: var(--cb-campaign-figure-panel-width, 22rem);
    color: var(--cb-campaign-figure-panel-color, #16324f);

    /*
     * Text und Schaltflaeche mittig, damit das Kreuz oben rechts fuer sich
     * steht und nichts daneben auf einer Kante klebt (Kundenwunsch
     * 26.10.2026). Die Grundlage stellt beide linksbuendig untereinander -
     * das passt im Fluss, wo die Blase keine Blase ist, sondern Text neben
     * der Figur.
     */
    align-items: center;
    text-align: center;
  }

  .cb-campaign-figure.is-overlay.is-open .cb-campaign-figure-panel {
    display: flex;
  }

  /*
   * DER ZIPFEL. Ein um 45 Grad gedrehtes Quadrat an der Kante, die zur Figur
   * zeigt - mit demselben Hintergrund und denselben zwei Raendern, die dort
   * sichtbar waeren. Dadurch wirkt es als Fortsetzung der Blase und nicht
   * als aufgesetztes Dreieck.
   *
   * Die Seite richtet sich nach der Ecke: links steht die Figur links neben
   * der Blase, rechts dreht `row-reverse` die Reihenfolge um.
   */
  .cb-campaign-figure.is-overlay .cb-campaign-figure-panel::after {
    content: "";
    position: absolute;
    bottom: var(--cb-campaign-figure-tail-bottom, 2.25rem);
    width: var(--cb-campaign-figure-tail, 14px);
    height: var(--cb-campaign-figure-tail, 14px);
    background: var(--cb-campaign-figure-panel-bg, #fff);
    transform: rotate(45deg);
  }

  .cb-campaign-figure.is-overlay.cb-campaign-figure-corner-left .cb-campaign-figure-panel::after {
    left: calc(-.5 * var(--cb-campaign-figure-tail, 14px) - 1px);
    border-left: 1px solid var(--cb-campaign-figure-panel-border, #dcd6cc);
    border-bottom: 1px solid var(--cb-campaign-figure-panel-border, #dcd6cc);
  }

  .cb-campaign-figure.is-overlay.cb-campaign-figure-corner-right .cb-campaign-figure-panel::after {
    right: calc(-.5 * var(--cb-campaign-figure-tail, 14px) - 1px);
    border-right: 1px solid var(--cb-campaign-figure-panel-border, #dcd6cc);
    border-top: 1px solid var(--cb-campaign-figure-panel-border, #dcd6cc);
  }

  .cb-campaign-figure.is-overlay .cb-campaign-figure-close {
    top: .35rem;
    right: .35rem;
  }
}

/* ------------------------------ overlay unter 768px: Leiste am unteren Rand */

@media (max-width: 767.98px) {
  /*
   * DIE LEISTE FAEHRT NACH UNTEN HINAUS.
   *
   * Dieselbe Bewegung wie bei der schwebenden Figur im Desktop, nur traegt
   * sie hier der Kasten selbst: die Leiste IST der Kasten, sie hat keine
   * eigene Grafik, die fuer sie gehen koennte.
   *
   * Drei Anlaesse, ein Weg - vor dem ersten Erscheinen bleibt es beim
   * kurzen Weg von 14 px der Grundregel, denn die Leiste laeuft ueber die
   * volle Breite und soll beim Erscheinen nicht ueber den halben Bildschirm
   * fahren. Beim Hinausfahren dagegen SOLL sie ganz verschwinden.
   */
  .cb-campaign-figure.is-overlay.is-hidden,
  .cb-campaign-figure.is-overlay.is-dismissed {
    transform: translateY(100%);
  }

  .cb-campaign-figure.is-overlay {
    left: 0;
    right: 0;
    width: auto;
    max-width: none;
    align-items: center;
    gap: .6rem;
    background: var(--cb-campaign-figure-bar-bg, #fff);
    border-top: 1px solid var(--cb-campaign-figure-panel-border, #dcd6cc);
    box-shadow: var(--cb-campaign-figure-bar-shadow, 0 -3px 14px rgba(0, 0, 0, .14));

    /*
     * BUENDIG MIT DER UNTERKANTE.
     *
     * Die Systemleiste auf iPhones wird als INNENABSTAND beruecksichtigt und
     * nicht als Abstand nach unten. Der Unterschied ist sichtbar: als
     * Abstand entstuende unter der Leiste ein Streifen, durch den der
     * Seiteninhalt hindurchscrollt — genau das war bis 05.10.2026 der Fall,
     * auch wegen eines zusaetzlichen Projektabstands. Als Innenabstand reicht
     * der Hintergrund bis an den Rand und der Text bleibt trotzdem ueber der
     * Systemleiste.
     *
     * `--cb-campaign-figure-bar-bottom` bleibt als Stellschraube erhalten,
     * steht aber auf 0: ein Projekt, das die Leiste doch anheben will, kann
     * es weiterhin tun.
     */
    padding: .5rem .75rem calc(.5rem + env(safe-area-inset-bottom, 0px));
    bottom: var(--cb-campaign-figure-bar-bottom, 0px);
  }

  .cb-campaign-figure.is-overlay.is-revealed {
    pointer-events: auto;
  }

  .cb-campaign-figure.is-overlay .cb-campaign-figure-visual {
    width: var(--cb-campaign-figure-bar-size, 72px);
  }

  .cb-campaign-figure.is-overlay .cb-campaign-figure-panel {
    /*
     * Text oben, Schaltflaeche darunter, beides mittig — und zwar mittig
     * zum verbleibenden Platz neben der Figur, nicht nur linksbuendig
     * untereinander.
     *
     * MITTIG ZWISCHEN FIGUR UND KREUZ, nicht mittig im eigenen Kasten.
     * Das ist nicht dasselbe, und der Unterschied war sichtbar: Der Kasten
     * reicht rechts UNTER das Kreuz (sein rechter Innenabstand haelt dessen
     * Flaeche frei), seine Mitte liegt damit rechts von der Mitte zwischen
     * Figur und Kreuz. Mit beidseitig gleichem Innenabstand sass der Inhalt
     * deshalb 22 px zu weit rechts - gemessen bei 390 px Breite 54 px
     * Abstand zur Figur und nur 10 px zum Kreuz. Am 26.10.2026 vom Kunden
     * gemeldet ("Text und Button zu Figur und X mittig platzieren").
     *
     * Links braucht es deshalb KEINEN Innenabstand: Den Abstand zur Figur
     * stellt die Leiste selbst her (ihr `gap`). Rechts bleibt er, er haelt
     * das Kreuz frei. Damit liegen beide Abstaende gleich.
     *
     * `position: static` hebt die Verankerung aus der Grundlage auf, damit
     * das Kreuz an der LEISTE haengt und nicht an diesem Kasten — nur so
     * sitzt es in deren oberer rechter Ecke und nicht an der Oberkante
     * eines Kastens, der neben der grossen Figur mittig steht.
     */
    position: static;
    flex-direction: column;
    align-items: center;
    text-align: center;
    flex: 1 1 auto;
    padding-inline: 0 var(--cb-campaign-figure-bar-inset, 2.75rem);
    gap: var(--cb-campaign-figure-bar-gap, .5rem);
  }

  .cb-campaign-figure.is-overlay .cb-campaign-figure-text-bubble { display: none; }

  .cb-campaign-figure.is-overlay .cb-campaign-figure-text-bar {
    display: block;
    flex: 0 1 auto;
    max-width: none;
    font-size: var(--cb-campaign-figure-bar-text-size, .9rem);
    line-height: 1.35;
  }

  /*
   * Kleine Schaltflaeche — und dieselbe Schriftgroesse wie der Text
   * daneben, aus EINER Variablen. Zwei getrennte Werte waeren frueher oder
   * spaeter auseinandergelaufen.
   *
   * GESETZT WERDEN BOOTSTRAPS EIGENE GROESSENVARIABLEN, nicht eigene
   * Deklarationen. Der Grund ist gemessen, nicht vermutet (05.10.2026): die
   * eigene Optik dieser Schaltflaeche steht bewusst in `:where()` mit Null
   * Spezifitaet, damit die Buttonregeln des Projekts gewinnen. Sie gewinnen
   * dann aber auch gegen eine Groessenangabe von hier — gemessen 16px
   * Schrift und 62px Hoehe statt der gewuenschten kleinen Schaltflaeche.
   * Ueber die Variablen greift dieselbe Mechanik, mit der Bootstrap selbst
   * `.btn-sm` baut; das Projekt kann sie weiterhin ueberschreiben.
   *
   * `margin: 0` steht hier, weil Projektregeln der Schaltflaeche einen
   * Aussenabstand mitgeben koennen — der verschoebe sie aus der Mitte.
   */
  .cb-campaign-figure.is-overlay .cb-campaign-figure-cta {
    --bs-btn-font-size: var(--cb-campaign-figure-bar-text-size, .9rem);
    --bs-btn-padding-y: .35rem;
    --bs-btn-padding-x: .75rem;
    margin: 0;
  }

  /* Obere rechte Ecke der Leiste. Verankert an der Leiste, siehe oben. */
  .cb-campaign-figure.is-overlay .cb-campaign-figure-close {
    top: .15rem;
    right: .15rem;
    transform: none;
  }
}

/* ------------------------------------------------------------------ Zeigerverfolgung */

/*
 * Das Skript schreibt je Figur nur zwei Zahlen (-1..1) und einen Winkel.
 * Die gesamte Bewegung macht CSS — kein Layout wird angefasst.
 *
 * `transform-box: fill-box` steht GEZIELT nur auf den Elementen, die per CSS
 * bewegt werden. Pauschal auf alle Kinder gesetzt verschiebt dieselbe Angabe
 * den Bezugspunkt von SVG-`transform`-Attributen mit eigenem Drehpunkt — die
 * Ohren des Hasen wandern dann aus dem Kopf.
 */
.cb-campaign-figure-lean,
.cb-campaign-figure-lean-more,
.cb-campaign-figure-box,
.cb-campaign-figure-lid,
.cb-campaign-figure-bow,
.cb-campaign-figure-box-shadow,
.cb-campaign-figure-nose,
.cb-campaign-figure-nose-rotate,
.cb-campaign-figure-glow-sun {
  transform-box: fill-box;
}

.cb-campaign-figure-eye {
  transform: translate(calc(var(--cb-campaign-figure-look-x, 0) * 3.4px),
                       calc(var(--cb-campaign-figure-look-y, 0) * 2.6px));
  transition: transform .12s ease-out;
}

/*
 * DER GLANZPUNKT ZIEHT NACH.
 *
 * Er sitzt in derselben Gruppe wie die Pupille und wuerde sich sonst exakt
 * mit ihr bewegen. Physikalisch ist er aber eine Spiegelung auf der
 * Augenoberflaeche: er gehoert zum Lichteinfall, nicht zur Blickrichtung,
 * und bleibt deshalb ein Stueck zurueck. Genau das gibt dem Auge Tiefe.
 *
 * Umgesetzt als GEGENBEWEGUNG: die Gruppe verschiebt beides um den vollen
 * Weg, der Glanzpunkt nimmt davon 45 % wieder zurueck und legt netto 55 %
 * zurueck. So bleibt er immer im Auge, ohne dass die Werte an zwei Stellen
 * gepflegt werden muessten — aendert sich der Ausschlag oben, zieht er
 * anteilig mit.
 *
 *   3.4px * 0.45 = 1.53px      2.6px * 0.45 = 1.17px
 */
.cb-campaign-figure-eye-glint {
  transform: translate(calc(var(--cb-campaign-figure-look-x, 0) * -1.53px),
                       calc(var(--cb-campaign-figure-look-y, 0) * -1.17px));
  transition: transform .18s ease-out;
}

/* Die Nase folgt schwaecher als die Augen — sie gehoert zum Gesichtsgeruest,
   nicht zum Blick. Das Folgen sitzt auf einer Gruppe UEBER der Nase: die Nase
   traegt beim Beruehren schon eine eigene Transformation, und zwei
   Transformationen am selben Element schliessen sich aus. */
/*
 * OHREN UND BOMMEL FOLGEN DEM ZEIGER - minimal.
 *
 * Vorgabe vom 05.10.2026: "nicht links-rechts ausschlagen, auch nicht
 * oben, sondern wirklich nur eine leichte kleine Animation". Deshalb nur
 * `look-x`, kein `look-y`, und Ausschlaege im Bereich weniger Grad.
 *
 * Gedreht wird um den ANSATZ, nicht um die Mitte - sonst wandert das ganze
 * Ohr seitlich, statt dass sich die Spitze neigt. `transform-box: view-box`
 * macht die Angaben zu Zeichenraster-Einheiten; ohne sie bezoegen sie sich
 * auf den Begrenzungskasten der Gruppe und der Ansatz saesse falsch.
 *
 * Gerechnet (Ohr 84 Einheiten lang, 2,6 Grad): die Spitze wandert rund
 * 3,8 Einheiten, bei 260 px Figurgroesse also etwa 5 px.
 */
.cb-campaign-figure-ear,
.cb-campaign-figure-bobble {
  transform-box: view-box;
  transition: transform .2s ease-out;
}

.cb-campaign-figure-ear {
  transform: rotate(calc(var(--cb-campaign-figure-look-x, 0) * 2.6deg));
}

/* Ohransatz: dort, wo die Ellipse den Kopf trifft. */
.cb-campaign-figure-ear-left  { transform-origin: 68px 88px; }
.cb-campaign-figure-ear-right { transform-origin: 132px 88px; }

/*
 * Die Bommel haengt am Muetzenende; gedreht wird um den Knick der Muetze
 * (132/50), nicht um die Bommel selbst. Abstand dorthin rund 38 Einheiten,
 * bei 3 Grad also knapp 2 Einheiten Weg - ein Wippen, kein Schwingen.
 */
.cb-campaign-figure-bobble {
  transform-origin: 132px 50px;
  transform: rotate(calc(var(--cb-campaign-figure-look-x, 0) * 3deg));
}

.cb-campaign-figure-nose-follow {
  transform: translate(calc(var(--cb-campaign-figure-look-x, 0) * 2.1px),
                       calc(var(--cb-campaign-figure-look-y, 0) * 1.7px));
  transition: transform .14s ease-out;
}

/* Drehpunkt ist die BASIS der Moehre, nicht ihre Mitte — sie steckt im Schnee
   und schwenkt um diesen Punkt. */
.cb-campaign-figure-nose-rotate {
  transform-origin: 7% 50%;
  transform: rotate(calc(var(--cb-campaign-figure-angle, 0) * 1deg));
  transition: transform .16s ease-out;
}

/*
 * SCHERUNG STATT DREHUNG.
 *
 * Rentier, Hase und Schneemann laufen unten aus dem Bild — ihre Unterkante
 * ist eine waagerechte Linie und liest sich als Standflaeche. Eine Drehung
 * kippt diese Linie zwangslaeufig mit, egal wo der Drehpunkt liegt; die
 * Figur sieht dann aus, als rutsche sie weg.
 *
 * `skewX` laesst waagerechte Linien waagerecht — es verschiebt Punkte nur
 * seitlich, und zwar proportional zu ihrem Abstand vom Bezugspunkt. Liegt
 * der an der Unterkante (100 %), bleiben deren beide Eckpunkte exakt
 * stehen, und nur der Kopf neigt sich.
 *
 * Das Vorzeichen ist negativ, weil Punkte OBERHALB des Bezugspunkts einen
 * negativen Abstand haben: erst so neigt sich die Figur zum Zeiger hin und
 * nicht von ihm weg.
 *
 * Kein `translateX` mehr — das verschob die ganze Figur samt Unterkante.
 */
.cb-campaign-figure-lean {
  transform-origin: 50% 100%;
  transform: skewX(calc(var(--cb-campaign-figure-look-x, 0) * -2.4deg));
  transition: transform .18s ease-out;
}

 /* Das Geschenk behaelt die Drehung: es ist ein Gegenstand, keine stehende
    Figur — dort ist eine gekippte Unterkante richtig. */
.cb-campaign-figure-lean-more {
  transform-origin: 50% 100%;
  transform: rotate(calc(var(--cb-campaign-figure-look-x, 0) * 4deg))
             translateX(calc(var(--cb-campaign-figure-look-x, 0) * 4px));
  transition: transform .18s ease-out;
}

/* ------------------------------------------------------------- Beruehrung der Nase */

/*
 * Das Groesserwerden traegt die Rueckmeldung, das Leuchten begleitet sie nur.
 * `drop-shadow` statt SVG-Filter: laeuft auf der Grafikkarte, braucht keine
 * filter-Definition im Dokument und faellt ohne Zeiger einfach weg.
 */
.cb-campaign-figure-nose,
.cb-campaign-figure-glow,
.cb-campaign-figure-glow-sun {
  cursor: pointer;
  transform-origin: center;
  transform: scale(1);
  filter: drop-shadow(0 0 0 rgba(230, 0, 5, 0));
  transition: filter .3s ease-out, transform .25s ease-out;
}

/*
 * Die Flaeche selbst glueht, nicht nur ihr Hof.
 *
 * `drop-shadow` allein legt einen Schein UM die Nase, laesst sie selbst aber
 * unveraendert dunkel — sie sieht dann beleuchtet aus statt leuchtend.
 * `brightness` hellt die Flaeche auf, `saturate` haelt sie dabei farbig,
 * statt sie ins Rosa kippen zu lassen. Erst beides zusammen liest sich als
 * Gluehen.
 *
 * Bewusst ueber `filter` und nicht ueber `fill`: die Nase ist je nach Figur
 * mal ein einzelner Kreis, mal eine Gruppe aus mehreren Formen — `filter`
 * wirkt in beiden Faellen gleich.
 */
@keyframes cb-campaign-figure-glow-red {
  0%, 100% { transform: scale(1.09);
             filter: brightness(1.18) saturate(1.1) drop-shadow(0 0 4px rgba(230, 0, 5, .5)); }
  50%      { transform: scale(1.16);
             filter: brightness(1.55) saturate(1.25) drop-shadow(0 0 10px rgba(230, 0, 5, .8)); }
}

@keyframes cb-campaign-figure-glow-carrot {
  0%, 100% { transform: scale(1.07);
             filter: brightness(1.15) saturate(1.1) drop-shadow(0 0 4px rgba(240, 125, 26, .55)); }
  50%      { transform: scale(1.14);
             filter: brightness(1.45) saturate(1.2) drop-shadow(0 0 10px rgba(240, 125, 26, .75)); }
}

@keyframes cb-campaign-figure-glow-gold {
  0%, 100% { transform: scale(1.07);
             filter: brightness(1.15) saturate(1.1) drop-shadow(0 0 4px rgba(245, 197, 24, .5)); }
  50%      { transform: scale(1.13);
             filter: brightness(1.45) saturate(1.2) drop-shadow(0 0 10px rgba(245, 197, 24, .72)); }
}

@keyframes cb-campaign-figure-glow-sun {
  0%, 100% { transform: scale(1.02);
             filter: brightness(1.1) saturate(1.05) drop-shadow(0 0 6px rgba(249, 180, 24, .6)); }
  50%      { transform: scale(1.05);
             filter: brightness(1.35) saturate(1.15) drop-shadow(0 0 16px rgba(249, 180, 24, .85)); }
}

/*
 * AUSGELOEST WIRD AM GANZEN ELEMENT, NICHT AN DER NASE.
 *
 * Vorher hing das Leuchten an `:hover` der Nase selbst. Die ist je nach
 * Figur 11 bis 13 Einheiten gross - man musste sie treffen, und wer die
 * Figur nur ueberstrich, sah nichts. Am 05.10.2026 so gemeldet.
 *
 * Bezug ist die FIGUR, nicht die Schaltflaeche um die Grafik. Damit zaehlt
 * auch die Sprechblase dazu und mobil die ganze Leiste - ausdruecklich so
 * gewuenscht. Ueber dem Element stehen heisst: die Nase geht an.
 *
 * Im Desktop traegt die Figur `pointer-events: none`, solange sie nicht
 * eingeblendet ist; eingeblendet ist sie durchlaessig (das war noetig,
 * damit der Weg zur Sprechblase nicht abreisst). Der Zeiger erreicht sie
 * also genau dann, wenn es etwas zu sehen gibt.
 *
 * Die Tastatur-Regel daneben bleibt an der Schaltflaeche: Fokus hat nur,
 * was fokussierbar ist.
 */
.cb-campaign-figure:hover .cb-campaign-figure-nose,
.cb-campaign-figure-trigger:focus-visible .cb-campaign-figure-nose {
  animation: cb-campaign-figure-glow-red 1.5s ease-in-out infinite;
}

/* Ein roter Schein auf oranger Flaeche sieht nach Fehler aus, nicht nach
   Absicht — die Moehre leuchtet orange. */
.cb-campaign-figure:hover .cb-campaign-figure-nose-carrot,
.cb-campaign-figure-trigger:focus-visible .cb-campaign-figure-nose-carrot {
  animation: cb-campaign-figure-glow-carrot 1.5s ease-in-out infinite;
}

.cb-campaign-figure:hover .cb-campaign-figure-glow,
.cb-campaign-figure-trigger:focus-visible .cb-campaign-figure-glow {
  animation: cb-campaign-figure-glow-gold 1.5s ease-in-out infinite;
}

/* Die Sonne hat keine Nase — sie leuchtet als Ganzes. */
.cb-campaign-figure:hover .cb-campaign-figure-glow-sun,
.cb-campaign-figure-trigger:focus-visible .cb-campaign-figure-glow-sun {
  animation: cb-campaign-figure-glow-sun 2s ease-in-out infinite;
}

/* -------------------------------------------------------------- Huepfer (Geschenk) */

/*
 * Gestaffelt statt als Block: die Schachtel setzt an, der Deckel folgt spaeter
 * und hebt weiter ab, die Schleife zuletzt. Dadurch federt es nach.
 */
.cb-campaign-figure-lid { transform: rotate(-4deg); }

@keyframes cb-campaign-figure-hop {
  0%, 62%, 100% { transform: translateY(0); }
  28%           { transform: translateY(-6px); }
}

@keyframes cb-campaign-figure-hop-lid {
  0%, 66%, 100% { transform: translateY(0) rotate(-4deg); }
  32%           { transform: translateY(-9px) rotate(-8deg); }
}

@keyframes cb-campaign-figure-hop-bow {
  0%, 70%, 100% { transform: translateY(0); }
  36%           { transform: translateY(-5px); }
}

@keyframes cb-campaign-figure-hop-shadow {
  0%, 64%, 100% { opacity: .20; }
  30%           { opacity: .34; }
}

.cb-campaign-figure-box        { animation: cb-campaign-figure-hop 1.9s ease-in-out infinite; }
.cb-campaign-figure-lid        { animation: cb-campaign-figure-hop-lid 1.9s ease-in-out infinite; }
.cb-campaign-figure-bow        { animation: cb-campaign-figure-hop-bow 1.9s ease-in-out infinite; }
.cb-campaign-figure-box-shadow { animation: cb-campaign-figure-hop-shadow 1.9s ease-in-out infinite; }

/* ------------------------------------------------------------- Bewegung reduzieren */

/*
 * Dauerbewegung und Verfolgung entfallen. Der SCHEIN an der Nase bleibt:
 * er ist die Rueckmeldung auf eine Handlung, nicht Zierde — ihn mitzunehmen
 * wuerde die Funktion entfernen statt die Bewegung.
 */
@media (prefers-reduced-motion: reduce) {
  .cb-campaign-figure-box,
  .cb-campaign-figure-lid,
  .cb-campaign-figure-bow,
  .cb-campaign-figure-box-shadow {
    animation: none;
  }

  .cb-campaign-figure-lid { transform: rotate(-4deg); }

  .cb-campaign-figure.is-overlay,
  .cb-campaign-figure.is-overlay.is-revealed { transform: none; }

  /* Das Absenken bleibt - es ist eine Position, keine Verzierung. Nur die
     Bewegung dorthin entfaellt. */
  .cb-campaign-figure.is-overlay .cb-campaign-figure-visual { transition: none; }


  .cb-campaign-figure-eye,
  .cb-campaign-figure-eye-glint,
  .cb-campaign-figure-nose-follow,
  .cb-campaign-figure-nose-rotate,
  .cb-campaign-figure-lean,
  .cb-campaign-figure-lean-more,
  .cb-campaign-figure-ear,
  .cb-campaign-figure-bobble {
    transform: none !important;
  }

  .cb-campaign-figure:hover .cb-campaign-figure-nose,
  .cb-campaign-figure:hover .cb-campaign-figure-nose-carrot,
  .cb-campaign-figure:hover .cb-campaign-figure-glow,
  .cb-campaign-figure:hover .cb-campaign-figure-glow-sun,
  .cb-campaign-figure-trigger:focus-visible .cb-campaign-figure-nose {
    animation: none;
    transform: scale(1.13);
    filter: brightness(1.35) saturate(1.15) drop-shadow(0 0 8px rgba(230, 0, 5, .65));
  }
}
