${c.name}
${c.scope?`${c.scope[LANG][0]}${c.scope[LANG].slice(1).join(', ')}`:''}
«Schnell, unkompliziert und genau so, wie ich es mir vorgestellt habe. Die Seite sieht auf dem Handy genauso gut aus wie am Computer.»
${tt.f}
${indepAll()}
${o.p}
${o.care.p}
${o.bonus.p}
${o.cmp.p}
${o.cmp.note}
${p.p}
${s.p}
${s.pay?`${s.pay}`:s.d}${a.p1}
${a.p2}
${q[1]}

${c.sector[LANG]}
${w.more} ${arrowR}${libText(cat)}
${indepAll()}
${q.text[LANG]}
${L.nf}
${L.live} ↗${L.wait}
${L.live} ↗`; f.classList.add('wait'); let loaded = false; f.addEventListener('load', ()=>{ loaded = true; f.classList.remove('wait'); }, {once:true}); setTimeout(()=>{ if(!loaded && pl.isConnected){ pl.querySelector('p').textContent = L.slowOld; const sp = pl.querySelector('.spin'); if(sp) sp.remove(); } }, 12000); f.setAttribute('sandbox','allow-popups allow-popups-to-escape-sandbox'); f.src = src.slice(4); } else if(src.startsWith('demo:')){ const dc = CASES.find(x=>x.id===src.slice(5)); if(dc && dc.demoUrl && /^https?:$/.test(location.protocol)){ f.src = dc.demoUrl; } else loadBlock('demo', src.slice(5)).then(html=>{ if(html) f.srcdoc = embedHtml(html,'about:srcdoc'); }); } else if(src.startsWith('old:')){ f.title='Alte Website'; loadBlock('old', src.slice(4)).then(html=>{ if(html) f.srcdoc = embedHtml(html,'about:srcdoc'); }); } guardOn = true; const m = body.querySelector('.mk'); if(m) m.remove(); f.addEventListener('load', ()=>{ try{ const r=body.getBoundingClientRect(), far = r.bottom < -innerHeight*0.6 || r.top > innerHeight*1.6; const d=f.contentDocument; if(d&&d.documentElement) d.documentElement.classList.toggle('mm-paused', far); }catch(_){} }); body.insertBefore(f, body.firstChild); if(pl) body.insertBefore(pl, f); // CSS zoom (Chrome/Safari/Firefox 126+) rasterises at the shown size and is much cheaper to scroll than transform:scale; transform stays as fallback const ZOOM = CSS.supports('zoom','0.5'); const fit = ()=>{ const w = body.clientWidth; if(!w) return; if(body.classList.contains('native')){ f.style.width='100%'; f.style.transform='none'; f.style.zoom=''; f.style.height='100%'; return; } const base = matchMedia('(max-width:640px)').matches?430:1280, s = w/base; f.style.width = base+'px'; f.style.height = (body.clientHeight/s)+'px'; if(ZOOM){ f.style.zoom = s; f.style.transform='none'; } else { f.style.transform = `scale(${s})`; } }; fit(); new ResizeObserver(fit).observe(body); body.addEventListener('refit', fit); } // rogue-scroll guard: embedded documents can scroll this page (fragment navigation, focus). If the page scrolls with no // user input and it wasn't us, put it back. if('scrollRestoration' in history) history.scrollRestoration = 'manual'; let lastInput = 0, ownUntil = performance.now() + 300, goodY = scrollY, guardOn = false; const GUARD_OFF = matchMedia('(pointer:coarse)').matches; // on touch screens the guard fought fast flicks ['wheel','touchstart','touchmove','keydown','pointerdown'].forEach(ev=>addEventListener(ev, ()=>{ lastInput = performance.now(); }, {passive:true, capture:true})); const _sto = window.scrollTo.bind(window); window.scrollTo = function(a){ const smooth = a && typeof a==='object' && a.behavior==='smooth'; ownUntil = Math.max(ownUntil, performance.now() + (smooth ? 1500 : 150)); return _sto.apply(window, arguments); }; addEventListener('hashchange', ()=>{ ownUntil = Math.max(ownUntil, performance.now() + 1500); }); const _siv = Element.prototype.scrollIntoView; Element.prototype.scrollIntoView = function(){ ownUntil = Math.max(ownUntil, performance.now() + 1500); return _siv.apply(this, arguments); }; addEventListener('scroll', ()=>{ const now = performance.now(); if(guardOn && !GUARD_OFF && now - lastInput > 600 && now > ownUntil && Math.abs(scrollY - goodY) > 120 && document.body.style.overflow!=='hidden'){ ownUntil = now + 150; _sto({top:goodY, behavior:'instant'}); return; } goodY = scrollY; },{passive:true}); // idle pre-mount: one pane every ~900ms, only when the user has not scrolled for 400ms and the tab is visible let lastScrollAt = 0; addEventListener('scroll', ()=>{ lastScrollAt = performance.now(); }, {passive:true}); const farIO = new IntersectionObserver(es=>es.forEach(e=>{ e.target.querySelectorAll('iframe').forEach(f=>{ try{ const d=f.contentDocument; if(d&&d.documentElement) d.documentElement.classList.toggle('mm-paused', !e.isIntersecting); }catch(_){} }); }),{rootMargin:'60% 0px 60% 0px'}); const SLOW = (()=>{ const c = navigator.connection; return !!(c && (c.saveData || /(^|-)2g$|3g/.test(c.effectiveType||''))); })(); function premountTick(){ if(SLOW) return; if(document.hidden || performance.now()-lastScrollAt < 250) return setTimeout(premountTick, 200); const phone = matchMedia('(max-width:640px)').matches; const next = [...document.querySelectorAll('.pane-body')].find(b=>b._mount && !b._done && (!phone || b.offsetParent)); if(next){ next._mount(); setTimeout(premountTick, 650); } else setTimeout(premountTick, 2000); } setTimeout(premountTick, 350); function syncPanes(){ const phone = matchMedia('(max-width:640px)').matches; document.querySelectorAll('[data-cmp]').forEach(el=>{ const tabs = el.previousElementSibling && el.previousElementSibling.hasAttribute('data-tabs') ? el.previousElementSibling : (el.closest('[data-vs]')||document).querySelector('[data-tabs]'); const on = tabs.querySelector('button.on').dataset.p; el.querySelectorAll('[data-pane]').forEach(pn=>pn.classList.toggle('hide', phone && pn.dataset.pane!==on)); }); } addEventListener('resize', syncPanes); function updateReach(){ const el = document.getElementById('reach'); if(!el) return; const a = t().about, H = CONTACT.hours; const p = new Intl.DateTimeFormat('en-GB',{timeZone:'Europe/Zurich',weekday:'short',hour:'numeric',hour12:false}).formatToParts(new Date()); const wd = {Sun:0,Mon:1,Tue:2,Wed:3,Thu:4,Fri:5,Sat:6}[p.find(x=>x.type==='weekday').value]; const hr = +p.find(x=>x.type==='hour').value; el.classList.add('on'); el.querySelector('span').textContent = a.reply; } setInterval(updateReach, 60000); /* ---------- v68: spam protection for the contact form ---------- */ const PAGE_T0 = Date.now(); function looksLikeBot(f){ if(Date.now() - PAGE_T0 < 3000) return true; // nobody fills the form within 3 s of opening the page if(!f._human) return true; // submitted by a script, not by a click or key press const msg = (f.querySelector('[name=nachricht]') || {}).value || ''; if((msg.match(/https?:\/\/|www\./gi) || []).length >= 3) return true; // link spam return false; } function fakeOk(f){ const k = t().contact.f, v = (f.querySelector('[name=vorname]') || {}).value || ''; const ok = f.querySelector('[data-ok]'); if(ok) ok.textContent = k.ok.replace('{name}', v); f.classList.add('sent'); } const SEND_KEY = 'mm-sends', SEND_MAX = 3, SEND_WINDOW = 60*60*1000; function sendLog(){ try{ return JSON.parse(localStorage.getItem(SEND_KEY) || '[]').filter(x=>Date.now()-x < SEND_WINDOW); }catch(e){ return []; } } function sendAllowed(){ return sendLog().length < SEND_MAX; } function countSend(){ try{ const l = sendLog(); l.push(Date.now()); localStorage.setItem(SEND_KEY, JSON.stringify(l)); }catch(e){} } function limitNote(f){ let n = f.querySelector('.limit-note'); if(!n){ n = document.createElement('p'); n.className = 'limit-note'; n.setAttribute('role','status'); const b = f.querySelector('button[type=submit]'); b.insertAdjacentElement('afterend', n); } n.innerHTML = LANG==='en' ? `You've already sent me a few messages in the last hour, thank you. I'll get back to you soon. Anything else? Write to ${CONTACT.email}.` : `Sie haben mir in der letzten Stunde schon ein paar Nachrichten geschickt, danke. Ich melde mich bald. Noch etwas? Schreiben Sie an ${CONTACT.email}.`; } function initForm(f){ const sel = f.querySelector('select[name=geschaeft]'), other = f.querySelector('label.other'); sel.addEventListener('change', ()=>{ const isOther = /Andere|Other/.test(sel.value); other.hidden = !isOther; other.querySelector('input').required = isOther; }); const tel = f.querySelector('input[name=telefon]'), telT = f.querySelector('.tel-t'), prefNote = f.querySelector('.pref-note'); f.querySelectorAll('input[name=kontakt]').forEach(r=>r.addEventListener('change', ()=>{ const needTel = r.checked && r.value !== t().contact.f.prefs[0]; tel.required = needTel; telT.textContent = needTel ? t().contact.f.phoneReq : t().contact.f.phone; prefNote.hidden = !needTel; })); // extras sit in an Ausbau and a Vollausbau box; the chosen package's box is outlined, Basis gets a note, an extra above the package moves it up const pk = f.querySelector('select[name=paket]'), ex = f.querySelector('.extras'), exNote = ex.querySelector('.ex-note'); const pkRank = v => { const o = t().offer; return v===o.t3.n ? 3 : v===o.t2.n ? 2 : v===o.t1.n ? 1 : 0; }; const pkName = n => n===3 ? t().offer.t3.n : t().offer.t2.n; function syncExtras(msg){ const r = pkRank(pk.value); ex.querySelectorAll('.xbox').forEach(b=>b.classList.toggle('on', r >= +b.dataset.tier)); const kf = t().contact.f; if(msg){ exNote.textContent = msg; exNote.classList.add('bump'); exNote.hidden = false; } else { exNote.classList.remove('bump'); exNote.textContent = r===1 ? kf.exLaunch : kf.exHint; exNote.hidden = r > 1; } } pk.addEventListener('change', ()=>{ const r = pkRank(pk.value), gone = []; if(r) ex.querySelectorAll('input:checked').forEach(c=>{ if(+c.closest('label').dataset.need > r){ c.checked = false; gone.push(c.value); } }); syncExtras(gone.length ? t().contact.f.exDrop.replace('{x}', gone.map(x=>'«'+x+'»').join(', ')).replace('{p}', pk.value) : ''); }); ex.querySelectorAll('input').forEach(c=>c.addEventListener('change', ()=>{ const need = +c.closest('label').dataset.need, r = pkRank(pk.value); if(c.checked && r < need){ pk.value = pkName(need); pk.classList.remove('flash'); void pk.offsetWidth; pk.classList.add('flash'); syncExtras(t().contact.f.exBump.replace('{p}', pkName(need)).replace('{x}', c.value)); } else syncExtras(); })); syncExtras(); ['pointerdown','keydown','touchstart'].forEach(ev=>f.addEventListener(ev, e=>{ if(e.isTrusted) f._human = true; }, {capture:true, passive:true})); f.addEventListener('submit', async e=>{ e.preventDefault(); if(f.querySelector('input[name=bot-field]').value){ f.classList.add('sent'); return; } if(looksLikeBot(f)){ fakeOk(f); return; } if(!f.reportValidity()) return; if(f.classList.contains('busy')) return; if(!sendAllowed()){ limitNote(f); return; } const btn = f.querySelector('button[type=submit]'); btn.disabled = true; const kf = t().contact.f, label = btn.textContent; f.classList.add('busy'); btn.classList.add('sending'); btn.innerHTML = '' + kf.sending; const slowNote = f.querySelector('.slow'); const slowT = setTimeout(()=>{ if(slowNote) slowNote.hidden = false; }, 5000); const ctrl = new AbortController(); const killT = setTimeout(()=>ctrl.abort(), 40000); const done = ()=>{ clearTimeout(slowT); clearTimeout(killT); if(slowNote) slowNote.hidden = true; f.classList.remove('busy'); btn.classList.remove('sending'); btn.textContent = label; }; const fd = new FormData(f); const wishes = [fd.get('start'), ...fd.getAll('wunsch')].filter(Boolean); const lines = [ ['Name', fd.get('vorname')+' '+fd.get('nachname')], ['E-Mail', fd.get('email')], ['Telefon', fd.get('telefon') || '-'], ['Kontakt per', fd.get('kontakt') || '-'], ['Geschäft', fd.get('geschaeft') + (fd.get('geschaeft_andere') ? ' ('+fd.get('geschaeft_andere')+')' : '')], ['Website', fd.get('website') || '-'], ['Paket', fd.get('paket') || '-'], ['Wunsch', wishes.length ? wishes.join(', ') : '-'], ['Nachricht', fd.get('nachricht') || '-'] ]; const subject = 'Website-Anfrage: ' + (fd.get('geschaeft') || '') + ' / ' + fd.get('vorname') + ' ' + fd.get('nachname'); const body = lines.map(l=>l[0]+': '+l[1]).join('\n'); const hosted = /^https?:$/.test(location.protocol) && !/^(localhost|127\.)/.test(location.hostname); try{ if(CONTACT.emailjs.key){ const p = { name: fd.get('vorname')+' '+fd.get('nachname'), vorname: fd.get('vorname'), nachname: fd.get('nachname'), email: fd.get('email'), telefon: (fd.get('telefon') || '-') + ' · Kontakt per ' + (fd.get('kontakt') || 'E-Mail'), kontakt: fd.get('kontakt') || '-', geschaeft: fd.get('geschaeft') + (fd.get('geschaeft_andere') ? ' ('+fd.get('geschaeft_andere')+')' : ''), website: fd.get('website') || '-', wunsch: (fd.get('paket') ? 'Paket: '+fd.get('paket')+(wishes.length?' / ':'') : '') + (wishes.length ? wishes.join(', ') : (fd.get('paket') ? '' : '-')), paket: fd.get('paket') || '-', nachricht: fd.get('nachricht') || '-', subject, sprache: LANG }; const r = await fetch('https://api.emailjs.com/api/v1.0/email/send', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({service_id: CONTACT.emailjs.service, template_id: CONTACT.emailjs.template, user_id: CONTACT.emailjs.key, template_params: p}), signal: ctrl.signal}); if(!r.ok) throw 0; } else if(!CONTACT.formspree.includes('YOUR_ID')){ const r = await fetch(CONTACT.formspree,{method:'POST',headers:{Accept:'application/json'},body:fd}); if(!r.ok) throw 0; } else if(hosted){ const params = new URLSearchParams(); params.set('form-name','anfrage'); fd.forEach((v,k)=>{ if(k==='wunsch') return; params.set(k, v); }); params.set('wunsch', wishes.join(', ')); params.set('subject', subject); const r = await fetch(location.pathname, {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: params.toString()}); if(!r.ok) throw 0; } else { location.href = 'mailto:' + CONTACT.email + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body); } const k = t().contact.f, viaMail = !CONTACT.emailjs.key && CONTACT.formspree.includes('YOUR_ID') && !hosted; f.querySelector('[data-ok]').textContent = (viaMail ? k.okMail+' ' : '') + k.ok.replace('{name}', fd.get('vorname')); f.querySelector('[data-okto]').textContent = k.okTo + ' ' + fd.get('email'); done(); btn.disabled = true; countSend(); f.classList.add('sent'); f.scrollIntoView({block:'center', behavior:'smooth'}); }catch(_){ done(); btn.disabled = false; f.querySelector('.fail').hidden = false; location.href = 'mailto:' + CONTACT.email + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body); } }); } /* ---------- language ---------- */ function applyLang(){ document.documentElement.lang = LANG; document.querySelectorAll('[data-t]').forEach(el=>{ el.textContent = t()[el.dataset.t]; }); document.querySelectorAll('[data-tlabel]').forEach(el=>el.setAttribute('aria-label', t()[el.dataset.tlabel])); document.querySelectorAll('[data-lang]').forEach(b=>b.classList.toggle('on', b.dataset.lang===LANG)); currentView = ''; route(); } /* ---------- v67: switching the language keeps you where you are ---------- */ const LANG_ANCHOR = 'main section, main article, main h1, main h2, main h3, main p, main li, main details, main figure, main blockquote, main .pane, main .tier, main .step, main .br-tile'; function langSnapshot(){ const top = (document.getElementById('hd') || {offsetHeight:64}).offsetHeight || 64; const els = [...app.querySelectorAll(LANG_ANCHOR)]; let bi = -1, bt = Infinity, ci = -1, ch = Infinity; els.forEach((el,i)=>{ const r = el.getBoundingClientRect(); if(!r.height) return; if(r.top >= top - 2 && r.top < bt){ bt = r.top; bi = i; } // first block starting below the header else if(r.top < top - 2 && r.bottom > top && r.height < ch){ ch = r.height; ci = i; } // smallest block crossing the header line }); const i = (ci >= 0 && (bi < 0 || bt > innerHeight * .5)) ? ci : bi; const st = { n: els.length, i, off: i >= 0 ? els[i].getBoundingClientRect().top : 0, ratio: scrollY / Math.max(1, document.documentElement.scrollHeight - innerHeight), y: scrollY, faq: [...app.querySelectorAll('.faq details')].map(d=>d.open || d.classList.contains('is-open')), vsm: [...app.querySelectorAll('.vsm')].map(v=>v.dataset.vsm), form: [] }; const f = app.querySelector('#form'); if(f) [...f.elements].forEach(el=>{ if(!el.name || el.name==='bot-field' || el.type==='file' || el.type==='submit' || el.type==='button') return; if(el.type==='radio' || el.type==='checkbox'){ if(el.checked) st.form.push([el.name, [...f.querySelectorAll(`[name="${el.name}"]`)].indexOf(el), 'chk']); } else if(el.tagName==='SELECT'){ if(el.selectedIndex > 0) st.form.push([el.name, el.selectedIndex, 'sel']); } else if(el.value) st.form.push([el.name, el.value, 'txt']); }); return st; } function langRestore(st){ app.querySelectorAll('.faq details').forEach((d,i)=>{ if(st.faq[i]){ d.open = true; d.classList.add('is-open'); } }); app.querySelectorAll('.vsm').forEach((v,i)=>{ const k = st.vsm[i]; if(!k) return; v.dataset.vsm = k; v.querySelectorAll('[data-vsm-b]').forEach(b=>b.classList.toggle('on', b.dataset.vsmB===k)); }); const f = app.querySelector('#form'); if(f) st.form.forEach(([n,v,k])=>{ const el = k==='chk' ? f.querySelectorAll(`[name="${n}"]`)[v] : f.querySelector(`[name="${n}"]`); if(!el) return; if(k==='chk') el.checked = true; else if(k==='sel') el.selectedIndex = v; else el.value = v; el.dispatchEvent(new Event(k==='txt' ? 'input' : 'change', {bubbles:true})); }); const place = ()=>{ if(st.y <= 2){ scrollTo({top:0, behavior:'instant'}); return; } const els = [...app.querySelectorAll(LANG_ANCHOR)]; let y = (st.i >= 0 && els.length === st.n && els[st.i]) ? els[st.i].getBoundingClientRect().top + scrollY - st.off : st.ratio * (document.documentElement.scrollHeight - innerHeight); scrollTo({top: Math.max(0, Math.round(y)), behavior: 'instant'}); }; place(); // what is on screen (or above it) is shown at once: no fade-in, no counting up again const html = document.documentElement; html.classList.add('lang-snap'); app.querySelectorAll('.rv, .split, .hero-facts, .cs, .steps').forEach(el=>{ if(el.getBoundingClientRect().top < innerHeight){ el.classList.add('in'); el.removeAttribute('data-count'); el.querySelectorAll('[data-count]').forEach(c=>c.removeAttribute('data-count')); } }); void html.offsetHeight; requestAnimationFrame(()=>{ place(); requestAnimationFrame(()=>{ html.classList.remove('lang-snap'); }); }); let moved = false; const mark = ()=>{ moved = true; }; addEventListener('wheel', mark, {passive:true, once:true}); addEventListener('touchstart', mark, {passive:true, once:true}); addEventListener('keydown', mark, {once:true}); setTimeout(()=>{ if(!moved) place(); removeEventListener('wheel', mark); removeEventListener('touchstart', mark); removeEventListener('keydown', mark); }, 350); } function switchLang(l){ if(l === LANG) return; const st = langSnapshot(); LANG = l; try{ localStorage.setItem('mm-lang', LANG); }catch(e){} window._langKeep = true; try{ applyLang(); } finally { window._langKeep = false; } langRestore(st); } document.querySelectorAll('[data-lang]').forEach(b=>b.addEventListener('click',()=>{ switchLang(b.dataset.lang); })); /* ---------- links + phone menu bar ---------- */ document.addEventListener('click', e=>{ if(e.defaultPrevented || e.button!==0 || e.metaKey || e.ctrlKey || e.shiftKey) return; const a = e.target.closest && e.target.closest('a[href^="#/"]'); if(!a) return; if(a.getAttribute('href')===location.hash && (!PATHMODE || location.pathname==='/')){ e.preventDefault(); route(); } }); document.addEventListener('focusin', e=>{ if(e.target.matches && e.target.matches('input:not([type=checkbox]):not([type=radio]),textarea,select')) document.documentElement.classList.add('kb'); }); document.addEventListener('focusout', ()=>{ setTimeout(()=>{ const el = document.activeElement; if(!(el && el.matches && el.matches('input:not([type=checkbox]):not([type=radio]),textarea,select'))) document.documentElement.classList.remove('kb'); }, 80); }); /* ---------- header ---------- */ const burger = document.getElementById('burger'); burger.addEventListener('click',()=>{ const hd=document.getElementById('hd'); const o=hd.classList.toggle('open'); burger.classList.toggle('x',o); burger.setAttribute('aria-expanded',o); }); document.getElementById('yr').textContent = new Date().getFullYear(); /* ---------- go ---------- */ window._first = true; addEventListener('hashchange', route); applyLang();