Design · Bausteine-Dossier

Kategorie

Bewegung & Scroll

Reveals, Bänder, Slider und Karussells — alles Vanilla, ohne Bibliothek. Jeder Baustein läuft hier in der Bühne und respektiert prefers-reduced-motion. Akzentfarbe über --akzent in einer Zeile anpassbar.

Scroll-Reveal mit Stagger & Richtung

Pol 2 · Klarheit Anlass: Sektionen / Leistungskarten

Elemente blenden beim Reinscrollen ein, zeitlich versetzt über data-delay, mit Richtungs-Varianten und re-triggerbar beim erneuten Eintritt. Ohne JS oder bei reduzierter Bewegung sofort sichtbar (das .js-Gate setzt nur JS-fähige Browser auf „unsichtbar"). Scrolle in der Box. Vorbild: NDS, Nördlicht.

↓ scrollen
Beratung vor Ort
Planung & Aufmaß
Montage durch Team
Übergabe & Nachsorge
— Ende —
<div class="fx-reveal-box">
  <div class="fx-reveal-item" data-delay="0">Beratung vor Ort</div>
  <div class="fx-reveal-item from-left" data-delay="0.1">Planung &amp; Aufmaß</div>
  <div class="fx-reveal-item from-right" data-delay="0.2">Montage durch Team</div>
  <div class="fx-reveal-item" data-delay="0.3">Übergabe</div>
</div>

<style>
/* .js wird per Script an <html> gesetzt: ohne JS bleiben Items sichtbar (Fallback) */
.js .fx-reveal-item{opacity:0;transform:translateY(28px);
  transition:opacity .7s cubic-bezier(.25,.46,.45,.94),transform .7s cubic-bezier(.25,.46,.45,.94)}
.js .fx-reveal-item.from-left{transform:translateX(-44px)}
.js .fx-reveal-item.from-right{transform:translateX(44px)}
.js .fx-reveal-item.is-visible{opacity:1;transform:translate(0,0)}
@media (prefers-reduced-motion:reduce){.js .fx-reveal-item{transition:none;opacity:1;transform:none}}
</style>

<script>
document.documentElement.classList.add('js');   // No-JS-Gate
(function(){
  var els = document.querySelectorAll('.fx-reveal-item');
  if(!('IntersectionObserver' in window) || !els.length){
    els.forEach(function(el){ el.classList.add('is-visible'); }); return;
  }
  var io = new IntersectionObserver(function(entries){
    entries.forEach(function(e){
      if(e.isIntersecting){
        var d = parseFloat(e.target.dataset.delay || '0');
        setTimeout(function(){ e.target.classList.add('is-visible'); }, d*1000);
      } else {
        e.target.classList.remove('is-visible');   // re-triggerbar
      }
    });
  }, { threshold:0.3 });           /* in einer Vollseite: kein root nötig */
  els.forEach(function(el){ io.observe(el); });
})();
</script>

Marquee-Band mit Rand-Fades

Pol 1 · Craft Anlass: Logo-Leiste / Bewertungen / Vorteile

Endlos laufendes Band: Inhalt doppelt im Markup, Animation schiebt um −50 % — dadurch nahtloser Loop. Seitliche mask-image-Fades, Pause bei Hover/Fokus, bei reduzierter Bewegung statisch umgebrochen. Vorbild: Hauszeit, NDS, Silver-Mountain.

Termintreue Festpreis-Garantie Meisterbetrieb 5 Jahre Gewährleistung
<div class="fx-marquee">
  <div class="fx-marquee-track">
    <span class="fx-marquee-item"><b>★</b>Termintreue</span>
    <span class="fx-marquee-item"><b>★</b>Festpreis-Garantie</span>
    <span class="fx-marquee-item"><b>★</b>Meisterbetrieb</span>
    <!-- denselben Satz erneut, aria-hidden, für nahtlosen Loop -->
    <span class="fx-marquee-item" aria-hidden="true"><b>★</b>Termintreue</span>
    <span class="fx-marquee-item" aria-hidden="true"><b>★</b>Festpreis-Garantie</span>
    <span class="fx-marquee-item" aria-hidden="true"><b>★</b>Meisterbetrieb</span>
  </div>
</div>

<style>
.fx-marquee{position:relative;width:100%;overflow:hidden;
  -webkit-mask-image:linear-gradient(90deg,transparent 0%,#000 8%,#000 92%,transparent 100%);
  mask-image:linear-gradient(90deg,transparent 0%,#000 8%,#000 92%,transparent 100%)}
.fx-marquee-track{display:flex;width:max-content;gap:14px;animation:fxMarquee 24s linear infinite}
.fx-marquee:hover .fx-marquee-track,.fx-marquee:focus-within .fx-marquee-track{animation-play-state:paused}
.fx-marquee-item{flex:0 0 auto;padding:.6rem 1.1rem;border:1px solid #26262c;border-radius:99px;
  background:#17171b;color:#e9e9ee;font-size:.85rem;white-space:nowrap}
.fx-marquee-item b{color:var(--akzent,#7c87ff);margin-right:.4rem}
@keyframes fxMarquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}
@media (prefers-reduced-motion:reduce){
  .fx-marquee-track{animation:none;flex-wrap:wrap;justify-content:center}
  .fx-marquee-item[aria-hidden="true"]{display:none}
}
</style>

Karten-Karussell mit Snap & Dot-Pagination

Pol 2 · Klarheit Anlass: Leistungskarten am Handy

Horizontaler scroll-snap-Streifen: zentrierte Karte wird hervorgehoben, JS erzeugt Pagination-Punkte und zentriert per Tap. Klick-Schutz: ein Tap auf eine nicht-zentrierte Karte zentriert sie nur, statt einem Link zu folgen. Wische oder tippe die Karten an. Vorbild: Hauszeit, Silver-Mountain.

Küchenmontage

Aufbau und Anschluss durch das eigene Team.

Badsanierung

Komplett aus einer Hand, ein Ansprechpartner.

Bodenverlegung

Parkett, Vinyl und Fliesen sauber verlegt.

Malerarbeiten

Innen wie außen, termintreu und sauber.

<div class="fx-snap">
  <article class="fx-snap-card"><h4>Küchenmontage</h4><p>…</p></article>
  <article class="fx-snap-card"><h4>Badsanierung</h4><p>…</p></article>
  <article class="fx-snap-card"><h4>Bodenverlegung</h4><p>…</p></article>
  <article class="fx-snap-card"><h4>Malerarbeiten</h4><p>…</p></article>
</div>

<style>
.fx-snap{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;
  padding:.5rem 40%;scrollbar-width:none}
.fx-snap::-webkit-scrollbar{display:none}
.fx-snap-card{flex:0 0 70%;scroll-snap-align:center;background:#17171b;border:1px solid #26262c;
  border-radius:14px;padding:1.1rem 1.25rem;color:#e9e9ee;cursor:pointer;
  transition:transform .3s ease,border-color .3s ease,opacity .3s ease}
.fx-snap-card[data-active="true"]{transform:scale(1.04);border-color:var(--akzent,#7c87ff)}
.fx-snap-card:not([data-active="true"]){opacity:.55}
.fx-snap-dots{display:flex;justify-content:center;gap:.5rem;margin-top:.9rem}
.fx-snap-dots .dot{width:8px;height:8px;border-radius:50%;border:none;cursor:pointer;
  background:#3a3a45;transition:background .25s ease,transform .25s ease}
.fx-snap-dots .dot.is-active{background:var(--akzent,#7c87ff);transform:scale(1.3)}
@media (prefers-reduced-motion:reduce){.fx-snap-card,.fx-snap-dots .dot{transition:none}}
</style>

<script>
document.querySelectorAll('.fx-snap').forEach(function(track){
  var cards = Array.from(track.children);
  if(cards.length < 2) return;
  /* Dot-Pagination erzeugen */
  var dots = document.createElement('div');
  dots.className = 'fx-snap-dots'; dots.setAttribute('aria-hidden','true');
  cards.forEach(function(_, i){
    var b = document.createElement('button');
    b.type='button'; b.className='dot'; b.setAttribute('aria-label','Zur Karte '+(i+1));
    b.addEventListener('click', function(){
      cards[i].scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'});
    });
    dots.appendChild(b);
  });
  track.parentNode.insertBefore(dots, track.nextSibling);
  var dotEls = Array.from(dots.children);

  function setActive(idx){
    cards.forEach(function(c,i){ if(i===idx) c.dataset.active='true'; else delete c.dataset.active; });
    dotEls.forEach(function(d,i){ d.classList.toggle('is-active', i===idx); });
  }
  function update(){
    var mid = track.getBoundingClientRect().left + track.clientWidth/2, best=0, bd=Infinity;
    cards.forEach(function(c,i){
      var r=c.getBoundingClientRect(), cen=r.left+r.width/2, dist=Math.abs(cen-mid);
      if(dist

Always-on-Slider: Drag, Wheel & Pfeile

Pol 1 · Craft Anlass: Referenz-Galerie / Logo-Reihe (Desktop)

Auch am Desktop bedienbarer Slider: mit der Maus ziehen (grab/grabbing-Cursor), horizontal mit dem Mausrad scrollen und über Pfeil-Buttons springen — die sich an den Enden deaktivieren. Ziehen unterdrückt den Klick danach. Vorbild: Silver-Mountain.

Projekt 01
Projekt 02
Projekt 03
Projekt 04
Projekt 05
Projekt 06
<div class="fx-slider-wrap">
  <div class="fx-slider">
    <div class="fx-slide">Projekt 01</div>
    <div class="fx-slide">Projekt 02</div>
    <!-- … -->
  </div>
  <div class="fx-slider-nav">
    <button type="button" class="fx-slider-prev" aria-label="Zurück">‹</button>
    <button type="button" class="fx-slider-next" aria-label="Weiter">›</button>
  </div>
</div>

<style>
.fx-slider{display:flex;gap:14px;overflow-x:auto;cursor:grab;scrollbar-width:none}
.fx-slider::-webkit-scrollbar{display:none}
.fx-slider.is-dragging{cursor:grabbing;scroll-behavior:auto}
.fx-slider.is-dragging *{pointer-events:none;user-select:none}
.fx-slide{flex:0 0 150px;height:96px;border-radius:12px;border:1px solid #26262c;
  background:#17171b;color:#e9e9ee;display:grid;place-items:center}
.fx-slider-nav button:disabled{opacity:.35;cursor:not-allowed}
</style>

<script>
document.querySelectorAll('.fx-slider-wrap').forEach(function(wrap){
  var s = wrap.querySelector('.fx-slider');
  var prev = wrap.querySelector('.fx-slider-prev'), next = wrap.querySelector('.fx-slider-next');
  if(!s) return;
  function syncArrows(){
    if(prev) prev.disabled = s.scrollLeft <= 1;
    if(next) next.disabled = s.scrollLeft >= s.scrollWidth - s.clientWidth - 1;
  }
  if(prev) prev.addEventListener('click', function(){ s.scrollBy({left:-s.clientWidth*0.8,behavior:'smooth'}); });
  if(next) next.addEventListener('click', function(){ s.scrollBy({left: s.clientWidth*0.8,behavior:'smooth'}); });
  s.addEventListener('scroll', syncArrows, {passive:true});

  /* Drag-to-Scroll */
  var dragging=false, startX=0, startScroll=0, moved=false;
  s.addEventListener('mousedown', function(e){
    dragging=true; moved=false; startX=e.clientX; startScroll=s.scrollLeft; s.classList.add('is-dragging');
  });
  window.addEventListener('mousemove', function(e){
    if(!dragging) return;
    var dx=e.clientX-startX; if(Math.abs(dx)>5) moved=true; s.scrollLeft=startScroll-dx;
  });
  window.addEventListener('mouseup', function(){
    if(!dragging) return; dragging=false; s.classList.remove('is-dragging');
  });
  /* Klick nach Drag verhindern */
  s.addEventListener('click', function(e){ if(moved){ e.preventDefault(); e.stopPropagation(); moved=false; } }, true);

  /* Mausrad → horizontal (nur innerhalb der Boundaries) */
  s.addEventListener('wheel', function(e){
    if(Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
    var canL = s.scrollLeft > 1, canR = s.scrollLeft < s.scrollWidth - s.clientWidth - 1;
    if((e.deltaY>0 && canR) || (e.deltaY<0 && canL)){ e.preventDefault(); s.scrollLeft += e.deltaY; }
  }, {passive:false});

  syncArrows();
});
</script>

Endlos-Center-Karussell (kompakt)

Pol 1 · Craft Anlass: Highlight-Slider / Partner-Spotlight

Aktive Karte mittig und größer, die Nachbarn als halbtransparenter Peek, Rest wartet unsichtbar am Rand — die Pfeile rotieren endlos in einer Richtung. Kompakte, repräsentative Vanilla-Version der Mechanik (ohne Touch-Swipe und Tastatur-Loop des Originals). Vorbild: Silver-Mountain.

Marke A
Marke B
Marke C
Marke D
Marke E
<div class="fx-center" data-fx-center>
  <div class="fx-center-track">
    <div class="fx-center-card">Marke A</div>
    <div class="fx-center-card">Marke B</div>
    <div class="fx-center-card">Marke C</div>
    <div class="fx-center-card">Marke D</div>
    <div class="fx-center-card">Marke E</div>
  </div>
</div>
<div class="fx-center-nav">
  <button type="button" class="fx-center-prev" aria-label="Vorherige">‹</button>
  <button type="button" class="fx-center-next" aria-label="Nächste">›</button>
</div>

<style>
.fx-center{position:relative;width:100%;max-width:360px;height:170px;margin:0 auto}
.fx-center-card{position:absolute;top:50%;left:50%;width:150px;height:130px;border-radius:14px;
  border:1px solid #26262c;background:#17171b;color:#e9e9ee;display:grid;place-items:center;
  transform:translate(-50%,-50%);transition:transform .35s cubic-bezier(.4,0,.2,1),opacity .35s ease}
.fx-center-card.is-center{border-color:var(--akzent,#7c87ff);box-shadow:0 8px 26px rgba(0,0,0,.4)}
@media (prefers-reduced-motion:reduce){.fx-center-card{transition:none}}
</style>

<script>
document.querySelectorAll('[data-fx-center]').forEach(function(box){
  var cards = Array.from(box.querySelectorAll('.fx-center-card'));
  var n = cards.length; if(n < 2) return;
  var wrap = box.parentNode;
  var prev = wrap.querySelector('.fx-center-prev'), next = wrap.querySelector('.fx-center-next');

  /* Positionen symmetrisch um die Mitte verteilen */
  var positions = cards.map(function(_, i){ var p=i%n; if(p>Math.floor(n/2)) p-=n; return p; });
  var animating = false;

  function place(card, p){
    var abs = Math.abs(p), x, op, z;
    if(p===0){ x=0; op=1; z=4; }
    else if(abs===1){ x=p*92; op=0.5; z=3; }
    else { x=(p>0?1:-1)*150; op=0; z=1; }
    card.style.transform = 'translate(calc(-50% + '+x+'px), -50%)';
    card.style.opacity = op; card.style.zIndex = z;
    card.classList.toggle('is-center', p===0);
    card.style.pointerEvents = abs<=1 ? 'auto' : 'none';
    card.setAttribute('aria-hidden', p===0 ? 'false' : 'true');
  }
  function layout(){ cards.forEach(function(c,i){ place(c, positions[i]); }); }

  function rotate(dir){
    if(animating) return; animating = true;
    cards.forEach(function(c,i){
      var np = positions[i] - dir;
      /* hinten raus → ohne Transition zur anderen Seite snappen (unsichtbar) */
      if(dir>0 && np < -1 && positions[i] <= -1){
        positions[i] = np + n; c.style.transition='none'; place(c, positions[i]);
        void c.offsetWidth; c.style.transition='';
      } else if(dir<0 && np > 1 && positions[i] >= 1){
        positions[i] = np - n; c.style.transition='none'; place(c, positions[i]);
        void c.offsetWidth; c.style.transition='';
      } else { positions[i] = np; place(c, positions[i]); }
    });
    setTimeout(function(){ animating=false; }, 60);
  }
  if(prev) prev.addEventListener('click', function(){ rotate(-1); });
  if(next) next.addEventListener('click', function(){ rotate(1); });
  /* Klick auf Peek-Karte zentriert sie */
  cards.forEach(function(c,i){
    c.addEventListener('click', function(){ if(positions[i]===1) rotate(1); else if(positions[i]===-1) rotate(-1); });
  });
  layout();
});
</script>

Smooth-Scroll mit Header-Offset

Pol 2 · Klarheit Anlass: Anker-Navigation (Util)

Anker-Links scrollen sanft zum Ziel — und rechnen die Höhe der fixierten Kopfleiste als Offset ab, damit die Überschrift nicht unter dem Header verschwindet. Hier in einer scrollbaren Box mit eigener Sticky-Leiste demonstriert. Vorbild: Hauszeit.

Beratung

Wir besprechen Ihr Vorhaben vor Ort und nehmen Maß.

Ablauf

Klarer Zeitplan, ein Ansprechpartner, termintreue Umsetzung.

Kontakt

Schreiben Sie uns für ein unverbindliches Angebot.

<header style="--header-h:72px; position:sticky; top:0">…Navigation…</header>
<a href="#kontakt">Kontakt</a>
<section id="kontakt">…</section>

<script>
document.querySelectorAll('a[href^="#"]').forEach(function(a){
  a.addEventListener('click', function(e){
    var id = a.getAttribute('href');
    if(!id || id === '#') return;
    var target = document.querySelector(id);
    if(!target) return;
    e.preventDefault();
    var headerH = parseInt(getComputedStyle(document.documentElement)
                    .getPropertyValue('--header-h')) || 72;
    var top = target.getBoundingClientRect().top + window.scrollY - headerH - 12;
    window.scrollTo({ top: top, behavior: 'smooth' });
  });
});
</script>

Live-Countdown

Pol 2 · Klarheit Anlass: Aktion / Event-Frist (Util)

Zählt sekundengenau auf ein Zieldatum herunter (Tage/Stunden/Minuten/Sekunden), zweistellig per padStart. Zieldatum als ISO-Wert im data-countdown-Attribut. Vorbild: Silver-Mountain.

Tage
Std
Min
Sek
<div class="fx-count" data-countdown="2026-12-31T23:59:59">
  <div class="fx-count-unit"><b data-unit="days">–</b><span>Tage</span></div>
  <div class="fx-count-unit"><b data-unit="hours">–</b><span>Std</span></div>
  <div class="fx-count-unit"><b data-unit="minutes">–</b><span>Min</span></div>
  <div class="fx-count-unit"><b data-unit="seconds">–</b><span>Sek</span></div>
</div>

<style>
.fx-count{display:inline-flex;gap:.6rem}
.fx-count-unit{min-width:62px;text-align:center;background:#17171b;border:1px solid #26262c;
  border-radius:12px;padding:.7rem .5rem}
.fx-count-unit b{display:block;font-size:1.6rem;font-weight:700;color:#f4f4f5;font-variant-numeric:tabular-nums}
.fx-count-unit span{display:block;margin-top:.35rem;font-size:.66rem;text-transform:uppercase;
  letter-spacing:.1em;color:#a1a1aa}
</style>

<script>
document.querySelectorAll('[data-countdown]').forEach(function(el){
  var target = new Date(el.dataset.countdown).getTime();
  var daysEl = el.querySelector('[data-unit="days"]'),
      hoursEl = el.querySelector('[data-unit="hours"]'),
      minsEl = el.querySelector('[data-unit="minutes"]'),
      secsEl = el.querySelector('[data-unit="seconds"]');
  function tick(){
    var diff = target - Date.now();
    if(diff <= 0){
      if(daysEl) daysEl.textContent='0';
      if(hoursEl) hoursEl.textContent='00';
      if(minsEl) minsEl.textContent='00';
      if(secsEl) secsEl.textContent='00';
      return;
    }
    var d = Math.floor(diff/86400000),
        h = Math.floor((diff%86400000)/3600000),
        m = Math.floor((diff%3600000)/60000),
        s = Math.floor((diff%60000)/1000);
    if(daysEl) daysEl.textContent = String(d);
    if(hoursEl) hoursEl.textContent = String(h).padStart(2,'0');
    if(minsEl) minsEl.textContent = String(m).padStart(2,'0');
    if(secsEl) secsEl.textContent = String(s).padStart(2,'0');
  }
  tick(); setInterval(tick, 1000);
});
</script>

Funken-Cursor

Pol 1 · Craft Anlass: Motto-Band / Hero (thematisch, sparsam)

Beim Bewegen der Maus über die Fläche entstehen kurzlebige Funken an der Cursor-Position; sie steigen auf, verblassen und entfernen sich selbst. Per performance.now() gedrosselt; bei reduzierter Bewegung komplett aus. Nische — nur für energetische Marken. Vorbild: Nördlicht.

Maus hier bewegen
<div class="fx-spark" data-fx-spark>
  <span class="fx-spark-label">Maus hier bewegen</span>
</div>

<style>
.fx-spark{position:relative;width:100%;max-width:340px;height:130px;border-radius:14px;overflow:hidden;
  display:grid;place-items:center;color:#e9e9ee;cursor:crosshair;
  background:linear-gradient(135deg,#1c1c2b,#101014);border:1px solid #26262c}
.fx-spark-label{position:relative;z-index:2;pointer-events:none}
.fx-spark-dot{position:absolute;border-radius:50%;pointer-events:none;z-index:1;
  background:radial-gradient(circle,#fff 0%,var(--akzent,#7c87ff) 55%,transparent 75%);
  opacity:0;transform:translate(-50%,-50%) scale(.4);animation:fxSpark .9s ease-out forwards}
@keyframes fxSpark{
  0%{opacity:1;transform:translate(-50%,-50%) scale(.4)}
  100%{opacity:0;transform:translate(-50%,-90%) scale(1.1)}
}
@media (prefers-reduced-motion:reduce){.fx-spark-dot{display:none}}
</style>

<script>
(function(){
  var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches;
  if(reduce) return;   /* bei reduzierter Bewegung komplett aus */
  document.querySelectorAll('[data-fx-spark]').forEach(function(field){
    var last = 0, throttle = 32;   /* ~30 Funken/s max */
    field.addEventListener('mousemove', function(e){
      var now = performance.now();
      if(now - last < throttle) return;
      last = now;
      var r = field.getBoundingClientRect();
      spawn(field, e.clientX - r.left, e.clientY - r.top);
    });
    function spawn(field, x, y){
      var s = document.createElement('span');
      s.className = 'fx-spark-dot';
      var spread = 18, size = 4 + Math.random()*7;
      s.style.left = (x + (Math.random()-0.5)*spread) + 'px';
      s.style.top  = (y + (Math.random()-0.5)*spread) + 'px';
      s.style.width = size + 'px';
      s.style.height = size + 'px';
      field.appendChild(s);
      setTimeout(function(){ if(s.parentNode) s.parentNode.removeChild(s); }, 900);
    }
  });
})();
</script>

Parallax-Ebenen

Pol 1 · Craft Anlass: Hero-Hintergrund

Mehrere Ebenen bewegen sich beim Scrollen unterschiedlich schnell und erzeugen so einen Tiefeneffekt. Modern per animation-timeline: scroll(); Fallback über einen scroll-Listener mit requestAnimationFrame, der die Ebenen per translateY verschiebt. In der Box scrollen.

Tiefe durch Tempo

↑ Ebenen ziehen unterschiedlich schnell mit — weiter scrollen

<div class="par-box">
  <div class="par-scene">
    <div class="par-stage">
      <div class="par-layer par-l1"></div>
      <div class="par-layer par-l2"></div>
      <div class="par-layer par-l3">Tiefe durch Tempo</div>
    </div>
  </div>
</div>

<style>
.par-box{position:relative;height:200px;overflow-y:auto;border:1px solid #26262c;border-radius:12px;background:#101014}
.par-scene{position:relative;height:620px}
.par-stage{position:sticky;top:0;height:200px;overflow:hidden}
.par-layer{position:absolute;inset:0;display:grid;place-items:center;will-change:transform}
.par-l3{color:#f4f4f5;font-weight:700}
/* Wenn scroll()-Timeline verfügbar: unterschiedliche Tempi ohne JS */
@supports (animation-timeline:scroll()){
  .par-box:not(.js-par) .par-l1{animation:parY1 linear both;animation-timeline:scroll(nearest)}
  .par-box:not(.js-par) .par-l2{animation:parY2 linear both;animation-timeline:scroll(nearest)}
  .par-box:not(.js-par) .par-l3{animation:parY3 linear both;animation-timeline:scroll(nearest)}
}
@keyframes parY1{to{transform:translateY(-30px)}}
@keyframes parY2{to{transform:translateY(-90px)}}
@keyframes parY3{to{transform:translateY(-150px)}}
@media (prefers-reduced-motion:reduce){.par-l1,.par-l2,.par-l3{animation:none!important;transform:none!important}}
</style>

<script>
/* Fallback: nur nötig, wenn scroll()-Timeline NICHT unterstützt wird */
(function(){
  if(CSS.supports && CSS.supports('animation-timeline:scroll()')) return;
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  document.querySelectorAll('.par-box').forEach(function(box){
    box.classList.add('js-par');            /* @supports-Regel deaktivieren */
    if(reduce) return;
    var l1=box.querySelector('.par-l1'), l2=box.querySelector('.par-l2'), l3=box.querySelector('.par-l3');
    var raf=0;
    function upd(){
      var max=box.scrollHeight-box.clientHeight, p=max>0?box.scrollTop/max:0;
      if(l1) l1.style.transform='translateY('+(-30*p)+'px)';
      if(l2) l2.style.transform='translateY('+(-90*p)+'px)';
      if(l3) l3.style.transform='translateY('+(-150*p)+'px)';
      raf=0;
    }
    box.addEventListener('scroll', function(){ if(!raf) raf=requestAnimationFrame(upd); }, {passive:true});
    upd();
  });
})();
</script>

Scale-/Zoom-on-Scroll

Pol 1 · Craft Anlass: Bild-Reveal

Ein Bild-Platzhalter wächst beim Reinscrollen von leicht verkleinert auf volle Größe. Modern per animation-timeline: view() (an der Sichtbarkeit des Elements gekoppelt); Fallback über IntersectionObserver plus Scroll-Fortschritt. In der Box scrollen.

↓ scrollen
— Ende —
<div class="zoom-box">
  <div class="zoom-frame"><div class="zoom-img"></div></div>
</div>

<style>
.zoom-frame{width:78%;aspect-ratio:16/10;margin:0 auto;border-radius:14px;overflow:hidden;border:1px solid #26262c}
.zoom-img{width:100%;height:100%;transform:scale(.72);transform-origin:center;
  background:linear-gradient(135deg,#23233a,#101016)}
@supports (animation-timeline:view()){
  .zoom-box:not(.js-zoom) .zoom-img{animation:zoomIn linear both;
    animation-timeline:view();animation-range:entry 5% cover 45%}
}
@keyframes zoomIn{from{transform:scale(.72)}to{transform:scale(1)}}
@media (prefers-reduced-motion:reduce){.zoom-img{animation:none!important;transform:scale(1)!important}}
</style>

<script>
/* Fallback ohne view()-Timeline: Skalierung an Scroll-Position koppeln */
(function(){
  if(CSS.supports && CSS.supports('animation-timeline:view()')) return;
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  document.querySelectorAll('.zoom-box').forEach(function(box){
    box.classList.add('js-zoom');
    var img=box.querySelector('.zoom-img'); if(!img) return;
    if(reduce){ img.style.transform='scale(1)'; return; }
    var raf=0;
    function upd(){
      var r=img.getBoundingClientRect(), b=box.getBoundingClientRect();
      /* 0 = Element betritt unten, 1 = Element mittig */
      var prog=(b.bottom - r.top)/(b.height*0.9);
      prog=Math.max(0,Math.min(1,prog));
      img.style.transform='scale('+(0.72+0.28*prog)+')';
      raf=0;
    }
    box.addEventListener('scroll', function(){ if(!raf) raf=requestAnimationFrame(upd); }, {passive:true});
    upd();
  });
})();
</script>

Sticky-Stack-Karten

Pol 1 · Craft Anlass: Storytelling / Schritte

Jede Karte wird beim Scrollen sticky und bleibt oben kleben, während die nächste sich darüberschiebt — die Karten „docken" gestapelt an. Reine CSS-Mechanik über position:sticky, kein JS nötig. Bei reduzierter Bewegung stehen die Karten schlicht untereinander. In der Box scrollen.

Schritt 01

Beratung

Wir hören zu und erfassen Ihr Vorhaben.

Schritt 02

Planung

Klarer Ablauf, Aufmaß und Angebot.

Schritt 03

Umsetzung

Termintreue Montage durch das Team.

Schritt 04

Übergabe

Abnahme und Nachsorge inklusive.

<div class="stack-box">
  <div class="stack-card"><span class="stack-num">Schritt 01</span><h4>Beratung</h4><p>…</p></div>
  <div class="stack-card"><span class="stack-num">Schritt 02</span><h4>Planung</h4><p>…</p></div>
  <div class="stack-card"><span class="stack-num">Schritt 03</span><h4>Umsetzung</h4><p>…</p></div>
  <div class="stack-card"><span class="stack-num">Schritt 04</span><h4>Übergabe</h4><p>…</p></div>
</div>

<style>
.stack-box{height:220px;overflow-y:auto;border:1px solid #26262c;border-radius:12px;background:#101014;padding:0 .9rem}
/* Der Trick: jede Karte klebt an derselben top-Position, die nächste schiebt sich darüber */
.stack-card{position:sticky;top:14px;margin:14px 0;min-height:120px;border-radius:14px;padding:1rem 1.1rem;
  border:1px solid #26262c;background:#17171b;box-shadow:0 -6px 20px rgba(0,0,0,.35)}
.stack-num{display:inline-block;font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;
  color:var(--akzent,#7c87ff);margin-bottom:.5rem}
/* leicht abgestufte Flächen erhöhen den Stapel-Eindruck */
.stack-card:nth-child(1){background:#191922}
.stack-card:nth-child(2){background:#17171e}
.stack-card:nth-child(3){background:#15151b}
.stack-card:nth-child(4){background:#131318}
@media (prefers-reduced-motion:reduce){.stack-card{position:static}}
</style>

Horizontal-Scroll-Sektion

Pol 1 · Craft Anlass: Galerie / Prozess

Vertikales Scrollen wird in eine horizontale Bewegung übersetzt: die Sektion wird „gepinnt" (sticky) und die Reihe per translateX verschoben. Modern über animation-timeline: scroll(); Fallback per scroll-Listener. Bei reduzierter Bewegung wird die Reihe stattdessen normal horizontal scrollbar. In der Box vertikal scrollen.

01Beratung
02Planung
03Umsetzung
04Übergabe
<div class="hscroll-box">
  <div class="hscroll-scene">
    <div class="hscroll-pin">
      <div class="hscroll-track">
        <div class="hscroll-item">01 Beratung</div>
        <div class="hscroll-item">02 Planung</div>
        <div class="hscroll-item">03 Umsetzung</div>
        <div class="hscroll-item">04 Übergabe</div>
      </div>
    </div>
  </div>
</div>

<style>
.hscroll-box{height:210px;overflow-y:auto;border:1px solid #26262c;border-radius:12px;background:#101014}
.hscroll-scene{height:640px}                      /* Scroll-Weg = horizontale Strecke */
.hscroll-pin{position:sticky;top:0;height:210px;display:flex;align-items:center;overflow:hidden}
.hscroll-track{display:flex;gap:14px;padding:0 1rem;will-change:transform}
.hscroll-item{flex:0 0 200px;height:130px;border:1px solid #26262c;background:#17171b;border-radius:14px}
@supports (animation-timeline:scroll()){
  .hscroll-box:not(.js-h) .hscroll-track{animation:hScroll linear both;animation-timeline:scroll(nearest)}
}
@keyframes hScroll{to{transform:translateX(calc(-100% + 300px))}}
@media (prefers-reduced-motion:reduce){
  .hscroll-track{animation:none!important;transform:none!important}
  .hscroll-pin{overflow-x:auto}
}
</style>

<script>
/* Fallback ohne scroll()-Timeline: translateX an den vertikalen Fortschritt koppeln */
(function(){
  if(CSS.supports && CSS.supports('animation-timeline:scroll()')) return;
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  document.querySelectorAll('.hscroll-box').forEach(function(box){
    box.classList.add('js-h');
    if(reduce){ box.querySelector('.hscroll-pin').style.overflowX='auto'; return; }
    var track=box.querySelector('.hscroll-track'); if(!track) return;
    var raf=0;
    function upd(){
      var max=box.scrollHeight-box.clientHeight, p=max>0?box.scrollTop/max:0;
      var dist=track.scrollWidth - box.clientWidth + 32;   /* zu verschiebende Strecke */
      track.style.transform='translateX('+(-dist*p)+'px)';
      raf=0;
    }
    box.addEventListener('scroll', function(){ if(!raf) raf=requestAnimationFrame(upd); }, {passive:true});
    upd();
  });
})();
</script>

Scroll-Fortschrittsleiste

Pol 2 · Klarheit Anlass: Blog / Langtext

Eine dünne Leiste oben zeigt den Lesefortschritt. Modern per animation-timeline: scroll() mit scaleX; Fallback über einen scroll-Listener, der denselben scaleX-Wert setzt. Kein Layout-Reflow, nur Transform. In der Box scrollen.

Langtext

Ein Beispieltext, der über die Höhe der Box hinausgeht. Beim Scrollen füllt sich die Leiste am oberen Rand.

Die Leiste bleibt sticky sichtbar und spiegelt die Position im Text — nützlich bei Blogartikeln und langen Seiten.

So behält der Leser jederzeit im Blick, wie viel Inhalt noch folgt, ohne dass eine Scrollleiste allein reicht.

Kurz vor dem Ende steht die Leiste fast voll. Am Ende ist sie komplett gefüllt.

<div class="sprog-box">
  <div class="sprog-bar" role="progressbar" aria-label="Lesefortschritt"></div>
  <div class="sprog-content">… Langtext …</div>
</div>

<style>
.sprog-box{position:relative;height:200px;overflow-y:auto;border:1px solid #26262c;border-radius:12px;background:#101014}
.sprog-bar{position:sticky;top:0;left:0;height:4px;width:100%;transform-origin:0 50%;transform:scaleX(0);
  background:var(--akzent,#7c87ff);z-index:3}
@supports (animation-timeline:scroll()){
  .sprog-box:not(.js-sp) .sprog-bar{animation:sprogGrow linear both;animation-timeline:scroll(nearest)}
}
@keyframes sprogGrow{to{transform:scaleX(1)}}
</style>

<script>
/* Fallback ohne scroll()-Timeline: scaleX an den Fortschritt koppeln
   (im Vollseiten-Einsatz statt der Box document.documentElement verwenden) */
(function(){
  if(CSS.supports && CSS.supports('animation-timeline:scroll()')) return;
  document.querySelectorAll('.sprog-box').forEach(function(box){
    box.classList.add('js-sp');
    var bar=box.querySelector('.sprog-bar'); if(!bar) return;
    var raf=0;
    function upd(){
      var max=box.scrollHeight-box.clientHeight, p=max>0?box.scrollTop/max:0;
      bar.style.transform='scaleX('+p+')';
      raf=0;
    }
    box.addEventListener('scroll', function(){ if(!raf) raf=requestAnimationFrame(upd); }, {passive:true});
    upd();
  });
})();
</script>

Clip-Reveal beim Scroll

Pol 1 · Craft Anlass: Bild-Auftritt

Ein Bild wird beim Reinscrollen per clip-path-Wischer von links nach rechts aufgedeckt. Modern per animation-timeline: view(); Fallback über IntersectionObserver plus Scroll-Fortschritt, der die inset()-Maske aufzieht. In der Box scrollen.

↓ scrollen
Referenz
— Ende —
<div class="clip-box">
  <div class="clip-frame"><div class="clip-img"></div></div>
</div>

<style>
.clip-frame{width:80%;aspect-ratio:16/10;margin:0 auto;border-radius:14px;overflow:hidden;border:1px solid #26262c}
.clip-img{width:100%;height:100%;background:linear-gradient(135deg,#22223a,#0f0f15);
  clip-path:inset(0 100% 0 0)}                 /* Startzustand: vollständig verdeckt */
@supports (animation-timeline:view()){
  .clip-box:not(.js-clip) .clip-img{animation:clipWipe linear both;
    animation-timeline:view();animation-range:entry 8% cover 42%}
}
@keyframes clipWipe{to{clip-path:inset(0 0 0 0)}}
@media (prefers-reduced-motion:reduce){.clip-img{animation:none!important;clip-path:inset(0 0 0 0)!important}}
</style>

<script>
/* Fallback ohne view()-Timeline: inset()-Maske an Scroll-Fortschritt koppeln */
(function(){
  if(CSS.supports && CSS.supports('animation-timeline:view()')) return;
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  document.querySelectorAll('.clip-box').forEach(function(box){
    box.classList.add('js-clip');
    var img=box.querySelector('.clip-img'); if(!img) return;
    if(reduce){ img.style.clipPath='inset(0 0 0 0)'; return; }
    var raf=0;
    function upd(){
      var r=img.getBoundingClientRect(), b=box.getBoundingClientRect();
      var prog=(b.bottom - r.top)/(b.height*0.85);
      prog=Math.max(0,Math.min(1,prog));
      img.style.clipPath='inset(0 '+(100-100*prog)+'% 0 0)';
      raf=0;
    }
    box.addEventListener('scroll', function(){ if(!raf) raf=requestAnimationFrame(upd); }, {passive:true});
    upd();
  });
})();
</script>

Text-Reveal Wort für Wort

Pol 1 · Craft Anlass: Zitat / Manifest

Die Wörter eines Fließtextes blenden beim Scrollen nacheinander von gedämpft auf voll auf. Jedes Wort ist ein <span>; modern per animation-timeline: view() je Wort, Fallback über IntersectionObserver, der die Wörter zeitversetzt aktiviert. Wörter werden per JS umschlossen, damit das Markup schlank bleibt. In der Box scrollen.

↓ scrollen

Gute Arbeit spricht für sich, doch der richtige Auftritt macht sie erst sichtbar.

— Ende —
<p class="tr-text" data-tr>Gute Arbeit spricht für sich …</p>

<style>
.tr-text{font-size:1.02rem;line-height:1.9;font-weight:600}
.tr-word{color:#3a3a45;transition:color .25s linear}   /* gedämpfter Startzustand */
.tr-word.is-on{color:#f4f4f5}
@supports (animation-timeline:view()){
  .tr-box:not(.js-tr) .tr-word{animation:trOn linear both;animation-timeline:view()}
}
@keyframes trOn{from{color:#3a3a45}to{color:#f4f4f5}}
@media (prefers-reduced-motion:reduce){.tr-word{animation:none!important;color:#f4f4f5!important}}
</style>

<script>
/* Wörter in spans zerlegen; Fallback aktiviert sie via IntersectionObserver */
(function(){
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  var hasView = CSS.supports && CSS.supports('animation-timeline:view()');
  document.querySelectorAll('[data-tr]').forEach(function(p){
    var box = p.closest('.tr-box') || document;
    p.innerHTML = p.textContent.trim().split(/\s+/).map(function(w){
      return '<span class="tr-word">'+w+'</span>';
    }).join(' ');
    var words = p.querySelectorAll('.tr-word');
    if(hasView) return;                     /* CSS view()-Timeline erledigt es */
    box.classList && box.classList.add('js-tr');
    if(reduce){ words.forEach(function(w){ w.classList.add('is-on'); }); return; }
    if(!('IntersectionObserver' in window)){ words.forEach(function(w){ w.classList.add('is-on'); }); return; }
    var io = new IntersectionObserver(function(entries){
      entries.forEach(function(e){ e.target.classList.toggle('is-on', e.isIntersecting); });
    }, { root: box.classList.contains('tr-box') ? box : null, threshold: 0.9 });
    words.forEach(function(w){ io.observe(w); });
  });
})();
</script>

Cardslider — Auto-Story-Slider

Pol 1 · Craft Anlass: Hero / Referenz-Story

Fullscreen-Story-Slider: die aktive Karte füllt die Bühne, die kommenden liegen als Kacheln unten rechts, ein Timer rückt automatisch vor. Beim Wechsel faded die Karte über und der Detail-Text (Rubrik/Titel/Text/CTA) blendet gestaffelt ein. Bilder als CSS-Verlauf angedeutet (im Projekt background-image je Karte setzen). Kompakte Clean-Room-Vanilla-Fassung (kein GSAP), responsiv Web + Mobil. Auto-Advance pausiert bei Hover/Fokus und ist bei prefers-reduced-motion aus (nur Pfeile/Kacheln).

Küche

Einbauküchen nach Maß

Vom Aufmaß bis zur Montage — sauber aus einer Hand.

Bad

Barrierefreie Bäder

Bodengleiche Duschen, durchdacht bis ins Detail.

Boden

Parkett und Dielen

Warme Böden, fachgerecht verlegt und versiegelt.

Außen

Terrassen und Zäune

Langlebige Außenanlagen aus Holz und Stein.

<div class="tcs" data-tcs role="group" aria-roledescription="Karussell" aria-label="Referenz-Slider">
  <div class="tcs-timer" aria-hidden="true"><div class="tcs-timer-fill"></div></div>
  <p class="tcs-count" aria-hidden="true"><b>01</b> / 04</p>
  <div class="tcs-nav">
    <button class="tcs-arrow" type="button" data-tcs-prev aria-label="Vorherige Folie"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
    <button class="tcs-arrow" type="button" data-tcs-next aria-label="Nächste Folie"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
  </div>
  <!-- je Karte: statt Verlauf-Klasse s1..s4 ein background-image setzen -->
  <article class="tcs-slide s1 is-active">
    <div class="tcs-detail">
      <span class="tcs-eyebrow">Küche</span>
      <h3 class="tcs-title">Einbauküchen nach Maß</h3>
      <p class="tcs-desc">Vom Aufmaß bis zur Montage – sauber aus einer Hand.</p>
      <button class="tcs-cta" type="button">Projekt ansehen</button>
    </div>
  </article>
  <article class="tcs-slide s2">…</article>  <!-- Bad -->
  <article class="tcs-slide s3">…</article>  <!-- Boden -->
  <article class="tcs-slide s4">…</article>  <!-- Außen -->
  <div class="tcs-thumbs"></div>  <!-- Kacheln der kommenden Karten (per JS) -->
</div>

<style>
.tcs{position:relative;width:100%;max-width:560px;aspect-ratio:16/10;min-height:300px;
  border-radius:16px;overflow:hidden;border:1px solid #26262c;background:#101014;color:#f4f4f5;isolation:isolate}
.tcs-slide{position:absolute;inset:0;opacity:0;visibility:hidden;transform:scale(1.06);
  transition:opacity .7s ease,transform .9s ease,visibility 0s .7s;background-size:cover;background-position:center}
.tcs-slide.is-active{opacity:1;visibility:visible;transform:scale(1);
  transition:opacity .7s ease,transform 6s linear,visibility 0s}
.tcs-slide::after{content:"";position:absolute;inset:0;z-index:1;
  background:linear-gradient(90deg,rgba(8,8,12,.86) 0%,rgba(8,8,12,.5) 42%,rgba(8,8,12,.08) 72%)}
.tcs-slide.s1{background-image:linear-gradient(135deg,#3b3b6e,#1a1a2b)}
.tcs-slide.s2{background-image:linear-gradient(135deg,#20514a,#122423)}
.tcs-slide.s3{background-image:linear-gradient(135deg,#5a3a53,#241522)}
.tcs-slide.s4{background-image:linear-gradient(135deg,#4a4632,#211f14)}
.tcs-detail{position:absolute;left:0;bottom:0;z-index:2;max-width:72%;padding:1.5rem 1.7rem;
  display:flex;flex-direction:column;gap:.55rem}
.tcs-eyebrow{font-size:.68rem;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:var(--akzent,#7c87ff)}
.tcs-title{margin:0;font-size:clamp(1.4rem,5vw,2.15rem);line-height:1.04;letter-spacing:-.02em;
  font-weight:800;overflow-wrap:break-word}
.tcs-desc{margin:0;font-size:.86rem;color:#d4d4dc;max-width:34ch}
.tcs-detail>*{opacity:0;transform:translateY(14px);transition:opacity .5s ease,transform .5s ease}
.tcs-slide.is-active .tcs-detail>*{opacity:1;transform:none}
.tcs-slide.is-active .tcs-eyebrow{transition-delay:.16s}
.tcs-slide.is-active .tcs-title{transition-delay:.29s}
.tcs-slide.is-active .tcs-desc{transition-delay:.42s}
.tcs-slide.is-active .tcs-cta{transition-delay:.55s}
.tcs-cta{align-self:flex-start;margin-top:.35rem;display:inline-flex;align-items:center;gap:.4rem;
  padding:.58rem 1rem;border-radius:99px;font-size:.8rem;font-weight:700;
  background:var(--akzent,#7c87ff);color:#0f0f12;border:0;cursor:pointer}
.tcs-cta:focus-visible{outline:2px solid #fff;outline-offset:2px}
.tcs-timer{position:absolute;top:0;left:0;right:0;height:3px;z-index:5;background:rgba(255,255,255,.14)}
.tcs-timer-fill{height:100%;width:0;background:var(--akzent,#7c87ff)}
.tcs-thumbs{position:absolute;right:1rem;bottom:1rem;z-index:4;display:flex;gap:.55rem}
.tcs-thumb{width:54px;height:72px;border-radius:10px;border:1px solid rgba(255,255,255,.22);
  background-size:cover;background-position:center;cursor:pointer;flex:0 0 auto;
  transition:transform .3s ease,border-color .3s ease}
.tcs-thumb:hover,.tcs-thumb:focus-visible{transform:translateY(-4px);border-color:#fff;outline:none}
.tcs-thumb.t1{background-image:linear-gradient(135deg,#3b3b6e,#1a1a2b)}
.tcs-thumb.t2{background-image:linear-gradient(135deg,#20514a,#122423)}
.tcs-thumb.t3{background-image:linear-gradient(135deg,#5a3a53,#241522)}
.tcs-thumb.t4{background-image:linear-gradient(135deg,#4a4632,#211f14)}
.tcs-nav{position:absolute;right:1rem;top:1rem;z-index:5;display:flex;gap:.4rem}
.tcs-arrow{width:38px;height:38px;display:grid;place-items:center;border-radius:50%;cursor:pointer;color:#f4f4f5;
  background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.2);
  -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);transition:background .25s ease}
.tcs-arrow:hover,.tcs-arrow:focus-visible{background:rgba(255,255,255,.26);outline:none}
.tcs-arrow svg{width:18px;height:18px}
.tcs-count{position:absolute;left:1.7rem;top:1.15rem;z-index:5;font-size:.78rem;
  font-variant-numeric:tabular-nums;color:#d4d4dc}
.tcs-count b{color:#fff;font-weight:800}
@media(max-width:520px){.tcs-detail{max-width:100%}.tcs-thumb{width:42px;height:56px}.tcs-desc{display:none}}
@media (prefers-reduced-motion:reduce){
  .tcs-slide,.tcs-slide.is-active,.tcs-detail>*{transition:none}
  .tcs-slide.is-active{transform:none}
}
</style>

<script>
(function(){
  var root = document.querySelector('[data-tcs]');
  if(!root) return;
  var slides = Array.prototype.slice.call(root.querySelectorAll('.tcs-slide'));
  var n = slides.length; if(n < 2) return;
  var fill = root.querySelector('.tcs-timer-fill'),
      thumbs = root.querySelector('.tcs-thumbs'),
      count = root.querySelector('.tcs-count');
  var cur = 0, DUR = 5200, raf = 0, elapsed = 0, last = 0, paused = false;
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  function pad(i){ return (i<10?'0':'')+i; }
  function renderThumbs(){
    thumbs.innerHTML = '';
    for(var k=1;k<n;k++){
      var idx = (cur+k)%n;
      var b = document.createElement('button');
      b.type = 'button'; b.className = 'tcs-thumb t'+(idx+1);
      b.setAttribute('aria-label','Zu Folie '+(idx+1));
      (function(t){ b.addEventListener('click', function(){ go(t); }); })(idx);
      thumbs.appendChild(b);
    }
  }
  function setActive(i){
    slides.forEach(function(s,k){ s.classList.toggle('is-active', k===i); });
    if(count) count.innerHTML = '<b>'+pad(i+1)+'</b> / '+pad(n);
    cur = i; renderThumbs();
  }
  function go(i){ setActive((i%n+n)%n); restart(); }
  function frame(t){
    if(!last) last = t;
    var dt = t - last; last = t;
    if(!paused) elapsed += dt;
    if(fill) fill.style.width = Math.min(elapsed/DUR,1)*100 + '%';
    if(elapsed >= DUR){ go(cur+1); return; }
    raf = requestAnimationFrame(frame);
  }
  function restart(){
    cancelAnimationFrame(raf); elapsed = 0; last = 0;
    if(fill) fill.style.width = '0%';
    if(!reduce) raf = requestAnimationFrame(frame);
  }
  var pv = root.querySelector('[data-tcs-prev]'), nx = root.querySelector('[data-tcs-next]');
  if(pv) pv.addEventListener('click', function(){ go(cur-1); });
  if(nx) nx.addEventListener('click', function(){ go(cur+1); });
  root.addEventListener('mouseenter', function(){ paused = true; });
  root.addEventListener('mouseleave', function(){ paused = false; });
  root.addEventListener('focusin', function(){ paused = true; });
  root.addEventListener('focusout', function(){ paused = false; });
  setActive(0); restart();
})();
</script>

Scroll-Farbwechsel

Pol 1 · Craft Anlass: Story / Prozess

Der Hintergrund (und die Textfarbe) der Sektion wechselt beim Durchscrollen sanft von Abschnitt zu Abschnitt das Farbschema. Technik: ein IntersectionObserver setzt beim Eintreten eines Abschnitts ein data-scheme am Container, den Rest erledigt eine CSS-Transition. In der Box scrollen.

Schritt 1

Anfrage

Sie schildern Ihr Vorhaben — unverbindlich.

Schritt 2

Planung

Wir entwickeln die passende Lösung.

Schritt 3

Umsetzung

Saubere Ausführung, fester Termin.

Fertig

Übergabe

Abnahme und Garantie inklusive.

<div class="scw" data-scw tabindex="0" aria-label="Farbwechsel beim Scrollen">
  <div class="scw-panel" data-scheme="1"><span class="n">Schritt 1</span><h4>Anfrage</h4><p>…</p></div>
  <div class="scw-panel" data-scheme="2"><span class="n">Schritt 2</span><h4>Planung</h4><p>…</p></div>
  <div class="scw-panel" data-scheme="3"><span class="n">Schritt 3</span><h4>Umsetzung</h4><p>…</p></div>
  <div class="scw-panel" data-scheme="4"><span class="n">Fertig</span><h4>Übergabe</h4><p>…</p></div>
</div>

<style>
.scw{width:100%;max-width:340px;height:260px;overflow-y:auto;border-radius:14px;border:1px solid #26262c;
  scrollbar-width:thin;background:#16161a;color:#e9e9ee;transition:background-color .6s ease,color .6s ease}
.scw[data-scheme="2"]{background:#15292a;color:#dff3ee}
.scw[data-scheme="3"]{background:#291826;color:#f3dfee}
.scw[data-scheme="4"]{background:var(--akzent,#7c87ff);color:#0f0f12}
.scw-panel{min-height:220px;display:grid;place-items:center;padding:1.4rem;text-align:center;gap:.35rem}
.scw-panel .n{font-size:.68rem;font-weight:700;letter-spacing:.2em;text-transform:uppercase;opacity:.7}
.scw-panel h4{margin:.1rem 0 .2rem;font-size:1.15rem}
.scw-panel p{margin:0;font-size:.82rem;opacity:.85;max-width:26ch}
@media (prefers-reduced-motion:reduce){.scw{transition:none}}
</style>

<script>
document.querySelectorAll('[data-scw]').forEach(function(box){
  if(!('IntersectionObserver' in window)) return;
  var io = new IntersectionObserver(function(entries){
    entries.forEach(function(e){
      if(e.isIntersecting) box.setAttribute('data-scheme', e.target.getAttribute('data-scheme'));
    });
  }, { root: box, threshold: 0.6 });
  box.querySelectorAll('.scw-panel').forEach(function(p){ io.observe(p); });
});
</script>

Pin & Scrub — gepinntes Medium

Pol 1 · Craft Anlass: Produkt-/Ablauf-Story

Ein Medium bleibt beim Scrollen oben „gepinnt", während daneben Text-Kapitel vorbeiziehen — das aktive Kapitel schaltet synchron den gezeigten Medien-Zustand um (Cross-Fade). Anders als der Sticky-Stack (Karten stapeln) und der Horizontal-Scroll (seitwärts): hier bleibt EIN Medium fix und der Inhalt wechselt dazu. In der Box scrollen.

Kapitel 1

Aufmaß vor Ort

Wir nehmen exakt Maß — die Basis für alles Weitere.

Kapitel 2

Fertigung

Passgenaue Vorfertigung in der Werkstatt.

Kapitel 3

Montage

Sauberer Einbau, termingerecht übergeben.

<div class="psc" data-psc tabindex="0" aria-label="Pin-und-Scrub-Demo">
  <div class="psc-media" aria-hidden="true">
    <div class="psc-layer l1 is-on">01</div>
    <div class="psc-layer l2">02</div>
    <div class="psc-layer l3">03</div>
  </div>
  <div class="psc-step is-active" data-i="0"><span class="n">Kapitel 1</span><h4>Aufmaß vor Ort</h4><p>…</p></div>
  <div class="psc-step" data-i="1"><span class="n">Kapitel 2</span><h4>Fertigung</h4><p>…</p></div>
  <div class="psc-step" data-i="2"><span class="n">Kapitel 3</span><h4>Montage</h4><p>…</p></div>
</div>

<style>
.psc{position:relative;width:100%;max-width:340px;height:300px;overflow-y:auto;border-radius:14px;
  border:1px solid #26262c;background:#101014;scrollbar-width:thin}
.psc-media{position:sticky;top:0;z-index:2;height:150px;overflow:hidden;border-bottom:1px solid #26262c}
.psc-layer{position:absolute;inset:0;opacity:0;transition:opacity .5s ease;display:grid;place-items:center;
  color:#fff;font-weight:800;font-size:1.5rem;letter-spacing:-.01em}
.psc-layer.is-on{opacity:1}
.psc-layer.l1{background:linear-gradient(135deg,#3b3b6e,#1a1a2b)}
.psc-layer.l2{background:linear-gradient(135deg,#20514a,#122423)}
.psc-layer.l3{background:linear-gradient(135deg,#5a3a53,#241522)}
.psc-step{min-height:150px;padding:1.2rem 1.35rem;display:flex;flex-direction:column;justify-content:center;
  gap:.35rem;border-bottom:1px solid #1e1e24;border-left:2px solid transparent;transition:border-color .3s ease}
.psc-step:last-child{border-bottom:0}
.psc-step.is-active{border-left-color:var(--akzent,#7c87ff)}
.psc-step .n{font-size:.68rem;font-weight:700;letter-spacing:.16em;text-transform:uppercase;color:var(--akzent,#7c87ff)}
.psc-step h4{margin:0;font-size:1rem;color:#f4f4f5}
.psc-step p{margin:0;font-size:.82rem;color:#a1a1aa}
@media (prefers-reduced-motion:reduce){.psc-layer,.psc-step{transition:none}}
</style>

<script>
document.querySelectorAll('[data-psc]').forEach(function(box){
  var steps = box.querySelectorAll('.psc-step'), layers = box.querySelectorAll('.psc-layer');
  if(!('IntersectionObserver' in window)) return;
  var io = new IntersectionObserver(function(entries){
    entries.forEach(function(e){
      if(!e.isIntersecting) return;
      var i = +e.target.getAttribute('data-i');
      steps.forEach(function(s,k){ s.classList.toggle('is-active', k===i); });
      layers.forEach(function(l,k){ l.classList.toggle('is-on', k===i); });
    });
  }, { root: box, threshold: 0.55 });
  steps.forEach(function(s){ io.observe(s); });
});
</script>

Karten-Reveal: Avatar-Pop + Unschärfe → Scharf

Pol 1 · Craft Anlass: Standort-/Profil-Karte, Auftritt

Beim Erscheinen ploppt der Avatar per Feder-Kurve auf, und die Karten-Infos (Titel + Ort) blenden im gleichen Tempo von unscharf zu scharf ein — beides synchron über ~0,5 s. Ausgelöst per IntersectionObserver (re-triggerbar beim Rein-/Rausscrollen) plus „Abspielen"-Knopf. Bei prefers-reduced-motion sofort der Endzustand. Avatar als Emoji, Karte als CSS-Verlauf — kein externes Kartenmaterial, kein Bild nötig.

<div class="crv-wrap">
  <div class="crv" role="img" aria-label="Standort: Paradiesstraße, Musterstadt">
    <span class="crv-avatar" aria-hidden="true">🙂</span>
    <div class="crv-info"><h4>Paradiesstraße</h4><p>Musterstadt</p></div>
  </div>
  <button type="button" class="crv-replay" data-crv-replay>↻ Abspielen</button>
</div>

<style>
.crv{position:relative;width:300px;max-width:100%;aspect-ratio:1/1;border-radius:26px;overflow:hidden;
  background:radial-gradient(120% 90% at 80% 15%,#2a4a6e,transparent 55%),linear-gradient(160deg,#1c3a52,#16303f 45%,#123047);
  border:1px solid #26303c;box-shadow:0 24px 60px -30px rgba(0,0,0,.8)}
.crv-avatar{position:absolute;top:46%;left:50%;width:104px;height:104px;border-radius:50%;z-index:2;
  display:grid;place-items:center;font-size:3rem;background:#f2e9dc;border:4px solid var(--akzent,#7c87ff);
  transform:translate(-50%,-50%) scale(0);transition:transform .5s cubic-bezier(.34,1.56,.64,1)}
.crv-info{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:22px;
  background:linear-gradient(0deg,rgba(9,13,19,.9),transparent);
  opacity:0;filter:blur(9px);transform:translateY(8px);
  transition:opacity .5s ease,filter .5s ease,transform .5s ease}
.crv-info h4{margin:0;font-size:1.5rem;font-weight:800;color:#fff}
.crv-info p{margin:.1rem 0 0;font-size:.95rem;color:#c9d4e0}
.crv.is-on .crv-avatar{transform:translate(-50%,-50%) scale(1)}
.crv.is-on .crv-info{opacity:1;filter:blur(0);transform:none}
@media (prefers-reduced-motion:reduce){.crv-avatar,.crv-info{transition:none}
  .crv .crv-avatar{transform:translate(-50%,-50%) scale(1)}.crv .crv-info{opacity:1;filter:none;transform:none}}
</style>

<script>
(function(){
  var cards = document.querySelectorAll('.crv');
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  if(reduce || !('IntersectionObserver' in window)){ cards.forEach(function(c){c.classList.add('is-on');}); return; }
  cards.forEach(function(c){
    new IntersectionObserver(function(en){ en.forEach(function(e){ c.classList.toggle('is-on', e.isIntersecting); }); }, { threshold:.6 }).observe(c);
  });
  document.querySelectorAll('[data-crv-replay]').forEach(function(b){
    b.addEventListener('click', function(){ var c=b.closest('.crv-wrap').querySelector('.crv');
      c.classList.remove('is-on'); void c.offsetWidth; c.classList.add('is-on'); });
  });
})();
</script>

Zahl zählt hoch beim Sichtbarwerden

Pol 2 · Klarheit Anlass: Kennzahlen / Erfolge

Zahlen zählen von 0 auf ihren Zielwert, sobald die Karte in den Blick scrollt — je Wert per data-to, mit optionalem Prä-/Suffix (z. B. „+" oder „%"). Ein IntersectionObserver startet das Zählen einmalig; das Ergebnis steht per textContent auch ohne Animation korrekt da. Bei reduzierter Bewegung wird der Zielwert sofort gesetzt. In der Box scrollen ist nicht nötig — die Karten liegen im Blickfeld.

+0
Projekte
0%
Weiterempfehlung
0
Jahre Erfahrung
<div class="fxcu" data-fxcu>
  <div class="fxcu-stat">
    <div class="fxcu-num"><span class="pre">+</span><span class="fxcu-val" data-to="1240">0</span></div>
    <div class="fxcu-label">Projekte</div>
  </div>
  <div class="fxcu-stat">
    <div class="fxcu-num"><span class="fxcu-val" data-to="98">0</span><span class="suf">%</span></div>
    <div class="fxcu-label">Weiterempfehlung</div>
  </div>
  <div class="fxcu-stat">
    <div class="fxcu-num"><span class="fxcu-val" data-to="25">0</span></div>
    <div class="fxcu-label">Jahre Erfahrung</div>
  </div>
</div>

<style>
.fxcu{display:flex;flex-wrap:wrap;gap:12px;width:100%;justify-content:center}
.fxcu-stat{flex:1 1 120px;min-width:0;background:#17171b;border:1px solid #26262c;border-radius:14px;
  padding:1.1rem 1rem;text-align:center}
.fxcu-num{display:flex;align-items:baseline;justify-content:center;gap:.1em;
  font-size:2rem;font-weight:800;line-height:1;color:#f4f4f5;font-variant-numeric:tabular-nums}
.fxcu-num .pre,.fxcu-num .suf{color:var(--akzent,#7c87ff);font-size:1.3rem;font-weight:700}
.fxcu-label{margin-top:.55rem;font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:#a1a1aa}
.fxcu-stat{opacity:0;transform:translateY(14px);transition:opacity .5s ease,transform .5s ease}
.fxcu-stat.is-in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){.fxcu-stat{transition:none;opacity:1;transform:none}}
</style>

<script>
(function(){
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  function countUp(el, to){
    if(reduce){ el.textContent = to.toLocaleString('de-DE'); return; }
    var dur = 1400, start = 0, t0 = 0;
    function step(t){
      if(!t0) t0 = t;
      var p = Math.min((t - t0)/dur, 1);
      var eased = 1 - Math.pow(1 - p, 3);                 /* easeOutCubic */
      el.textContent = Math.round(start + (to-start)*eased).toLocaleString('de-DE');
      if(p < 1) requestAnimationFrame(step);
    }
    requestAnimationFrame(step);
  }
  document.querySelectorAll('[data-fxcu]').forEach(function(group){
    var stats = group.querySelectorAll('.fxcu-stat');
    if(!('IntersectionObserver' in window)){
      stats.forEach(function(s){
        s.classList.add('is-in');
        var v = s.querySelector('.fxcu-val'); if(v) v.textContent = (+v.dataset.to).toLocaleString('de-DE');
      });
      return;
    }
    var io = new IntersectionObserver(function(entries, obs){
      entries.forEach(function(e){
        if(!e.isIntersecting) return;
        e.target.classList.add('is-in');
        var v = e.target.querySelector('.fxcu-val');
        if(v) countUp(v, +v.dataset.to);
        obs.unobserve(e.target);                          /* nur einmal */
      });
    }, { threshold: 0.5 });
    stats.forEach(function(s){ io.observe(s); });
  });
})();
</script>

Typewriter mit Wort-Rotation

Pol 1 · Craft Anlass: Hero-Headline / Claim

Ein fester Satzanfang, dahinter tippt sich ein wechselndes Wort Zeichen für Zeichen, hält kurz, löscht sich wieder und wechselt zum nächsten Begriff — mit blinkendem Cursor. Wörter kommen aus einem data-words-Attribut (kommagetrennt). Bei reduzierter Bewegung steht das erste Wort still, der Cursor blinkt nicht. Starttext ist im Markup vorhanden (Fallback ohne JS).

Wir bauen  Küchen

<p class="fxtw">
  <span class="fxtw-lead">Wir bauen </span>
  <span class="fxtw-type" data-words="Küchen,Bäder,Böden,Terrassen">Küchen</span><span class="fxtw-caret" aria-hidden="true"></span>
</p>

<style>
.fxtw{font-size:1.15rem;line-height:1.5;color:#e9e9ee;font-weight:600}
.fxtw-lead{color:#a1a1aa;font-weight:500}
.fxtw-type{color:var(--akzent,#7c87ff);font-weight:800}
.fxtw-caret{display:inline-block;width:2px;height:1.05em;margin-left:2px;vertical-align:-2px;
  background:var(--akzent,#7c87ff);animation:fxtwBlink 1s step-end infinite}
@keyframes fxtwBlink{0%,100%{opacity:1}50%{opacity:0}}
@media (prefers-reduced-motion:reduce){.fxtw-caret{animation:none;opacity:1}}
</style>

<script>
(function(){
  var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches;
  document.querySelectorAll('.fxtw-type').forEach(function(el){
    var words = (el.dataset.words || '').split(',').map(function(w){ return w.trim(); }).filter(Boolean);
    if(words.length < 2 || reduce){ el.textContent = words[0] || el.textContent; return; }
    var wi = 0, ci = words[0].length, deleting = false;
    el.textContent = words[0];
    function tick(){
      var word = words[wi];
      if(!deleting){
        ci++;
        el.textContent = word.slice(0, ci);
        if(ci >= word.length){ deleting = true; return setTimeout(tick, 1300); }  /* Wort steht */
        setTimeout(tick, 90);
      } else {
        ci--;
        el.textContent = word.slice(0, ci);
        if(ci <= 0){ deleting = false; wi = (wi+1)%words.length; return setTimeout(tick, 240); }
        setTimeout(tick, 45);
      }
    }
    setTimeout(tick, 1300);
  });
})();
</script>

Auto-Fade-Bildwechsel

Pol 2 · Klarheit Anlass: Hero-Bild / Stimmungswechsel

Ein Bild blendet automatisch ins nächste über — mit leichtem, langsamem Heranzoomen (Ken-Burns). Dezente Punkte springen direkt zu einem Bild. Reiner Cross-Fade auf EINEM Standbild, ohne Titel-Staffel, Timer-Leiste oder Vorschau-Kacheln — dadurch klar getrennt vom Story-Cardslider. Bilder hier als CSS-Verlauf angedeutet (im Projekt background-image je Slide setzen). Auto-Wechsel pausiert bei Hover/Fokus, bei reduzierter Bewegung kein Zoom.

<div class="fxff" data-fxff role="group" aria-roledescription="Diashow" aria-label="Stimmungsbilder">
  <!-- je Slide statt Verlauf-Klasse f1..f3 ein background-image setzen -->
  <div class="fxff-slide f1 is-on" role="img" aria-label="Bild 1"><span class="fxff-cap">Wohnküche</span></div>
  <div class="fxff-slide f2" role="img" aria-label="Bild 2"><span class="fxff-cap">Badezimmer</span></div>
  <div class="fxff-slide f3" role="img" aria-label="Bild 3"><span class="fxff-cap">Wohnraum</span></div>
  <div class="fxff-dots"></div>  <!-- Punkte per JS -->
</div>

<style>
.fxff{position:relative;width:100%;max-width:360px;aspect-ratio:16/10;margin:0 auto;border-radius:16px;
  overflow:hidden;border:1px solid #26262c;background:#101014;isolation:isolate}
.fxff-slide{position:absolute;inset:0;opacity:0;transform:scale(1.02);
  transition:opacity 1s ease,transform 6s ease;background-size:cover;background-position:center}
.fxff-slide.is-on{opacity:1;transform:scale(1.09)}
.fxff-slide::after{content:"";position:absolute;inset:0;
  background:linear-gradient(0deg,rgba(8,8,12,.72),transparent 55%)}
.fxff-slide.f1{background-image:linear-gradient(135deg,#2b2b52,#12121e)}
.fxff-slide.f2{background-image:linear-gradient(135deg,#1f4a44,#101f1d)}
.fxff-slide.f3{background-image:linear-gradient(135deg,#4a2c47,#1c1220)}
.fxff-cap{position:absolute;left:1rem;bottom:.9rem;z-index:2;font-size:.85rem;font-weight:700;color:#f4f4f5;
  text-shadow:0 1px 8px rgba(0,0,0,.7)}
.fxff-dots{position:absolute;right:1rem;bottom:1rem;z-index:3;display:flex;gap:.45rem}
.fxff-dots button{width:9px;height:9px;padding:0;border:1px solid rgba(255,255,255,.6);border-radius:50%;
  background:transparent;cursor:pointer;transition:background .25s ease,transform .25s ease}
.fxff-dots button.is-on{background:#fff;transform:scale(1.25)}
.fxff-dots button:focus-visible{outline:2px solid #fff;outline-offset:2px}
@media (prefers-reduced-motion:reduce){
  .fxff-slide{transition:opacity .4s ease;transform:none}
  .fxff-slide.is-on{transform:none}
  .fxff-dots button{transition:none}
}
</style>

<script>
document.querySelectorAll('[data-fxff]').forEach(function(box){
  var slides = Array.prototype.slice.call(box.querySelectorAll('.fxff-slide'));
  var dotsWrap = box.querySelector('.fxff-dots');
  var n = slides.length; if(n < 2) return;
  var cur = 0, timer = 0, paused = false;
  var dots = slides.map(function(_, i){
    var b = document.createElement('button');
    b.type = 'button'; b.setAttribute('aria-label','Zu Bild '+(i+1));
    b.addEventListener('click', function(){ show(i); reset(); });
    if(dotsWrap) dotsWrap.appendChild(b);
    return b;
  });
  function show(i){
    cur = (i%n+n)%n;
    slides.forEach(function(s,k){ s.classList.toggle('is-on', k===cur); });
    dots.forEach(function(d,k){ d.classList.toggle('is-on', k===cur); });
  }
  function reset(){
    clearInterval(timer);
    timer = setInterval(function(){ if(!paused) show(cur+1); }, 4500);
  }
  box.addEventListener('mouseenter', function(){ paused = true; });
  box.addEventListener('mouseleave', function(){ paused = false; });
  box.addEventListener('focusin', function(){ paused = true; });
  box.addEventListener('focusout', function(){ paused = false; });
  show(0); reset();
});
</script>

Gestaffelte Checkliste mit Haken-Zeichnung

Pol 2 · Klarheit Anlass: Leistungs-Haken / Vorteile

Listenpunkte schieben sich beim Reinscrollen nacheinander von links herein, und der Haken im Kreis wird per stroke-dashoffset „gezeichnet". Der Versatz kommt allein aus per-Item-Verzögerung im IntersectionObserver — kein Timeline-Feature nötig. Anders als der allgemeine Scroll-Reveal (#1): hier eine echte Checklisten-Semantik mit gezeichnetem SVG-Haken. Bei reduzierter Bewegung sofort fertig. In der Box scrollen.

↓ scrollen
  • Fester Ansprechpartner
  • Verbindlicher Festpreis
  • Termintreue Umsetzung
  • Sauberer Abschluss
<ul class="fxck" data-fxck>
  <li class="fxck-item"><span class="fxck-mark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg></span>Fester Ansprechpartner</li>
  <li class="fxck-item"><span class="fxck-mark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg></span>Verbindlicher Festpreis</li>
  <li class="fxck-item"><span class="fxck-mark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg></span>Termintreue Umsetzung</li>
  <li class="fxck-item"><span class="fxck-mark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 13l4 4L19 7"/></svg></span>Sauberer Abschluss</li>
</ul>

<style>
.fxck{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.6rem}
.fxck-item{display:flex;align-items:center;gap:.7rem;padding:.7rem .85rem;border:1px solid #26262c;
  border-radius:12px;background:#17171b;color:#e9e9ee;font-size:.9rem;
  opacity:0;transform:translateX(-16px);transition:opacity .5s ease,transform .5s ease,border-color .3s ease}
.fxck-item.is-in{opacity:1;transform:none}
.fxck-item:hover,.fxck-item:focus-within{border-color:var(--akzent,#7c87ff)}
.fxck-mark{flex:0 0 auto;width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
  border:1px solid #26262c;background:#101014}
.fxck-mark svg{width:15px;height:15px;stroke:var(--akzent,#7c87ff);stroke-width:2.4;fill:none;
  stroke-linecap:round;stroke-linejoin:round;
  stroke-dasharray:24;stroke-dashoffset:24;transition:stroke-dashoffset .45s ease .2s}
.fxck-item.is-in .fxck-mark svg{stroke-dashoffset:0}
@media (prefers-reduced-motion:reduce){
  .fxck-item{transition:border-color .3s ease;opacity:1;transform:none}
  .fxck-mark svg{transition:none;stroke-dashoffset:0}
}
</style>

<script>
document.querySelectorAll('[data-fxck]').forEach(function(list){
  var items = Array.prototype.slice.call(list.querySelectorAll('.fxck-item'));
  if(!('IntersectionObserver' in window)){ items.forEach(function(it){ it.classList.add('is-in'); }); return; }
  var io = new IntersectionObserver(function(entries){
    entries.forEach(function(e){
      var i = items.indexOf(e.target);
      if(e.isIntersecting){
        setTimeout(function(){ e.target.classList.add('is-in'); }, i>-1 ? i*130 : 0);  /* Staffel */
      } else {
        e.target.classList.remove('is-in');                                            /* re-triggerbar */
      }
    });
  }, { threshold: 0.6 });                     /* in einer Vollseite: kein root nötig */
  items.forEach(function(it){ io.observe(it); });
});
</script>