`;
I('zAgain').onclick = start; return; }
requestAnimationFrame(step);
}
function start() { items = []; pops = []; score = 0; spawn = 0; over.style.display = 'none'; running = true; last = performance.now(); end = last + 30000; requestAnimationFrame(step); }
I('zStart').onclick = start;
})();
/* ---------- QUIZ ---------- */
(() => {
const Q = [
['Die Hausbank hat automatisch die besten Konditionen.', 0, 'Das muss nicht sein. Konditionen unterscheiden sich von Bank zu Bank, deshalb lohnt sich ein Vergleich.'],
['Ein Darlehen mit langer Zinsbindung kann man nach 10 Jahren mit einer Frist von 6 Monaten kündigen.', 1, 'Stimmt. Das gesetzliche Kündigungsrecht nach § 489 BGB gilt zehn Jahre nach vollständigem Empfang des Darlehens.'],
['Ohne Eigenkapital ist eine Baufinanzierung unmöglich.', 0, 'Nicht unbedingt. Je nach Bonität sind auch Finanzierungen mit wenig Eigenkapital möglich, meist aber zu einem höheren Zins.'],
['In Bayern beträgt die Grunderwerbsteuer 3,5 %.', 1, 'Richtig. Bayern hat mit 3,5 % den niedrigsten Satz in Deutschland.'],
['Sondertilgungen sind immer kostenlos möglich.', 0, 'Nur, wenn sie im Darlehensvertrag vereinbart sind. Darauf achten wir beim Vergleich.'],
['Meine Beratung bei Yasin kostet nichts.', 1, 'Fakt. Die Beratung ist unverbindlich und komplett kostenlos, und du hast immer das letzte Wort.']];
let i = 0, ok = 0; const box = I('quiz'), prog = I('qProg');
prog.innerHTML = Q.map(() => '
').join('');
function show() {
$$('#qProg i').forEach((el, k) => el.classList.toggle('cur', k === i));
if (i >= Q.length) { box.innerHTML = `
Ergebnis ${ok} von ${Q.length} richtig. ${ok >= 5 ? 'Du kennst dich aus!' : ok >= 3 ? 'Gut gemacht!' : 'Jetzt weißt du mehr als vorher.'}
`;
I('qAgain').onclick = () => { i = 0; ok = 0; $$('#qProg i').forEach(e => e.className = ''); I('qScore').textContent = '0 / ' + Q.length; show(); }; return; }
const [s] = Q[i];
box.innerHTML = `
Aussage ${i + 1} von ${Q.length} ${s}
Mythos Fakt
`;
$$('.qbtns button', box).forEach(b => b.onclick = () => answer(+b.dataset.v));
}
function answer(v) { const [, f, e] = Q[i], right = v === f; if (right) ok++; $$('#qProg i')[i].className = right ? 'ok' : 'no'; I('qScore').textContent = ok + ' / ' + Q.length;
const c = $('.qcard', box); c.querySelector('.qbtns').outerHTML = `
${right ? 'Richtig!' : 'Nicht ganz.'} Das ist ein ${f ? 'Fakt' : 'Mythos'}. ${e}
${i + 1 < Q.length ? 'Nächste Aussage' : 'Ergebnis ansehen'} `;
I('qNext').onclick = () => { i++; show(); }; }
show();
})();
/* ---------- CHAT TAKEOVER ---------- */
$$('.ct').forEach(ct => {
if (RM || !hasG) return; const msgs = $$('.ct-m', ct); gsap.set(msgs, { display: 'none' });
ScrollTrigger.create({ trigger: ct, start: 'top 75%', once: true, onEnter: () => { const tl = gsap.timeline();
msgs.forEach(m => { const out = m.dataset.side === 'out', ty = document.createElement('div'); ty.className = 'ct-typing' + (out ? ' out' : ''); ty.style.color = out ? '#fff' : '#0d1b2e'; ty.innerHTML = '
'; ty.setAttribute('aria-hidden', 'true');
tl.call(() => m.before(ty)).to({}, { duration: Math.min(1.2, .35 + m.textContent.length * .012) }).call(() => { ty.remove(); gsap.set(m, { display: 'block' }); })
.from(m, { scale: .85, opacity: 0, y: 8, duration: .35, ease: 'back.out(1.8)' }).to({}, { duration: .25 }); });
tl.call(() => ScrollTrigger.refresh()); } });
});
/* ---------- CHECKLIST POP ---------- */
$$('.cl li').forEach(li => { const p = li.querySelector('path'), l = p.getTotalLength(); p.style.strokeDasharray = l; p.style.strokeDashoffset = RM || !hasG ? 0 : l; });
if (hasG && !RM) ScrollTrigger.batch('.cl li', { start: 'top 88%', once: true, onEnter: batch => { const tl = gsap.timeline();
batch.forEach((li, k) => tl.from(li, { x: -18, opacity: 0, duration: .5, ease: 'expo.out' }, k * .12).from(li.querySelector('circle'), { scale: .3, duration: .5, ease: 'back.out(2.4)' }, k * .12 + .05).to(li.querySelector('path'), { strokeDashoffset: 0, duration: .35, ease: 'power2.out' }, k * .12 + .25)); } });
/* ---------- RADAR: Projekte nach Richtung und Entfernung ---------- */
(() => {
const HOME = [48.268, 10.889];
const C = [['Bobingen', 48.271, 10.835], ['Wehringen', 48.25, 10.80], ['Großaitingen', 48.228, 10.78], ['Augsburg', 48.371, 10.898], ['Göggingen', 48.34, 10.87], ['Graben', 48.19, 10.83], ['Lagerlechfeld', 48.214, 10.856], ['Untermeitingen', 48.16, 10.80], ['Schwabmünchen', 48.18, 10.76], ['Dinkelscherben', 48.35, 10.59], ['Landsberg', 48.05, 10.87], ['Fürstenfeldbruck', 48.18, 11.25], ['München', 48.14, 11.58], ['Ingolstadt', 48.77, 11.42], ['Ulm', 48.40, 9.99], ['Nürnberg', 49.45, 11.08], ['Leipzig', 51.34, 12.37], ['Köln', 50.94, 6.96], ['Osnabrück', 52.28, 8.05], ['Hannover', 52.37, 9.73], ['Berlin', 52.52, 13.40], ['Hamburg', 53.55, 9.99]];
const svg = I('radar'), ns = 'http://www.w3.org/2000/svg', el = (n, a, p) => { const e = document.createElementNS(ns, n); for (const k in a) e.setAttribute(k, a[k]); (p || svg).appendChild(e); return e; };
const R = 270, maxKm = 700, rr = km => R * Math.log(1 + km / 6) / Math.log(1 + maxKm / 6);
const toRad = d => d * Math.PI / 180;
const geo = (la, lo) => { const dy = (la - HOME[0]) * 111.2, dx = (lo - HOME[1]) * 111.2 * Math.cos(toRad(HOME[0])); return [Math.hypot(dx, dy), Math.atan2(dx, dy)]; };
const defs = el('defs'); const g = el('linearGradient', { id: 'sw', x1: '0', y1: '0', x2: '1', y2: '0' }, defs); el('stop', { offset: '0', 'stop-color': 'rgba(26,138,212,0)' }, g); el('stop', { offset: '1', 'stop-color': 'rgba(26,138,212,.35)' }, g);
el('circle', { cx: 300, cy: 300, r: R + 8, fill: '#fff' });
[10, 50, 200, 600].forEach(km => { el('circle', { class: 'ring', cx: 300, cy: 300, r: rr(km) }); const t = el('text', { class: 'rl', x: 304, y: 300 - rr(km) + 14 }); t.textContent = km + ' km'; });
['N', 'O', 'S', 'W'].forEach((d, i) => { const a = i * Math.PI / 2, t = el('text', { class: 'rl', x: 300 + Math.sin(a) * (R + 22) - 4, y: 300 - Math.cos(a) * (R + 22) + 4 }); t.textContent = d; });
const sweep = el('g', { class: 'sweep' }); el('path', { d: `M300 300 L${300 + R} 300 A${R} ${R} 0 0 0 ${300 + R * Math.cos(-.6)} ${300 + R * Math.sin(-.6)} Z`, fill: 'url(#sw)' }, sweep);
el('line', { x1: 300, y1: 300, x2: 300 + R, y2: 300, stroke: 'rgba(26,138,212,.6)', 'stroke-width': 2 }, sweep);
const home = el('g', { class: 'pin home lab' }); el('circle', { cx: 300, cy: 300, r: 8 }, home); const ht = el('text', { x: 312, y: 318 }, home); ht.textContent = 'Königsbrunn';
const pins = C.map(([n, la, lo]) => { const [d, b] = geo(la, lo), r = rr(d), x = 300 + Math.sin(b) * r, y = 300 - Math.cos(b) * r, gp = el('g', { class: 'pin' });
const halo = el('circle', { class: 'halo', cx: x, cy: y, r: 9 }, gp); el('circle', { cx: x, cy: y, r: 5 }, gp);
const t = el('text', { x: x + 9, y: y - 8 }, gp); t.textContent = n + ' · ' + Math.round(d) + ' km';
return { n, ang: (b * 180 / Math.PI + 360) % 360, gp, halo }; });
const list = I('cities'); pins.forEach(p => { const s = document.createElement('span'); s.textContent = p.n; p.chip = s; list.appendChild(s); });
if (RM) { pins.forEach(p => p.chip.classList.add('hit')); return; }
let a0 = 0, vis = false; new IntersectionObserver(es => vis = es[0].isIntersecting).observe(svg);
(function tick() { requestAnimationFrame(tick); if (!vis) return; a0 = (a0 + .6) % 360; sweep.setAttribute('transform', `rotate(${a0 - 90} 300 300)`);
pins.forEach(p => { const diff = (a0 - p.ang + 360) % 360; const k = diff < 30 ? 1 - diff / 30 : 0; p.halo.setAttribute('r', 9 + k * 10); p.halo.style.opacity = k; p.gp.classList.toggle('lab', k > .15); if (diff < 4) { p.chip.classList.add('hit'); setTimeout(() => p.chip.classList.remove('hit'), 900); } }); })();
})();
/* ---------- HÄNGENDER SCHLÜSSELANHÄNGER (Verlet) ---------- */
$$('[data-hang]').forEach(root => {
const svg = $('svg', root), strap = $('path.strap', root), hl = $('path.hl', root), card = $('.tag', root);
const N = 14, G = .55, IT = 10; let W, Hh, ax, sg, pts = [], drag = null, moved = false, ang = 0, angV = 0, mouse = null, lm = null;
function init() { W = root.clientWidth; Hh = root.clientHeight; ax = W * +(mobile() ? .84 : root.dataset.x); sg = Hh * +(mobile() ? .16 : root.dataset.len) / N; pts = [];
for (let i = 0; i <= N; i++) { const y = RM ? i * sg : -Hh * .3 + i * sg * .3, x = ax + (RM ? 0 : i / N * 40); pts.push({ x, y, ox: x, oy: y }); } svg.setAttribute('viewBox', `0 0 ${W} ${Hh}`); }
function step() { const last = pts[N];
for (let i = 1; i <= N; i++) { const p = pts[i]; if (drag && i === N) continue; let vx = (p.x - p.ox) * .985, vy = (p.y - p.oy) * .985; const sp = Math.hypot(vx, vy); if (sp > 17) { vx *= 17 / sp; vy *= 17 / sp; }
if (mouse) { const dx = p.x - mouse.x, dy = p.y - mouse.y; if (dx * dx + dy * dy < 9000) { vx += mouse.vx * .04; vy += mouse.vy * .02; } }
p.ox = p.x; p.oy = p.y; p.x += vx; p.y += vy + G * (i === N ? 2.2 : 1); }
if (drag) { last.x = drag.x; last.y = drag.y; }
for (let k = 0; k < IT; k++) { pts[0].x = ax; pts[0].y = 0;
for (let i = 0; i < N; i++) { const a = pts[i], b = pts[i + 1], dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy) || 1e-4; if (d < sg) continue;
const df = (d - sg) / d, fa = i === 0 ? 0 : (drag && i + 1 === N ? 1 : .5), fb = (drag && i + 1 === N) ? 0 : (i === 0 ? 1 : .5);
a.x += dx * df * fa; a.y += dy * df * fa; b.x -= dx * df * fb; b.y -= dy * df * fb; } }
const p1 = pts[N - 2], p2 = pts[N], tg = -Math.atan2(p2.x - p1.x, p2.y - p1.y); angV = (angV + (tg - ang) * .12) * .86; ang += angV; }
function draw() { let d = `M ${pts[0].x} ${pts[0].y}`; for (let i = 1; i < N; i++) { const mx = (pts[i].x + pts[i + 1].x) / 2, my = (pts[i].y + pts[i + 1].y) / 2; d += ` Q ${pts[i].x} ${pts[i].y} ${mx} ${my}`; }
d += ` L ${pts[N].x} ${pts[N].y}`; strap.setAttribute('d', d); hl.setAttribute('d', d); card.style.transform = `translate(${pts[N].x - card.offsetWidth / 2}px, ${pts[N].y - 6}px) rotate(${ang}rad)`; }
const loc = e => { const r = root.getBoundingClientRect(); return { x: e.clientX - r.left, y: e.clientY - r.top }; };
card.addEventListener('pointerdown', e => { e.preventDefault(); card.setPointerCapture(e.pointerId); drag = loc(e); moved = false; card.classList.add('drag'); });
card.addEventListener('pointermove', e => { if (!drag) return; const q = loc(e); if (Math.hypot(q.x - drag.x, q.y - drag.y) > 3) moved = true; drag = { x: q.x, y: q.y - 20 }; });
const up = () => { drag = null; card.classList.remove('drag'); }; card.addEventListener('pointerup', up); card.addEventListener('pointercancel', up);
card.addEventListener('click', e => { if (moved) { e.preventDefault(); e.stopImmediatePropagation(); } }, true);
card.addEventListener('keydown', e => { if (e.key === ' ') { e.preventDefault(); pts[N].ox = pts[N].x - 18; } });
root.parentElement.addEventListener('pointermove', e => { const q = loc(e); mouse = lm ? { x: q.x, y: q.y, vx: q.x - lm.x, vy: q.y - lm.y } : null; lm = q; });
root.parentElement.addEventListener('pointerleave', () => { mouse = null; lm = null; });
let lastW = 0;
const resize = () => { const w = root.clientWidth; if (!lastW) { init(); lastW = w; return; } const oax = ax; W = w; Hh = root.clientHeight; ax = W * +(mobile() ? .84 : root.dataset.x); sg = Hh * +(mobile() ? .16 : root.dataset.len) / N;
pts.forEach(q => { q.x += ax - oax; q.ox += ax - oax; }); svg.setAttribute('viewBox', `0 0 ${W} ${Hh}`); lastW = w; };
resize(); new ResizeObserver(() => { resize(); if (RM) { for (let k = 0; k < 200; k++) step(); draw(); } }).observe(root);
if (RM) { for (let k = 0; k < 200; k++) step(); draw(); return; }
let vis = true; new IntersectionObserver(es => vis = es[0].isIntersecting).observe(root);
let lt = performance.now();
(function loop(now) { now = now || performance.now(); const n = Math.min(8, Math.max(1, Math.round((now - lt) / 16.7))); lt = now; if (vis) { for (let k = 0; k < n; k++) step(); draw(); } requestAnimationFrame(loop); })();
});
/* ---------- Formular ---------- */
I('form').addEventListener('submit', e => { e.preventDefault(); const f = e.target, req = $$('[required]', f), bad = req.filter(x => x.type === 'checkbox' ? !x.checked : !x.value.trim());
req.forEach(x => x.style.borderColor = ''); bad.forEach(x => x.style.borderColor = '#d0473a');
I('fNote').textContent = bad.length ? 'Bitte fülle die markierten Felder aus.' : 'Danke! Ich melde mich so schnell wie möglich bei dir.'; });
/* ---------- NAV: Blob folgt Hover und aktivem Bereich ---------- */
(() => {
const nav = $('#nav'), blob = $('.nav-blob', nav), links = $$('a', nav), secs = links.map(a => $(a.getAttribute('href')));
let cur = -1, hov = -1;
const put = i => { if (i < 0) { blob.style.opacity = 0; return; } const a = links[i]; blob.style.opacity = 1; blob.style.width = a.offsetWidth + 'px'; blob.style.transform = `translateX(${a.offsetLeft}px)`; blob.classList.toggle('h', hov >= 0 && hov !== cur); };
const upd = () => { put(hov >= 0 ? hov : cur); links.forEach((a, i) => a.classList.toggle('on', i === (hov >= 0 ? hov : cur))); };
links.forEach((a, i) => { a.addEventListener('pointerenter', () => { hov = i; upd(); }); a.addEventListener('focus', () => { hov = i; upd(); }); a.addEventListener('blur', () => { hov = -1; upd(); }); });
nav.addEventListener('pointerleave', () => { hov = -1; upd(); });
const spy = () => { let c = -1; secs.forEach((sec, i) => { if (sec && sec.getBoundingClientRect().top < innerHeight * .4) c = i; }); if (c !== cur) { cur = c; upd(); } };
addEventListener('scroll', spy, { passive: true }); addEventListener('resize', upd); spy();
})();
/* ---------- VOLLBILD-MENÜ: Kreis öffnet sich aus dem Button ---------- */
(() => {
const btn = $('#menuBtn'), menu = $('#menu'), prev = $('#mlPrev'), links = $$('a', menu);
$('#menuOffice').textContent = $('#office span').textContent;
const set = open => {
const r = btn.getBoundingClientRect(); menu.style.setProperty('--mx', (r.left + r.width / 2) + 'px'); menu.style.setProperty('--my', (r.top + r.height / 2) + 'px');
menu.classList.toggle('open', open); menu.setAttribute('aria-hidden', !open); btn.setAttribute('aria-expanded', open);
document.body.classList.toggle('menu-open', open); hdr.classList.remove('hide'); hdr.style.zIndex = open ? 120 : '';
if (lenis) open ? lenis.stop() : lenis.start();
if (open) setTimeout(() => links[0].focus({ preventScroll: true }), 400); else { prev.style.opacity = 0; btn.focus({ preventScroll: true }); }
};
btn.addEventListener('click', () => set(!menu.classList.contains('open')));
addEventListener('keydown', e => { if (e.key === 'Escape' && menu.classList.contains('open')) set(false);
if (e.key === 'Tab' && menu.classList.contains('open')) { const f = [btn, ...links], i = f.indexOf(document.activeElement); if (e.shiftKey && i <= 0) { e.preventDefault(); f[f.length - 1].focus(); } else if (!e.shiftKey && i === f.length - 1) { e.preventDefault(); f[0].focus(); } } });
links.forEach(a => a.addEventListener('click', () => set(false), true));
if (RM || !hasG || !matchMedia('(hover:hover) and (pointer:fine)').matches) return;
$$('.ml a[data-img]').forEach(a => { new Image().src = a.dataset.img; });
const xTo = gsap.quickTo(prev, 'x', { duration: .5, ease: 'power3.out' }), yTo = gsap.quickTo(prev, 'y', { duration: .5, ease: 'power3.out' });
$('.ml').addEventListener('pointermove', e => { xTo(e.clientX + 30); yTo(e.clientY - 180); });
$$('.ml a[data-img]').forEach(a => { a.addEventListener('pointerenter', () => { prev.src = a.dataset.img; gsap.to(prev, { opacity: 1, scale: 1, rotation: gsap.utils.random(-6, 6), duration: .35, ease: 'expo.out', overwrite: 'auto' }); });
a.addEventListener('pointerleave', () => gsap.to(prev, { opacity: 0, scale: .9, duration: .2, overwrite: 'auto' })); });
})();
/* ---------- BUTTONS: Pfeil, Ripple, Glanzpunkt, Magnet ---------- */
(() => {
const AR = '
';
$$('.btn').forEach(b => { if (b.querySelector('.bt')) return; const t = document.createElement('span'); t.className = 'bt'; while (b.firstChild) t.appendChild(b.firstChild); b.appendChild(t);
if (b.classList.contains('btn-blue')) t.insertAdjacentHTML('beforeend', `
${AR}${AR} `);
b.addEventListener('pointerdown', e => { const r = b.getBoundingClientRect(), d = Math.max(r.width, r.height) * 2.2, s = document.createElement('span'); s.className = 'rip';
s.style.cssText = `width:${d}px;height:${d}px;left:${e.clientX - r.left - d / 2}px;top:${e.clientY - r.top - d / 2}px`; b.appendChild(s); setTimeout(() => s.remove(), 750); });
b.addEventListener('pointermove', e => { const r = b.getBoundingClientRect(); b.style.setProperty('--bx', (e.clientX - r.left) + 'px'); b.style.setProperty('--by', (e.clientY - r.top) + 'px'); });
});
if (RM || !hasG || !matchMedia('(hover:hover) and (pointer:fine)').matches) return;
$$('.btn:not([data-goo]), .menu-btn, .tel').forEach(b => {
const xT = gsap.quickTo(b, 'x', { duration: .5, ease: 'elastic.out(1,.45)' }), yT = gsap.quickTo(b, 'y', { duration: .5, ease: 'elastic.out(1,.45)' });
b.addEventListener('pointermove', e => { const r = b.getBoundingClientRect(); xT((e.clientX - r.left - r.width / 2) * .25); yT((e.clientY - r.top - r.height / 2) * .35); });
b.addEventListener('pointerleave', () => { xT(0); yT(0); });
});
})();
/* ---------- KLEBRIGER BUTTON (Goo) im Hero ---------- */
(() => {
const btns = $$('[data-goo]'); if (RM || !btns.length) { $$('.goo-hint').forEach(h => h.remove()); return; }
const cv = document.createElement('canvas'); cv.className = 'goo-canvas'; cv.setAttribute('aria-hidden', 'true'); document.body.appendChild(cv);
const ctx = cv.getContext('2d'); let W = 0, H = 0;
const resize = () => { const d = Math.min(devicePixelRatio || 1, 2); W = innerWidth; H = innerHeight; cv.width = W * d; cv.height = H * d; ctx.setTransform(d, 0, 0, d, 0, 0); }; resize(); addEventListener('resize', resize);
const SEG = 12, GRAV = .35, rnd = (a, b) => a + Math.random() * (b - a), strands = []; let raf = 0;
const mk = (st, ax, ay, hx, hy) => { const pts = []; for (let i = 0; i <= SEG; i++) { const t = i / SEG, x = ax + (hx - ax) * t, y = ay + (hy - ay) * t; pts.push({ x, y, ox: x, oy: y }); }
return { st, lx: ax - st.cx, ly: ay - st.cy, hox: rnd(-9, 9), hoy: rnd(-6, 10), w: rnd(4, 9), max: rnd(220, 520), pts, mode: 'held', rest: 0, life: 1 }; };
const snap = s => { s.mode = 'snap'; let L = 0; for (let i = 1; i < s.pts.length; i++) L += Math.hypot(s.pts[i].x - s.pts[i - 1].x, s.pts[i].y - s.pts[i - 1].y); s.rest = L / SEG; };
function grab(b, e) { const r = b.getBoundingClientRect(), st = { btn: b, cx: r.left + r.width / 2, cy: r.top + r.height / 2, px: e.clientX, py: e.clientY, sx: e.clientX, sy: e.clientY, tx: 0, ty: 0, rx: 0, ry: 0, vx: 0, vy: 0, down: true, moved: false };
b.setPointerCapture(e.pointerId); const n = Math.round(rnd(7, 11));
for (let i = 0; i < n; i++) { let ax = e.clientX + rnd(-.22, .22) * r.width, ay = e.clientY + rnd(-.3, .3) * r.height; ax = cl(ax, r.left + r.height * .45, r.right - r.height * .45); ay = cl(ay, r.top + r.height * .2, r.bottom - r.height * .2); strands.push(mk(st, ax, ay, e.clientX, e.clientY)); }
st.strands = strands.filter(s => s.st === st); b._goo = st; loop(); }
const release = b => { const st = b._goo; if (!st) return; st.down = false; st.strands.forEach(s => s.mode === 'held' && snap(s)); };
btns.forEach(b => { b.addEventListener('pointerdown', e => { if (e.button === 0) { e.preventDefault(); grab(b, e); } });
b.addEventListener('pointermove', e => { const st = b._goo; if (!st || !st.down) return; st.px = e.clientX; st.py = e.clientY; if (Math.hypot(st.px - st.sx, st.py - st.sy) > 6) st.moved = true; });
b.addEventListener('pointerup', () => release(b)); b.addEventListener('pointercancel', () => release(b));
b.addEventListener('click', e => { if (b._goo && b._goo.moved) { e.preventDefault(); e.stopImmediatePropagation(); } }, true); });
const anchor = s => ({ x: s.st.cx + s.st.tx + s.lx * (1 - Math.abs(s.st.ry) / 120), y: s.st.cy + s.st.ty + s.ly * (1 - Math.abs(s.st.rx) / 120) });
function step() { const sts = new Set(strands.map(s => s.st)); btns.forEach(b => b._goo && sts.add(b._goo));
sts.forEach(st => { const dx = st.down ? st.px - st.sx : 0, dy = st.down ? st.py - st.sy : 0, d = Math.hypot(dx, dy), k = d ? Math.min(d, 600) / d : 0;
st.vx = (st.vx + (dx * k * .16 - st.tx) * .18) * .72; st.vy = (st.vy + (dy * k * .16 - st.ty) * .18) * .72; st.tx += st.vx; st.ty += st.vy;
st.rx += ((st.down ? cl(-dy * .07, -28, 28) : 0) - st.rx) * (st.down ? .2 : .12); st.ry += ((st.down ? cl(dx * .06, -28, 28) : 0) - st.ry) * (st.down ? .2 : .12);
const sq = st.down ? Math.min(.06, d / 4000) : 0; st.btn.style.transform = `perspective(700px) translate(${st.tx}px,${st.ty}px) rotateX(${st.rx}deg) rotateY(${st.ry}deg) scale(${1 + sq},${1 - sq})`;
if (!st.down && Math.abs(st.tx) + Math.abs(st.ty) + Math.abs(st.rx) + Math.abs(st.ry) < .05 && !strands.some(s => s.st === st)) { st.btn.style.transform = ''; st.btn._goo = null; } });
for (let j = strands.length - 1; j >= 0; j--) { const s = strands[j], p = s.pts, a = anchor(s), st = s.st;
for (let i = 1; i < p.length; i++) { const q = p[i]; if (s.mode === 'held' && i === p.length - 1) continue; const vx = (q.x - q.ox) * .96, vy = (q.y - q.oy) * .96; q.ox = q.x; q.oy = q.y; q.x += vx; q.y += vy + GRAV * .5; }
p[0].x = p[0].ox = a.x; p[0].y = p[0].oy = a.y;
if (s.mode === 'held') { const h = p[p.length - 1]; h.x = h.ox = st.px + s.hox; h.y = h.oy = st.py + s.hoy; const dist = Math.hypot(h.x - a.x, h.y - a.y); if (dist > s.max) snap(s); else s.rest = dist / SEG * 1.01; }
else { s.rest *= .86; s.life -= .012; }
for (let it = 0; it < 6; it++) { for (let i = 0; i < SEG; i++) { const A = p[i], B = p[i + 1], ddx = B.x - A.x, ddy = B.y - A.y, dd = Math.hypot(ddx, ddy) || 1e-4, df = (dd - s.rest) / dd * .5, fA = i === 0, fB = s.mode === 'held' && i + 1 === SEG;
if (fA && fB) continue; if (fA) { B.x -= ddx * df * 2; B.y -= ddy * df * 2; } else if (fB) { A.x += ddx * df * 2; A.y += ddy * df * 2; } else { A.x += ddx * df; A.y += ddy * df; B.x -= ddx * df; B.y -= ddy * df; } } p[0].x = a.x; p[0].y = a.y; }
if (s.mode === 'snap' && (s.rest < .25 || s.life <= 0)) strands.splice(j, 1); } }
function draw() { ctx.clearRect(0, 0, W, H);
for (const s of strands) { const p = s.pts, h = p[p.length - 1], a = p[0], len = Math.hypot(h.x - a.x, h.y - a.y), thin = s.mode === 'held' ? Math.max(.3, Math.min(1, 110 / (len + 30))) : Math.max(.3, s.life);
ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.strokeStyle = ctx.fillStyle = '#1a8ad4'; ctx.shadowColor = 'rgba(13,27,46,.25)'; ctx.shadowBlur = 8; ctx.shadowOffsetY = 7;
for (let i = 0; i < SEG; i++) { const t = (i + .5) / SEG, pr = .22 + .78 * Math.pow(Math.abs(2 * t - 1), 2.2) + (t < .15 ? .35 : 0); ctx.lineWidth = Math.max(1.1, s.w * pr * (.5 + .5 * thin)); ctx.beginPath(); ctx.moveTo(p[i].x, p[i].y); ctx.lineTo(p[i + 1].x, p[i + 1].y); ctx.stroke(); }
ctx.beginPath(); ctx.ellipse(a.x, a.y, s.w * 1.05, s.w * .8, 0, 0, 7); ctx.fill(); ctx.beginPath(); ctx.arc(h.x, h.y, s.w * (s.mode === 'held' ? .9 : .55), 0, 7); ctx.fill(); ctx.shadowColor = 'transparent';
ctx.strokeStyle = 'rgba(255,255,255,.35)'; ctx.lineWidth = .9; ctx.beginPath(); ctx.moveTo(p[1].x - 1, p[1].y); for (let i = 2; i < SEG - 1; i++) ctx.lineTo(p[i].x - s.w * .25, p[i].y); ctx.stroke(); } }
function loop() { if (raf) return; const tick = () => { step(); draw(); const busy = strands.length || btns.some(b => b._goo); raf = busy ? requestAnimationFrame(tick) : 0; if (!busy) ctx.clearRect(0, 0, W, H); }; raf = requestAnimationFrame(tick); }
addEventListener('scroll', () => btns.forEach(b => b._goo && b._goo.down && release(b)), { passive: true });
})();
/* ---------- FLÜSSIGE TAB-LEISTE (Handy) ---------- */
(() => {
const nav = $('#ltb'), svg = $('svg.shape', nav), path = $('path', svg), dot = $('circle', svg), tabs = $$('a', nav).filter(a => !a.hasAttribute('data-call')), call = $('[data-call]', nav), all = $$('a', nav);
const secs = tabs.map(a => $(a.getAttribute('href'))), css = getComputedStyle(nav), n = all.length;
let W, Ht, barH, R, top, idx = 0; const st = { x: 0, vx: 0, lift: 0, vl: 0, tx: 0, tl: 0 }, FIL = 12;
const center = i => (i + .5) * W / n;
const measure = () => { W = nav.clientWidth; Ht = nav.clientHeight; barH = parseFloat(css.getPropertyValue('--h')); R = parseFloat(css.getPropertyValue('--R')); top = Ht - barH; svg.setAttribute('viewBox', `0 0 ${W} ${Ht}`); st.tx = center(idx); if (!st.x) st.x = st.tx; st.tl = R * .95; };
function shape(cx, lift) { const rr = barH / 2, y0 = top, y1 = Ht, cy = top - lift + R * .55, fy = top - FIL, dy = cy - fy, D = R + FIL; let bump = '';
if (lift > 2 && Math.abs(dy) < D) { const d = Math.sqrt(D * D - dy * dy), f1 = cx - d, f2 = cx + d, p1 = [f1 + (cx - f1) * FIL / D, fy + (cy - fy) * FIL / D], p2 = [f2 + (cx - f2) * FIL / D, fy + (cy - fy) * FIL / D], lg = (cy - p1[1]) > 0 ? 0 : 1;
bump = `L ${f1} ${y0} A ${FIL} ${FIL} 0 0 0 ${p1[0]} ${p1[1]} A ${R} ${R} 0 ${lg} 1 ${p2[0]} ${p2[1]} A ${FIL} ${FIL} 0 0 0 ${f2} ${y0} `; }
path.setAttribute('d', `M ${rr} ${y0} ${bump}L ${W - rr} ${y0} A ${rr} ${rr} 0 0 1 ${W - rr} ${y1} L ${rr} ${y1} A ${rr} ${rr} 0 0 1 ${rr} ${y0} Z`);
dot.setAttribute('cx', cx); dot.setAttribute('cy', cy); dot.setAttribute('r', Math.max(0, R - 5) * Math.min(1, lift / (R * .5)));
all.forEach((t, i) => { const k = i === idx ? Math.max(0, 1 - Math.abs(cx - center(i)) / (W / n * .5)) : 0; $('svg', t).style.transform = k ? `translateY(${(cy - (top + barH / 2)) * k}px)` : ''; }); }
let raf = 0;
const tick = () => { const dist = Math.abs(st.tx - st.x), want = dist > 6 ? Math.max(R * .5, R * .95 - dist * .3) : st.tl;
st.vx = (st.vx + (st.tx - st.x) * .045) * .8; st.x += st.vx; st.vl = (st.vl + (want - st.lift) * .14) * .72; st.lift += st.vl; shape(st.x, st.lift);
raf = Math.abs(st.vx) + Math.abs(st.vl) + Math.abs(st.tx - st.x) > .05 ? requestAnimationFrame(tick) : 0; };
const select = i => { if (i === idx) return; all[idx].setAttribute('aria-current', 'false'); idx = i; all[i].setAttribute('aria-current', 'true'); st.tx = center(i);
if (RM) { st.x = st.tx; shape(st.x, st.lift); return; } if (!raf) raf = requestAnimationFrame(tick); };
let lock = 0;
tabs.forEach((t, i) => t.addEventListener('click', () => { select(i); lock = Date.now() + 1200; }));
call.addEventListener('click', () => { select(n - 1); setTimeout(() => spy(true), 1500); });
const spy = force => { if (!force && Date.now() < lock) return; let c = 0; secs.forEach((s, i) => { if (s && s.getBoundingClientRect().top < innerHeight * .5) c = i; }); select(c); };
addEventListener('scroll', () => spy(), { passive: true });
measure(); st.lift = st.tl; shape(st.x, st.lift);
new ResizeObserver(() => { measure(); st.x = st.tx; shape(st.x, st.lift); }).observe(nav);
})();
/* ---------- Formular-Button: Laden, dann Häkchen ---------- */
(() => { const f = I('form'), b = $('button[type=submit]', f);
f.addEventListener('submit', () => { setTimeout(() => { if (!/Danke/.test(I('fNote').textContent)) return; const t = $('.bt', b), old = t.innerHTML; b.disabled = true;
t.innerHTML = '
Wird gesendet'; setTimeout(() => { b.classList.add('done'); t.innerHTML = '✓ Gesendet'; if (hasG && !RM) gsap.fromTo(b, { scale: .92 }, { scale: 1, duration: .6, ease: 'elastic.out(1,.4)' }); }, 1100); }, 0); }); })();
if (hasG) { ScrollTrigger.sort(); ScrollTrigger.refresh(); addEventListener('load', () => ScrollTrigger.refresh()); }
})();