PageSourceSearch

https://www.cogswell.de/components.js?v=20260911v46

js cogswell.de collected 2026-09-24 13:40:39 UTC 28,157 bytes, 526 lines download raw bytes

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 &amp; 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- &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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 &amp; Karriereseiten</a>
235            <a href="software-entwicklung">App- &amp; Software</a>
236            <a href="chatbot">KI-Chatbots</a>
237            <a href="it-service">MS Office &amp; 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 &amp; Datenschutz</a>
263            <a href="weiterempfehlung">Weiterempfehlen &amp; 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 &amp; 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.