1// ââ Shared Nav + Footer ââ 2// Usage: <script src="components.js"></script> 3// Then call: renderNav('leistungen'); renderFooter(); 4 5// Nav und Footer arbeiten mit relativen Pfaden. Auf Seiten in Unterordnern 6// (z. B. /blog/artikel.html) zeigen die sonst ins Leere â Logo 404, alle 7// Links auf /blog/â¦. Deshalb hier die Verschachtelungstiefe ermitteln und 8// relative href/src beim Einsetzen entsprechend voranstellen. 9const CW_BASE = (() => { 10 // Tiefe = Pfadsegmente ohne das letzte (Datei oder '' bei Verzeichnis-URL wie /en/) 11 const segs = location.pathname.replace(/^\//, '').split('/'); 12 segs.pop(); 13 return '../'.repeat(segs.length); 14})(); 15function cwPaths(html) { 16 return CW_BASE 17 ? html.replace(/\b(href|src)="(?!https?:|\/\/|\/|#|mailto:|tel:|data:|javascript:)/g, `$1="${CW_BASE}`) 18 : html; 19} 20 21const NAV_LINKS = [ 22 { href: '/', label: 'Home' }, 23 { 24 href: 'leistungen', 25 label: 'Leistungen', 26 sub: [ 27 { icon: '<svg viewBox="0 0 24 24"><path d="M12 3l1.7 5.1 5.1 1.7-5.1 1.7L12 16.6l-1.7-5.1L5.2 9.8l5.1-1.7z"/><path d="M19 15l.7 1.8 1.8.7-1.8.7L19 20l-.7-1.8-1.8-.7 1.8-.7z"/></svg>', href: 'ki-beratung', title: 'KI-Beratung & Strategie', desc: 'Potenzialanalyse, Roadmap und Begleitung der KI-Transformation.' }, 28 { icon: '<svg viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg>', href: 'voice-agenten', title: 'KI-Voice-Agenten', desc: 'Automatische Telefonannahme 24/7 â Bestellungen, Buchungen.' }, 29 { icon: '<svg viewBox="0 0 24 24"><path d="M4 9a8 8 0 0 1 13.5-2.8L20 8"/><path d="M20 4v4h-4"/><path d="M20 15a8 8 0 0 1-13.5 2.8L4 16"/><path d="M4 20v-4h4"/></svg>', href: 'automation', title: 'Prozessautomatisierung', desc: 'Workflows verbinden, Systeme integrieren â Zeit sparen.' }, 30 { icon: '<svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="16" rx="2"/><path d="M4 9.5h16M8 3v4M16 3v4"/></svg>', href: 'terminbuchung', title: 'Terminbuchungssoftware', desc: 'Online-Buchung, automatische Erinnerungen, Kalender-Sync.' }, 31 { icon: '<svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 9.5h18M9 9.5V19"/></svg>', href: 'webdesign', title: 'Webdesign & Webprojekte', desc: 'Moderne Websites, die konvertieren â schnell & klar.' }, 32 { icon: '<svg viewBox="0 0 24 24"><path d="M8 6l-5 6 5 6M16 6l5 6-5 6"/></svg>', href: 'software-entwicklung', title: 'App- & Softwareentwicklung', desc: 'Apps, individuelle Software und Schnittstellen für deinen Betrieb.' }, 33 { icon: '<svg viewBox="0 0 24 24"><path d="M7 8V6.2A2.2 2.2 0 0 1 9.2 4h5.6A2.2 2.2 0 0 1 17 6.2V8"/><rect x="3" y="8" width="18" height="12" rx="2"/><path d="M3 13h18"/></svg>', href: 'karriereseiten', title: 'Recruiting & Karriereseiten', desc: 'Stellenanzeigen im eigenen Design, Bewerbung in zwei Minuten.' }, 34 { icon: '<svg viewBox="0 0 24 24"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v8a1.5 1.5 0 0 1-1.5 1.5H9l-5 4z"/></svg>', href: 'chatbot', title: 'KI-Chatbots', desc: 'Intelligente Assistenten für deine Website & Apps.' }, 35 { icon: '<svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg>', href: 'it-service', title: 'MS Office & IT-Service', desc: 'Microsoft 365, Cloud, IT-Support â dein digitaler Arbeitsplatz.' }, 36 { icon: '<svg viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.5"/></svg>', href: 'tools', title: 'Web-Tools & Web-Apps', desc: 'Web-Apps, individuelle Anwendungen und Schnittstellen â plus fertige Bausteine.' }, 37 ], 38 foot: { 39 href: 'leistungen', 40 title: 'Leistungen entdecken', 41 desc: 'Ãbersicht über das komplette Angebot.', 42 }, 43 }, 44 { href: 'blog', label: 'Insights' }, 45 { href: 'ueber-uns', label: 'Ãber uns' },
46 { href: 'kontakt', label: 'Kontakt' }, 47]; 48 49// Aufrufer übergeben mal '/webdesign', mal 'webdesign', mal 'webdesign.html'. 50// Und Unterseiten wie ki-beratung-mannheim sollen "Leistungen" markieren, 51// statt gar nichts. Beides hier zentral auflösen. 52function resolveActive(page) { 53 if (!page) return null; 54 let p = String(page).replace(/^\/+/, '').replace(/\.html$/, ''); 55 if (!p) return '/'; 56 if (NAV_LINKS.some(l => l.href === p)) return p; 57 const leistungen = NAV_LINKS.find(l => l.sub); 58 const unter = leistungen ? leistungen.sub.map(s => s.href) : []; 59 // direkte Unterseite oder lokale Variante davon (z. B. webdesign-mannheim) 60 if (unter.some(h => p === h || p.startsWith(h + '-'))) return 'leistungen'; 61 if (['wartungsvertraege', 'sicherheit', 'preise', 'referenzen', 62 'webdesign-konfigurator', 'it-dienstleister-bensheim', 63 'prozessautomatisierung-rhein-neckar', 'voice-agent-bensheim'].includes(p)) return 'leistungen'; 64 return p; 65} 66 67function renderNav(activePage) { 68 activePage = resolveActive(activePage); 69 const linksHTML = NAV_LINKS.map(l => { 70 const isActive = l.href === activePage; 71 if (l.sub) { 72 const groups = [ 73 { label: 'KI & Automatisierung', items: l.sub.filter(s => ['ki-beratung','voice-agenten','automation','chatbot'].includes(s.href)) }, 74 { label: 'Web', items: l.sub.filter(s => ['webdesign','karriereseiten','terminbuchung','software-entwicklung','tools'].includes(s.href)) }, 75 { label: 'Service', items: l.sub.filter(s => ['it-service'].includes(s.href)).concat([{ href: 'support', title: 'Support & Tickets' }, { href: 'wartungsvertraege', title: 'Wartungsverträge' }, { href: 'sicherheit', title: 'Sicherheit & Datenschutz' }, { href: 'termin-buchen', title: 'Termin buchen' }]) }, 76 ]; 77 const cols = groups.map(g => `
78 <div class="mega-col"> 79 <div class="mega-col-label">${g.label}</div> 80 ${g.items.map(s => `<a href="${s.href}" class="mega-link">${s.title}</a>`).join('')} 81 </div>`).join(''); 82 return `<li class="nav-item"> 83 <a href="${l.href}" class="nav-link has-sub${isActive ? ' nav-active' : ''}">${l.label}</a> 84 <div class="mega"> 85 <a href="${l.foot.href}" class="mega-head">${l.foot.title} 86 <svg width="16" height="16" viewBox="0 0 14 14" fill="none"><path d="M2.5 7h9M8 3.5l3.5 3.5L8 10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> 87 </a> 88 <div class="mega-cols">${cols}</div> 89 </div> 90 </li>`; 91 } 92 return `<li class="nav-item"><a href="${l.href}" class="nav-link${isActive ? ' nav-active' : ''}">${l.label}</a></li>`; 93 }).join(''); 94 95 document.getElementById('nav-placeholder').innerHTML = cwPaths(` 96 <nav id="nav"> 97 <div class="nav-inner"> 98 <a href="/" class="nav-logo" aria-label="Cogswell IT â zur Startseite"> 99 <img src="logo-full-dark.png" alt="Cogswell IT" class="logo-full logo-v-dark" style="height:34px;width:auto;" /> 100 <img src="logo-full-white.png" alt="" aria-hidden="true" class="logo-full logo-v-white" style="height:34px;width:auto;" /> 101 <img src="logo-icon-dark.png" alt="Cogswell IT" class="logo-icon logo-v-dark" style="height:32px;width:auto;" /> 102 <img src="logo-icon-white.png" alt="" aria-hidden="true" class="logo-icon logo-v-white" style="height:32px;width:auto;" /> 103 </a> 104 <ul class="nav-links" id="navLinks">${linksHTML}<li class="nav-mobile-cta"><a href="termin-buchen" class="nav-mobile-cta-btn" data-booking>Termin buchen</a><a href="kontakt" class="nav-mobile-cta-ghost">Kontaktieren</a><a href="https://cxhub.cogswell.net/portal" target="_blank" rel="noopener" class="nav-mobile-cta-ghost">Anmelden</a></li></ul> 105 <div class="nav-right" id="navRight"></div> 106 107 <div class="nav-lang" id="navLang" role="group" aria-label="Sprache wählen"> 108 <button type="button" class="nav-lang-btn" data-lang="de" aria-label="Deutsch">DE</button> 109 <button type="button" class="nav-lang-btn" data-lang="en" aria-label="English">EN</button> 110 </div> 111 <button class="nav-burger" id="navBurger" aria-label="Menü"> 112 <svg width="20" height="14" viewBox="0 0 20 14" fill="none"> 113 <rect y="0" width="20" height="2" rx="1" fill="currentColor"/> 114 <rect y="6" width="20" height="2" rx="1" fill="currentColor"/> 115 <rect y="12" width="20" height="2" rx="1" fill="currentColor"/> 116 </svg> 117 </button> 118 </div> 119 </nav>`); 120 121 // Smart Anmelden / Dashboard buttons (auth-aware) 122 let user = null; 123 try { user = JSON.parse(localStorage.getItem('cogswell_user') || 'null'); } catch {} 124 const navRight = document.getElementById('navRight'); 125 if (user) { 126 const dashHref = user.role === 'admin' ? 'admin-dashboard' : 'kunden-dashboard'; 127 const initial = ((user.vorname || user.name || '?')[0] || '').toUpperCase(); 128 navRight.innerHTML = cwPaths(` 129 <a href="${dashHref}" class="nav-btn-ghost" aria-label="Mein Dashboard">Dashboard</a> 130 <a href="${dashHref}" class="nav-btn-solid" style="display:inline-flex;align-items:center;gap:8px;"> 131 <span style="width:24px;height:24px;border-radius:50%;background:linear-gradient(135deg,#2fc6e0,#00728f);display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;">${initial}</span> 132 ${user.vorname || (user.name||'').split(' ')[0] || 'Konto'} 133 </a>`); 134 } else { 135 navRight.innerHTML = cwPaths(` 136 <a href="https://cxhub.cogswell.net/portal" target="_blank" rel="noopener" class="nav-btn-ghost">Anmelden</a> 137 <a href="termin-buchen" class="nav-btn-ghost" data-booking>Termin buchen</a> 138 <a href="kontakt" class="nav-btn-solid">Kontaktieren</a>`); 139 } 140 141 document.getElementById('navBurger').addEventListener('click', () => { 142 const open = document.getElementById('navLinks').classList.toggle('open'); 143 // Hintergrund einfrieren, sonst scrollt auf iOS die Seite statt des Menüs
144 document.body.classList.toggle('nav-open', open); 145 }); 146 147 // Sprachumschalter (DE/EN) â persistiert in localStorage, 148 // setzt html[lang] und sendet ein 'cogswell:langchange'-Event, 149 // damit Ãbersetzungs-Hooks später daran andocken können. 150 (function initLangSwitch() { 151 // Seiten unter /en/ sind bereits englisch â ihre Sprachauszeichnung darf der 152 // gespeicherte Umschalter-Wert nicht überschreiben (Screenreader + Suchmaschinen). 153 const onEnPage = /^\/en(\/|$)/.test(location.pathname) || /\/en\//.test(location.pathname); 154 const savedLang = onEnPage ? 'en' : (localStorage.getItem('cogswell_lang') || 'de').toLowerCase(); 155 applyLang(savedLang); 156 // Auf /en/-Seiten Nav+Footer immer übersetzen â auch ohne gespeicherte 157 // Sprachwahl (Direktaufruf); englischer Seiteninhalt bleibt unberührt, 158 // da das Dictionary nur deutsche Schlüssel kennt. 159 if (savedLang === 'en' && window.i18nApply) window.i18nApply('en'); 160 161 document.querySelectorAll('.nav-lang-btn').forEach(btn => { 162 btn.addEventListener('click', () => { 163 const next = btn.dataset.lang; 164 if (next === (localStorage.getItem('cogswell_lang') || 'de')) return; 165 applyLang(next); 166 localStorage.setItem('cogswell_lang', next); 167 if (window.i18nApply) window.i18nApply(next); 168 document.dispatchEvent(new CustomEvent('cogswell:langchange', { detail: { lang: next } })); 169 }); 170 }); 171 172 function applyLang(lang) { 173 document.documentElement.lang = lang; 174 document.querySelectorAll('.nav-lang-btn').forEach(b => { 175 b.classList.toggle('is-active', b.dataset.lang === lang); 176 b.setAttribute('aria-pressed', b.dataset.lang === lang ? 'true' : 'false'); 177 }); 178 } 179 })(); 180 181 // Mobile: tap to open mega â WAAPI-Höhe statt max-height (kein Layout-Thrash, 182 // Kurve läuft über die echte Höhe; Timer-Settle wie beim FAQ-Animator) 183 document.querySelectorAll('.nav-item .has-sub').forEach(link => { 184 var item = link.parentElement, mega = item.querySelector('.mega'); 185 var anim = null, timer = null; 186 link.addEventListener('click', (e) => { 187 if (window.innerWidth > 1080) return; 188 e.preventDefault(); 189 if (!mega) { item.classList.toggle('open'); return; } 190 var opening = !item.classList.contains('open'); 191 if (anim) { try { anim.cancel(); } catch (_) {} anim = null; } 192 clearTimeout(timer); 193 var cur = mega.getBoundingClientRect().height; 194 var target = 0; 195 if (opening) { item.classList.add('open'); target = mega.getBoundingClientRect().height; } 196 else { item.classList.remove('open'); } 197 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 198 var dur = opening ? 320 : 260; 199 anim = mega.animate({ height: [cur + 'px', target + 'px'] }, { duration: dur, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'forwards' }); 200 timer = setTimeout(function () { try { anim.cancel(); } catch (_) {} anim = null; }, dur + 20); 201 }); 202 }); 203 204 document.documentElement.setAttribute('data-theme','light'); 205 206 window.addEventListener('scroll', () => { 207 const nav = document.getElementById('nav'); 208 if (nav) nav.classList.toggle('scrolled', window.scrollY > 40); 209 }); 210 211 if (window.i18nRefresh) window.i18nRefresh(); 212} 213 214function renderFooter() { 215 document.getElementById('footer-placeholder').innerHTML = cwPaths(` 216 <footer> 217 <div class="container"> 218 <div class="footer-top"> 219 <div> 220 <a href="/" style="display:inline-flex;align-items:center;text-decoration:none;"> 221 <img src="logo-full-dark.png" alt="Cogswell IT â Where Technology Meets Innovation" style="height:32px;width:auto;display:block;" /> 222 </a> 223 <p class="footer-desc">Intelligente IT-Lösungen für Unternehmen â von KI-Beratung bis zur Prozessautomatisierung. Aus Bensheim.</p> 224 <p class="footer-desc" style="margin-top:10px;">Cogswell IT · Georg-Moller-Weg 29 · 64625 Bensheim<br /><a href="tel:+4962519743999" style="color:inherit;text-decoration:none;">+49 6251 9743999</a> · MoâFr 10â16 Uhr</p> 225 </div> 226 <div> 227 <div class="footer-col-title">Leistungen</div> 228 <nav class="footer-links"> 229 <a href="ki-beratung">KI-Beratung</a> 230 <a href="voice-agenten">KI-Voice-Agenten</a> 231 <a href="automation">Prozessautomatisierung</a> 232 <a href="terminbuchung">Terminbuchung</a> 233 <a href="webdesign">Webdesign</a> 234 <a href="karriereseiten">Recruiting & Karriereseiten</a>
235 <a href="software-entwicklung">App- & Software</a> 236 <a href="chatbot">KI-Chatbots</a> 237 <a href="it-service">MS Office & IT-Service</a> 238 </nav> 239 </div> 240 <div> 241 <div class="footer-col-title">Regionen</div> 242 <nav class="footer-links"> 243 <a href="it-dienstleister-bensheim">IT-Dienstleister Bensheim</a> 244 <a href="webdesign-bensheim">Webdesign Bensheim</a> 245 <a href="webdesign-darmstadt">Webdesign Darmstadt</a> 246 <a href="webdesign-heidelberg">Webdesign Heidelberg</a> 247 <a href="webdesign-mannheim">Webdesign Mannheim</a> 248 <a href="webdesign-bergstrasse">Webdesign BergstraÃe</a> 249 <a href="webdesign-rhein-neckar">Webdesign Rhein-Neckar</a> 250 <a href="ki-beratung-bensheim">KI-Beratung Bensheim</a> 251 <a href="ki-beratung-darmstadt">KI-Beratung Darmstadt</a> 252 <a href="ki-beratung-heidelberg">KI-Beratung Heidelberg</a> 253 <a href="ki-beratung-mannheim">KI-Beratung Mannheim</a> 254 <a href="voice-agent-bensheim">Voice-Agent Bensheim</a> 255 <a href="prozessautomatisierung-rhein-neckar">Automatisierung Rhein-Neckar</a> 256 </nav> 257 </div> 258 <div> 259 <div class="footer-col-title">Unternehmen</div> 260 <nav class="footer-links"> 261 <a href="ueber-uns">Ãber uns</a> 262 <a href="sicherheit">Sicherheit & Datenschutz</a> 263 <a href="weiterempfehlung">Weiterempfehlen & profitieren</a> 264 <a href="wartungsvertraege">Wartungsverträge</a> 265 <a href="blog">Insights</a> 266 <a href="cogswell-app">Cogswell IT App</a> 267 <a href="karriere">Karriere</a> 268 <a href="kontakt">Kontakt</a> 269 <a href="support">Support & Tickets</a> 270 </nav> 271 </div> 272 </div> 273 <div class="footer-bottom"> 274 <span>© 2026 Cogswell IT</span> 275 <div class="footer-social"> 276 <a href="https://www.linkedin.com/company/cogswell-it" target="_blank" rel="noopener" aria-label="Cogswell IT auf LinkedIn"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M4.98 3.5A2.5 2.5 0 1 1 0 3.5a2.5 2.5 0 0 1 4.98 0zM.4 8.4h4.2V24H.4zM8.4 8.4h4v2.1h.06c.56-1.06 1.93-2.18 3.97-2.18 4.25 0 5.03 2.8 5.03 6.43V24h-4.2v-7.4c0-1.77-.03-4.04-2.46-4.04-2.47 0-2.85 1.93-2.85 3.92V24H8.4z"/></svg></a> 277 <a href="https://www.facebook.com/cogswellit" target="_blank" rel="noopener" aria-label="Cogswell IT auf Facebook"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.96h-1.51c-1.49 0-1.96.93-1.96 1.89v2.26h3.33l-.53 3.49h-2.8V24C19.61 23.1 24 18.1 24 12.07z"/></svg></a> 278 <a href="https://www.instagram.com/cogswell.it" target="_blank" rel="noopener" aria-label="Cogswell IT auf Instagram"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="2.5" y="2.5" width="19" height="19" rx="5.5"/><circle cx="12" cy="12" r="4.2"/><circle cx="17.6" cy="6.4" r="1.2" fill="currentColor" stroke="none"/></svg></a> 279 </div> 280 <div class="footer-legal"> 281 <a href="kuendigung">Vertrag kündigen</a> 282 <a href="impressum">Impressum</a> 283 <a href="datenschutz">Datenschutz</a> 284 <a href="agb">AGB</a> 285 <a href="barrierefreiheit">Barrierefreiheit</a> 286 <a href="cookies">Cookie-Richtlinie</a> 287 <a href="#" onclick="event.preventDefault(); if(window.cogswellOpenConsent) window.cogswellOpenConsent();">Cookie-Einstellungen</a> 288 </div> 289 </div> 290 </div> 291 </footer>`); 292 if (window.i18nRefresh) window.i18nRefresh(); 293} 294 295// Scroll reveal 296function initReveal() { 297 const obs = new IntersectionObserver(entries => { 298 entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('in'); }); 299 }, { threshold: 0.07, rootMargin: '0px 0px -30px 0px' }); 300 document.querySelectorAll('.reveal, .reveal-mask').forEach(el => obs.observe(el)); 301} 302 303// Magnetic CTA 304function initMagnetic() { 305 document.querySelectorAll('.magnetic').forEach(el => { 306 el.addEventListener('mousemove', e => { 307 const r = el.getBoundingClientRect(); 308 const x = e.clientX - r.left - r.width/2; 309 const y = e.clientY - r.top - r.height/2; 310 el.style.transform = `translate(${x*0.25}px, ${y*0.35}px)`; 311 }); 312 el.addEventListener('mouseleave', () => el.style.transform = ''); 313 }); 314} 315
316// Mouse-tracked card glow 317function initGlowCards() { 318 document.querySelectorAll('.glow-card').forEach(el => { 319 el.addEventListener('mousemove', e => { 320 const r = el.getBoundingClientRect(); 321 el.style.setProperty('--mx', (e.clientX - r.left) + 'px'); 322 el.style.setProperty('--my', (e.clientY - r.top) + 'px'); 323 }); 324 }); 325} 326 327// 3D tilt 328function initTilt() { 329 document.querySelectorAll('.tilt').forEach(el => { 330 el.addEventListener('mousemove', e => { 331 const r = el.getBoundingClientRect(); 332 const x = (e.clientX - r.left) / r.width - 0.5; 333 const y = (e.clientY - r.top) / r.height - 0.5; 334 el.style.transform = `perspective(900px) rotateY(${x*8}deg) rotateX(${-y*8}deg)`; 335 }); 336 el.addEventListener('mouseleave', () => el.style.transform = ''); 337 }); 338} 339 340// Count-up numbers 341function initCountUp() { 342 const obs = new IntersectionObserver(entries => { 343 entries.forEach(e => { 344 if (!e.isIntersecting || e.target.dataset.counted) return; 345 e.target.dataset.counted = '1'; 346 const target = parseFloat(e.target.dataset.count); 347 const suffix = e.target.dataset.suffix || ''; 348 const dur = 1400; const t0 = performance.now(); 349 const tick = (t) => { 350 const p = Math.min(1, (t-t0)/dur); 351 const eased = 1 - Math.pow(1-p, 3); 352 const v = target * eased; 353 e.target.textContent = (target % 1 === 0 ? Math.round(v) : v.toFixed(1)) + suffix; 354 if (p < 1) requestAnimationFrame(tick); 355 }; 356 requestAnimationFrame(tick); 357 }); 358 }, { threshold: 0.5 }); 359 document.querySelectorAll('[data-count]').forEach(el => obs.observe(el)); 360} 361 362 363// FAQ-Akkordeons: animiert öffnen/schlieÃen, unterbrechbar, startet immer vom Ist-Zustand 364function initAccordions() { 365 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 366 var EASE = 'cubic-bezier(.22,1,.36,1)'; 367 document.querySelectorAll('.wdx-faq details, details.rp-faq, .rp-faq details, .sp-faq details').forEach(function (d) { 368 var sum = d.querySelector('summary'); 369 if (!sum || d.dataset.acc) return; 370 d.dataset.acc = '1'; 371 var closedH = d.open ? 0 : d.offsetHeight; // Höhe im zugeklappten Zustand merken 372 var anim = null, timer = null, closing = false; 373 function settle() { 374 if (closing) { d.open = false; closing = false; } 375 if (anim) { anim.cancel(); anim = null; } 376 d.style.overflow = ''; d.style.height = ''; 377 timer = null; 378 } 379 sum.addEventListener('click', function (e) { 380 e.preventDefault(); 381 var cur = d.getBoundingClientRect().height; 382 if (anim) { anim.cancel(); anim = null; } 383 if (timer) { clearTimeout(timer); timer = null; } 384 d.style.overflow = 'hidden'; 385 var opening = !d.open || closing; 386 var dur, target; 387 if (opening) { 388 closing = false; 389 d.open = true; 390 d.style.height = 'auto'; 391 target = d.getBoundingClientRect().height; 392 d.style.height = ''; 393 dur = 320; 394 } else { 395 closing = true; 396 if (!closedH) closedH = sum.getBoundingClientRect().height; 397 target = closedH; 398 dur = 260; 399 } 400 // fill:forwards hält den Endwert, bis settle() den echten Zustand setzt â 401 // Abschluss über Timer statt onfinish (feuert nicht in gedrosselten Tabs) 402 anim = d.animate({ height: [cur + 'px', target + 'px'] }, { duration: dur, easing: EASE, fill: 'forwards' }); 403 timer = setTimeout(settle, dur + 20); 404 }); 405 }); 406} 407 408/* Schwere Autoplay-Videos erst laden, wenn sie fast im Bild sind. 409 Vorher zeigt das Poster â spart Megabytes für alle, die nicht so weit scrollen. */ 410/* WCAG 2.2.2: Jedes automatisch laufende Video bekommt eine Pause-Taste. 411 Wahl wird je Sitzung gemerkt (sessionStorage) und stoppt dann auch das 412 Lazy-Autoplay. Unter prefers-reduced-motion startet ohnehin nichts â 413 die Taste erlaubt dort das bewusste Abspielen. */ 414function initVideoControls() { 415 var KEY = 'cogswell_videos_paused'; 416 var pausedPref = null; 417 try { pausedPref = sessionStorage.getItem(KEY); } catch (e) {} 418 var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 419 420 document.querySelectorAll('video[autoplay], video[data-src]').forEach(function (v) { 421 if (v.controls || v.dataset.novc) return; 422 var host = v.parentElement; 423 if (!host) return; 424 if (getComputedStyle(host).position === 'static') host.style.position = 'relative'; 425 426 var btn = document.createElement('button'); 427 btn.type = 'button'; 428 btn.className = 'cw-vc'; 429 var icoPause = '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="7" y="5.5" width="3.4" height="13" rx="1"/><rect x="13.6" y="5.5" width="3.4" height="13" rx="1"/></svg>'; 430 var icoPlay = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.2v13.6L19 12z"/></svg>'; 431 function playing() { return !v.paused && !v.ended; } 432 function render() { 433 var p = playing(); 434 btn.innerHTML = p ? icoPause : icoPlay; 435 btn.setAttribute('aria-label', p ? 'Video pausieren' : 'Video abspielen'); 436 btn.setAttribute('aria-pressed', String(!p)); 437 } 438 btn.addEventListener('click', function (e) { 439 e.preventDefault(); e.stopPropagation(); 440 if (playing()) { 441 v.pause(); 442 try { sessionStorage.setItem(KEY, '1'); } catch (err) {} 443 } else { 444 if (!v.src && v.dataset.src) { v.src = v.dataset.src; v.preload = 'auto'; } 445 v.play().catch(function () {}); 446 try { sessionStorage.removeItem(KEY); } catch (err) {} 447 } 448 }); 449 v.addEventListener('play', render); 450 v.addEventListener('pause', render); 451 host.appendChild(btn); 452 453 // Gemerkte Wahl bzw. Reduced-Motion: nicht automatisch starten 454 if (pausedPref === '1' || reduce) {
455 try { v.pause(); v.removeAttribute('autoplay'); } catch (e) {} 456 } 457 render(); 458 }); 459} 460 461function initLazyVideos() { 462 const vids = document.querySelectorAll('video[data-src]'); 463 if (!vids.length) return; 464 if (window.matchMedia('(prefers-reduced-motion:reduce)').matches) return; 465 try { if (sessionStorage.getItem('cogswell_videos_paused') === '1') return; } catch (e) {} 466 const io = new IntersectionObserver(es => es.forEach(e => { 467 if (!e.isIntersecting) return; 468 const v = e.target; 469 io.unobserve(v); 470 v.src = v.dataset.src; 471 v.preload = 'auto'; 472 v.play().catch(() => {}); 473 }), { rootMargin: '300px 0px' }); 474 vids.forEach(v => io.observe(v)); 475} 476 477document.addEventListener('DOMContentLoaded', () => { 478 initLazyVideos(); 479 initVideoControls(); 480 // Apple §14: Autoplay-Bewegtbild respektiert Reduced-Motion 481 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 482 document.querySelectorAll('video[autoplay]').forEach(v => { try { v.pause(); v.removeAttribute('autoplay'); } catch (e) {} }); 483 } 484 // E-Mail-Adressen entschlüsseln (Bot-Schutz: Adressen stehen nur 485 // base64-kodiert im HTML; data-eml = Text+href, -href = nur Link, -v = Feldwert) 486 function initEmailCloak() { 487 const dec = b => { try { return atob(b); } catch (e) { return ''; } }; 488 document.querySelectorAll('[data-eml]').forEach(el => { 489 const a = dec(el.getAttribute('data-eml')); 490 if (!a) return; 491 if (el.tagName === 'A') el.setAttribute('href', 'mailto:' + a); 492 el.textContent = a; 493 }); 494 document.querySelectorAll('[data-eml-href]').forEach(el => { 495 const a = dec(el.getAttribute('data-eml-href')); 496 if (a) el.setAttribute('href', 'mailto:' + a); 497 }); 498 document.querySelectorAll('input[data-eml-v]').forEach(el => { 499 const a = dec(el.getAttribute('data-eml-v')); 500 if (a) el.value = a; 501 }); 502 } 503 initReveal(); initMagnetic(); initGlowCards(); initTilt(); initCountUp(); initAccordions(); 504 initEmailCloak(); 505 // Conversion-Modul (Termin-Popup, Bewertungs-Band, Exit-Hinweis) 506 if (!document.querySelector('script[src*="conversion.js"]')) { 507 const cs = document.createElement('script'); 508 // CW_BASE, damit das Modul auch aus Unterordnern wie /blog/ lädt 509 cs.src = CW_BASE + 'conversion.js?v=20260827k14'; 510 document.body.appendChild(cs); 511 } 512 // Turnstile-Guard sitewide: auch Seiten ohne eigenes Formular POSTen 513 // (Exit-Hinweis â lead), der Fetch-Guard hängt die Tokens an 514 if (!document.querySelector('script[src*="turnstile-config.js"]')) { 515 const tc = document.createElement('script'); 516 tc.src = CW_BASE + 'turnstile-config.js?v=20260828t4'; 517 tc.onload = () => { 518 if (document.querySelector('script[src*="challenges.cloudflare.com"]')) return; 519 const cf = document.createElement('script'); 520 cf.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=cogswellTurnstileInit'; 521 cf.defer = true; 522 document.body.appendChild(cf); 523 }; 524 document.body.appendChild(tc); 525 } 526});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.