Update29082025

This commit is contained in:
astosic 2025-08-29 19:13:51 +02:00
parent 463e7fa1ea
commit 2f9aa2adac
15 changed files with 817 additions and 513 deletions

View file

@ -75,51 +75,63 @@
{{ end }}
</div>
<!-- Intro-Text rechts + USP darunter -->
<!-- Intro-Text rechts -->
<div class="col-lg-6 order-2 order-lg-2" data-reveal>
{{ if .Content }}
<div class="service-hero__text">
{{ .Content }}
{{ with .Params.usp }}
<div class="usp-inline" role="list">
{{ range $i, $u := . }}
<div class="usp-item" role="listitem">
<div class="usp-icon" aria-hidden="true">
{{ if eq (mod $i 3) 0 }}
<!-- Shield -->
<svg viewBox="0 0 24 24" role="img" focusable="false">
<path d="M12 3l7 3v6c0 4.97-3.58 9.43-7 10-3.42-.57-7-5.03-7-10V6l7-3z" fill="currentColor" opacity=".2"/>
<path d="M12 3l7 3v6c0 4.97-3.58 9.43-7 10-3.42-.57-7-5.03-7-10V6l7-3z" fill="none" stroke="currentColor" stroke-width="1.5"/>
</svg>
{{ else if eq (mod $i 3) 1 }}
<!-- Badge -->
<svg viewBox="0 0 24 24" role="img" focusable="false">
<circle cx="12" cy="9" r="5" fill="currentColor" opacity=".2"/>
<circle cx="12" cy="9" r="5" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 14l-2 7 6-3 6 3-2-7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>
{{ else }}
<!-- Target -->
<svg viewBox="0 0 24 24" role="img" focusable="false">
<circle cx="12" cy="12" r="9" fill="currentColor" opacity=".15"/>
<circle cx="12" cy="12" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="2" fill="currentColor"/>
</svg>
{{ end }}
</div>
<p class="usp-text">{{ $u }}</p>
</div>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>
<!-- Eigenständiger Container für USPs unterhalb der Text-/Bild-Reihe -->
{{ with .Params.usp }}
<div class="usp-container" style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
justify-items: center;
gap: 2rem;
margin-top: 2rem;
text-align: center;
">
{{ range $i, $u := . }}
<div class="usp-item" style="display:flex; flex-direction:column; align-items:center;">
<div class="usp-icon" aria-hidden="true">
{{ if eq (mod $i 3) 0 }}
<!-- Shield -->
<svg viewBox="0 0 24 24" role="img" focusable="false">
<path d="M12 3l7 3v6c0 4.97-3.58 9.43-7 10-3.42-.57-7-5.03-7-10V6l7-3z" fill="currentColor" opacity=".2"/>
<path d="M12 3l7 3v6c0 4.97-3.58 9.43-7 10-3.42-.57-7-5.03-7-10V6l7-3z" fill="none" stroke="currentColor" stroke-width="1.5"/>
</svg>
{{ else if eq (mod $i 3) 1 }}
<!-- Badge -->
<svg viewBox="0 0 24 24" role="img" focusable="false">
<circle cx="12" cy="9" r="5" fill="currentColor" opacity=".2"/>
<circle cx="12" cy="9" r="5" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M8 14l-2 7 6-3 6 3-2-7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
</svg>
{{ else }}
<!-- Target -->
<svg viewBox="0 0 24 24" role="img" focusable="false">
<circle cx="12" cy="12" r="9" fill="currentColor" opacity=".15"/>
<circle cx="12" cy="12" r="6" fill="none" stroke="currentColor" stroke-width="1.5"/>
<circle cx="12" cy="12" r="2" fill="currentColor"/>
</svg>
{{ end }}
</div>
<p class="usp-text" style="text-align:center;">{{ $u | markdownify }}</p>
</div>
{{ end }}
</div>
{{ end }}
</div>
</section>
<!-- =========================
FEATURES CAROUSEL (Große Cards mit Auto-Play)
Quelle: .Params.features (array of {title, text, icon?})
@ -143,9 +155,6 @@
.sf-title {
text-align: center;
margin: 0 0 40px;
font-size: 2rem;
font-weight: 700;
color: #333;
}
/* Viewport & Track */
@ -171,7 +180,6 @@
.sf-card-inner {
background: #fff;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.08);
padding: 40px 30px;
height: 100%;
min-height: 320px;
@ -210,16 +218,11 @@
/* Content */
.sf-card h3 {
margin: 0 0 16px;
font-size: 1.5rem;
font-weight: 700;
color: #333;
line-height: 1.3;
}
.sf-card p {
margin: 0;
font-size: 1rem;
color: #666;
line-height: 1.6;
}
@ -401,234 +404,155 @@
</div>
</div>
<script>
(function() {
const carousel = document.getElementById('sfCarousel');
if (!carousel) return;
const track = document.getElementById('sfTrack');
const cards = track.querySelectorAll('.sf-card');
const dots = carousel.querySelectorAll('.sf-dot');
const prevBtn = carousel.querySelector('.sf-arrow.prev');
const nextBtn = carousel.querySelector('.sf-arrow.next');
const progress = carousel.querySelector('.sf-progress');
let currentIndex = 0;
let autoplayInterval;
let touchStartX = 0;
let touchEndX = 0;
let isDragging = false;
const AUTOPLAY_DELAY = 8000; // 8 seconds
// Clone first and last cards for infinite loop
function setupInfiniteLoop() {
const firstClone = cards[0].cloneNode(true);
const lastClone = cards[cards.length - 1].cloneNode(true);
firstClone.setAttribute('aria-hidden', 'true');
lastClone.setAttribute('aria-hidden', 'true');
track.appendChild(firstClone);
track.insertBefore(lastClone, cards[0]);
}
// Update carousel position
function updateCarousel(animate = true) {
const translateX = -(currentIndex + 1) * 100; // +1 because of the cloned card at the beginning
if (animate) {
track.style.transition = 'transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
} else {
track.style.transition = 'none';
}
track.style.transform = `translateX(${translateX}%)`;
// Update dots
const realIndex = currentIndex % cards.length;
dots.forEach((dot, index) => {
dot.classList.toggle('active', index === realIndex);
dot.setAttribute('aria-selected', index === realIndex);
});
// Remove disabled state from buttons for infinite loop
prevBtn.disabled = false;
nextBtn.disabled = false;
}
// Handle infinite loop transitions
function handleInfiniteTransition() {
track.addEventListener('transitionend', () => {
if (currentIndex === cards.length) {
currentIndex = 0;
updateCarousel(false);
} else if (currentIndex === -1) {
currentIndex = cards.length - 1;
updateCarousel(false);
}
});
}
// Go to specific slide
function goToSlide(index) {
currentIndex = index;
updateCarousel();
resetAutoplay();
}
// Next slide
function nextSlide() {
currentIndex++;
if (currentIndex > cards.length) {
updateCarousel();
setTimeout(() => {
currentIndex = 0;
updateCarousel(false);
}, 500);
} else {
updateCarousel();
}
}
// Previous slide
function prevSlide() {
currentIndex--;
if (currentIndex < -1) {
updateCarousel();
setTimeout(() => {
currentIndex = cards.length - 1;
updateCarousel(false);
}, 500);
} else {
updateCarousel();
}
}
// Autoplay functionality
function startAutoplay() {
carousel.classList.add('autoplay');
progress.style.transition = 'none';
progress.style.width = '0';
setTimeout(() => {
progress.style.transition = `width ${AUTOPLAY_DELAY}ms linear`;
progress.style.width = '100%';
}, 10);
autoplayInterval = setInterval(nextSlide, AUTOPLAY_DELAY);
}
function stopAutoplay() {
carousel.classList.remove('autoplay');
clearInterval(autoplayInterval);
progress.style.transition = 'none';
progress.style.width = '0';
}
function resetAutoplay() {
stopAutoplay();
startAutoplay();
}
// Touch/Drag handling
function handleTouchStart(e) {
touchStartX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
isDragging = true;
carousel.classList.add('is-dragging');
if (e.type.includes('mouse')) {
e.preventDefault();
}
stopAutoplay();
}
function handleTouchMove(e) {
if (!isDragging) return;
if (e.type.includes('mouse')) {
e.preventDefault();
}
}
function handleTouchEnd(e) {
if (!isDragging) return;
isDragging = false;
carousel.classList.remove('is-dragging');
touchEndX = e.type.includes('mouse') ? e.clientX : (e.changedTouches[0]?.clientX || touchStartX);
handleSwipe();
resetAutoplay();
}
function handleSwipe() {
const swipeThreshold = 50;
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
nextSlide();
} else {
prevSlide();
}
}
}
// Event listeners
prevBtn.addEventListener('click', () => {
prevSlide();
resetAutoplay();
});
nextBtn.addEventListener('click', () => {
nextSlide();
resetAutoplay();
});
// Dot navigation
<script>
(function () {
const carousel = document.getElementById('sfCarousel');
if (!carousel) return;
const track = document.getElementById('sfTrack');
const cards = track.querySelectorAll('.sf-card');
const dots = carousel.querySelectorAll('.sf-dot');
const prevBtn = carousel.querySelector('.sf-arrow.prev');
const nextBtn = carousel.querySelector('.sf-arrow.next');
const progress = carousel.querySelector('.sf-progress');
let currentIndex = 0;
let autoplayInterval;
let touchStartX = 0;
let touchEndX = 0;
let isDragging = false;
let isAnimating = false; // blockiert neue Aktionen
const AUTOPLAY_DELAY = 20000;
function setupInfiniteLoop() {
const firstClone = cards[0].cloneNode(true);
const lastClone = cards[cards.length - 1].cloneNode(true);
firstClone.setAttribute('aria-hidden', 'true');
lastClone.setAttribute('aria-hidden', 'true');
track.appendChild(firstClone);
track.insertBefore(lastClone, cards[0]);
}
function updateCarousel(animate = true) {
const translateX = -(currentIndex + 1) * 100;
track.style.transition = animate
? 'transform 0.5s cubic-bezier(0.4, 0, 0.2, 1)'
: 'none';
track.style.transform = `translateX(${translateX}%)`;
const realIndex = (currentIndex + cards.length) % cards.length;
dots.forEach((dot, index) => {
dot.addEventListener('click', () => {
goToSlide(index);
});
dot.classList.toggle('active', index === realIndex);
dot.setAttribute('aria-selected', index === realIndex);
});
// Touch events
track.addEventListener('touchstart', handleTouchStart, { passive: true });
track.addEventListener('touchmove', handleTouchMove, { passive: true });
track.addEventListener('touchend', handleTouchEnd, { passive: true });
// Mouse events for desktop drag
track.addEventListener('mousedown', handleTouchStart);
track.addEventListener('mousemove', handleTouchMove);
track.addEventListener('mouseup', handleTouchEnd);
track.addEventListener('mouseleave', () => {
if (isDragging) {
handleTouchEnd({ type: 'mouse', clientX: touchStartX });
}
});
// Keyboard navigation
carousel.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
prevSlide();
resetAutoplay();
}
if (e.key === 'ArrowRight') {
nextSlide();
resetAutoplay();
}
});
// Pause on hover (optional - can be removed if you want continuous autoplay)
carousel.addEventListener('mouseenter', stopAutoplay);
carousel.addEventListener('mouseleave', startAutoplay);
// Initialize
setupInfiniteLoop();
handleInfiniteTransition();
// Buttons während Animation blockieren
prevBtn.disabled = animate;
nextBtn.disabled = animate;
}
track.addEventListener('transitionend', () => {
if (currentIndex === cards.length) currentIndex = 0;
if (currentIndex === -1) currentIndex = cards.length - 1;
updateCarousel(false);
isAnimating = false;
prevBtn.disabled = false;
nextBtn.disabled = false;
});
function goToSlide(index) {
if (isAnimating) return;
isAnimating = true;
currentIndex = index;
updateCarousel();
resetAutoplay();
}
function nextSlide() {
if (isAnimating) return;
isAnimating = true;
currentIndex++;
updateCarousel();
}
function prevSlide() {
if (isAnimating) return;
isAnimating = true;
currentIndex--;
updateCarousel();
}
function startAutoplay() {
carousel.classList.add('autoplay');
progress.style.transition = 'none';
progress.style.width = '0%';
requestAnimationFrame(() => {
progress.style.transition = `width ${AUTOPLAY_DELAY}ms linear`;
progress.style.width = '100%';
});
autoplayInterval = setInterval(nextSlide, AUTOPLAY_DELAY);
}
function stopAutoplay() {
carousel.classList.remove('autoplay');
clearInterval(autoplayInterval);
progress.style.transition = 'none';
progress.style.width = '0%';
}
function resetAutoplay() {
stopAutoplay();
startAutoplay();
})();
</script>
}
// Touch/Swipe
function handleTouchStart(e) {
if (isAnimating) return;
touchStartX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
isDragging = true;
carousel.classList.add('is-dragging');
stopAutoplay();
}
function handleTouchEnd(e) {
if (!isDragging) return;
isDragging = false;
carousel.classList.remove('is-dragging');
touchEndX = e.type.includes('mouse')
? e.clientX
: e.changedTouches[0]?.clientX || touchStartX;
handleSwipe();
resetAutoplay();
}
function handleSwipe() {
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > 50) diff > 0 ? nextSlide() : prevSlide();
}
// Events
prevBtn.addEventListener('click', () => prevSlide());
nextBtn.addEventListener('click', () => nextSlide());
dots.forEach((dot, i) => dot.addEventListener('click', () => goToSlide(i)));
track.addEventListener('touchstart', handleTouchStart, { passive: true });
track.addEventListener('touchend', handleTouchEnd, { passive: true });
track.addEventListener('mousedown', handleTouchStart);
track.addEventListener('mouseup', handleTouchEnd);
carousel.addEventListener('mouseenter', stopAutoplay);
carousel.addEventListener('mouseleave', startAutoplay);
carousel.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') prevSlide();
if (e.key === 'ArrowRight') nextSlide();
});
setupInfiniteLoop();
updateCarousel(false);
startAutoplay();
})();
</script>
</section>
{{ end }}
@ -654,14 +578,14 @@
<!-- Text links -->
<div class="col-lg-6" data-reveal>
<h3 class="mb-3">Ergebnis & Mehrwert</h3>
<h2 class="mb-3">Ergebnis & Mehrwert</h2>
<ul class="checklist">
{{ range . }}
<li data-reveal>
<svg class="check" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true">
<path d="M20 6L9 17l-5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span>{{ . }}</span>
<span>{{ . | markdownify }}</span>
</li>
{{ end }}
</ul>