/* ========================================================================== Olzinger – Hauptskript Navigation, mobiles Menü, dezente Einblendungen, Leistungs-Karussell (mobil) und Kartenstapel (Desktop). Bewusst ohne Bibliotheken. ========================================================================== */ (function (w, d) { 'use strict'; var html = d.documentElement; var $ = function (s, c) { return (c || d).querySelector(s); }; var $$ = function (s, c) { return Array.prototype.slice.call((c || d).querySelectorAll(s)); }; var reduce = w.matchMedia('(prefers-reduced-motion: reduce)').matches; var hasIO = 'IntersectionObserver' in w; w.__olzReady = true; /* ---------- Dynamische Jahreszahlen ---------- */ var nowYear = new Date().getFullYear(); $$('[data-current-year]').forEach(function (el) { el.textContent = nowYear; }); $$('[data-years]').forEach(function (el) { el.textContent = nowYear - 1985; }); /* ---------- Wärmebild (ruhiger Hintergrund im Hero) ---------- */ var canvas = $('[data-thermal]'); if (canvas && w.OlzThermal) { var thermal = w.OlzThermal.init(canvas, { hero: $('[data-hero]'), calm: $('.hero__aside'), reduceMotion: reduce }); if (thermal) thermal.heatUp(); } else if (canvas) { html.classList.add('no-webgl'); } /* ---------- Mobiles Menü ---------- */ var menu = $('[data-menu]'); var toggle = $('[data-menu-toggle]'); var menuOpen = false; if (menu) { menu.hidden = false; menu.setAttribute('inert', ''); menu.setAttribute('aria-hidden', 'true'); } function setMenu(open) { if (!menu || !toggle || open === menuOpen) return; menuOpen = open; menu.classList.toggle('is-open', open); if (open) menu.removeAttribute('inert'); else menu.setAttribute('inert', ''); menu.setAttribute('aria-hidden', open ? 'false' : 'true'); toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); toggle.querySelector('.sr-only').textContent = open ? 'Menü schließen' : 'Menü öffnen'; html.classList.toggle('menu-open', open); // Inhalt hinter dem Menü für Tastatur und Screenreader sperren [d.getElementById('main'), $('.footer')].forEach(function (el) { if (el) el.inert = open; }); } if (toggle) toggle.addEventListener('click', function () { setMenu(!menuOpen); }); d.addEventListener('keydown', function (e) { if (e.key === 'Escape' && menuOpen) { setMenu(false); toggle.focus(); } }); // Wird das Fenster breit genug für die Desktop-Navigation (z. B. Tablet drehen), Menü schließen var deskMQ = w.matchMedia('(min-width: 1181px)'); var onDesk = function (e) { if (e.matches) setMenu(false); }; if (deskMQ.addEventListener) deskMQ.addEventListener('change', onDesk); else if (deskMQ.addListener) deskMQ.addListener(onDesk); /* ---------- Anker-Links: Menü schließen, Thema ans Formular geben ---------- */ d.addEventListener('click', function (e) { var a = e.target.closest('a[href*="#"]'); if (!a) return; if (a.dataset.topic) w.dispatchEvent(new CustomEvent('olz:topic', { detail: a.dataset.topic })); if (menuOpen) setMenu(false); }); /* ---------- Navigation: Hintergrund ab Scroll, beim Runterscrollen ausblenden ---------- */ var nav = $('[data-nav]'); if (nav) { var lastY = w.scrollY, ticking = false; var onScroll = function () { ticking = false; var y = w.scrollY; nav.classList.toggle('is-scrolled', y > 40); if (!menuOpen) { if (y > 420 && y > lastY + 4) nav.classList.add('is-hidden'); else if (y < lastY - 4 || y <= 420) nav.classList.remove('is-hidden'); } lastY = y; }; w.addEventListener('scroll', function () { if (!ticking) { ticking = true; w.requestAnimationFrame(onScroll); } }, { passive: true }); nav.addEventListener('focusin', function () { nav.classList.remove('is-hidden'); }); onScroll(); } // Aktiven Menüpunkt markieren var navLinks = $$('[data-nav-link]'); if (hasIO && navLinks.length) { var linkFor = {}; navLinks.forEach(function (l) { linkFor[l.getAttribute('href').slice(1)] = l; }); var activeIO = new IntersectionObserver(function (entries) { entries.forEach(function (en) { var link = linkFor[en.target.id]; if (link) link.classList.toggle('is-active', en.isIntersecting); }); }, { rootMargin: '-45% 0px -54% 0px' }); Object.keys(linkFor).forEach(function (id) { var sec = d.getElementById(id); if (sec) activeIO.observe(sec); }); } /* ---------- Dezente Einblendungen ---------- */ var reveals = $$('[data-reveal]'); if (hasIO && !reduce) { var revealIO = new IntersectionObserver(function (entries) { entries.forEach(function (en) { if (!en.isIntersecting) return; en.target.classList.add('is-in'); revealIO.unobserve(en.target); }); }, { rootMargin: '0px 0px -8% 0px' }); reveals.forEach(function (el) { revealIO.observe(el); }); } else { reveals.forEach(function (el) { el.classList.add('is-in'); }); } /* ---------- Leistungen: Karussell (mobil) bzw. Kartenstapel (Desktop) ---------- */ var stack = $('[data-stack]'); var cards = $$('.svc'); var stackNav = $('[data-stack-nav]'); var stackMQ = w.matchMedia('(min-width: 900px) and (min-height: 660px)'); if (stack && stackNav) { var prevBtn = $('[data-stack-prev]', stackNav); var nextBtn = $('[data-stack-next]', stackNav); var currentEl = $('[data-stack-current]', stackNav); var current = 0; var isCarousel = function () { return stack.scrollWidth > stack.clientWidth + 2; }; var cardLeft = function (c) { return c.offsetLeft - cards[0].offsetLeft; }; var cardIndex = function () { var x = stack.scrollLeft, best = 0, bestDist = Infinity; if (x >= stack.scrollWidth - stack.clientWidth - 2) return cards.length - 1; cards.forEach(function (c, i) { var dist = Math.abs(cardLeft(c) - x); if (dist < bestDist) { bestDist = dist; best = i; } }); return best; }; var updateNav = function () { stackNav.hidden = !isCarousel(); current = cardIndex(); currentEl.textContent = current + 1; prevBtn.disabled = current === 0; nextBtn.disabled = current === cards.length - 1; }; // Schritt relativ zur echten Scrollposition (bei zwei Karten nebeneinander // liegen die letzten Karten jenseits der maximalen Scrollweite) var step = function (dir) { var x = stack.scrollLeft, i; if (dir > 0) { for (i = 0; i < cards.length - 1; i++) if (cardLeft(cards[i]) > x + 2) break; } else { for (i = cards.length - 1; i > 0; i--) if (cardLeft(cards[i]) < x - 2) break; } stack.scrollTo({ left: Math.min(cardLeft(cards[i]), stack.scrollWidth - stack.clientWidth), behavior: reduce ? 'auto' : 'smooth' }); }; var navTick = false; stack.addEventListener('scroll', function () { if (!navTick) { navTick = true; w.requestAnimationFrame(function () { navTick = false; updateNav(); }); } }, { passive: true }); prevBtn.addEventListener('click', function () { step(-1); }); nextBtn.addEventListener('click', function () { step(1); }); w.addEventListener('resize', updateNav); updateNav(); } // Desktop: hintere Karten treten beim Überdecken leicht zurück if (stack && !reduce) { var inners = cards.map(function (c) { return $('.svc__inner', c); }); var stackTick = false; var paintStack = function () { stackTick = false; if (!stackMQ.matches) { inners.forEach(function (el) { el.style.transform = ''; el.style.removeProperty('--shade'); }); return; } var vh = w.innerHeight; cards.forEach(function (card, i) { var next = cards[i + 1]; if (!next) return; var stickyTop = parseFloat(getComputedStyle(next).top) || 100; var start = vh * 0.85; var p = (start - next.getBoundingClientRect().top) / (start - stickyTop); p = Math.max(0, Math.min(1, p)); inners[i].style.transform = p ? 'scale(' + (1 - p * (0.1 - i * 0.012)) + ')' : ''; inners[i].style.setProperty('--shade', (p * 0.5).toFixed(3)); }); }; var requestPaint = function () { if (!stackTick) { stackTick = true; w.requestAnimationFrame(paintStack); } }; w.addEventListener('scroll', requestPaint, { passive: true }); w.addEventListener('resize', requestPaint); paintStack(); } })(window, document);