Maßgeschneiderte Website für Olzinger Haustechnik
Neuer statischer Webauftritt für die Olzinger GmbH (Heizung, Sanitär, Lüftung, regenerative Energien – Ergolding bei Landshut, seit 1985). - Hero mit live berechnetem WebGL-„Wärmebild“: Maus als Wärmequelle, Wärmespur, Isothermen und Temperaturanzeige; Preloader „Thermostat“ - Gestapelte Leistungskarten mit animierten SVG-Illustrationen - Interaktives Energie-Haus (Wärmepumpe, Solarthermie, Speicher, Fußbodenheizung, Bad, Lüftung) mit Tabs, Hotspots und Autoplay - Heizungs-Finder mit Empfehlung und Übernahme ins Kontaktformular - Ablauf als sich füllende Rohrleitung, Zeitleiste mit Zählwerk, Live-Öffnungsstatus inkl. bayerischer Feiertage - Kontaktformular mit PHP-Versand (Honeypot, Zeitfalle) und mailto-Fallback; Impressum, Datenschutz, 404-Seite - Smooth-Scroll (Lenis) und GSAP-Animationen, alles lokal gehostet; reduzierte Bewegung, Betrieb ohne JavaScript, axe ohne Befunde - SEO: Schema.org HVACBusiness, Open-Graph-Bild, Sitemap, robots.txt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kjd7UMh3HKyVziCgiZ9A2i
This commit is contained in:
34 files changed
+5377
No files matched your search
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
node_modules/
|
||||
*.log
|
||||
@@ -0,0 +1,33 @@
|
||||
# Olzinger Haustechnik – Apache-Konfiguration (optional)
|
||||
# Alle Blöcke sind abgesichert, damit fehlende Module keinen Serverfehler auslösen.
|
||||
|
||||
ErrorDocument 404 /404.html
|
||||
|
||||
<IfModule mod_mime.c>
|
||||
AddType font/woff2 .woff2
|
||||
AddType image/svg+xml .svg
|
||||
AddType application/manifest+json .webmanifest
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
Header always set X-Content-Type-Options "nosniff"
|
||||
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Header always set X-Frame-Options "SAMEORIGIN"
|
||||
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/html "access plus 0 seconds"
|
||||
ExpiresByType text/css "access plus 1 week"
|
||||
ExpiresByType application/javascript "access plus 1 week"
|
||||
ExpiresByType text/javascript "access plus 1 week"
|
||||
ExpiresByType font/woff2 "access plus 1 year"
|
||||
ExpiresByType image/svg+xml "access plus 1 month"
|
||||
ExpiresByType image/png "access plus 1 month"
|
||||
ExpiresByType image/jpeg "access plus 1 month"
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_deflate.c>
|
||||
AddOutputFilterByType DEFLATE text/html text/css application/javascript text/javascript image/svg+xml application/json
|
||||
</IfModule>
|
||||
@@ -0,0 +1,96 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Seite nicht gefunden – Olzinger Haustechnik</title>
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="theme-color" content="#0B151E">
|
||||
<!-- Absolute Pfade, damit die Seite auch bei verschachtelten URLs korrekt lädt -->
|
||||
<link rel="icon" href="/assets/img/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/assets/css/main.css">
|
||||
<style>
|
||||
.e404 { min-height: 100vh; min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; background: var(--ink); color: var(--paper); position: relative; overflow: hidden; isolation: isolate; }
|
||||
.e404::before { content: ""; position: absolute; inset: 0; z-index: -1; background: radial-gradient(45% 60% at 80% 30%, rgba(255,106,43,.35), rgba(138,35,80,.16) 50%, transparent 75%), radial-gradient(40% 55% at 10% 90%, rgba(46,155,239,.22), transparent 70%); }
|
||||
.e404__top { display: flex; justify-content: space-between; align-items: center; padding: 22px var(--gutter); }
|
||||
.e404__main { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(24px, 5vw, 80px); align-items: center; padding: 24px var(--gutter) 48px; max-width: calc(var(--container) + var(--gutter) * 2); margin: 0 auto; width: 100%; }
|
||||
.e404__code { font-family: var(--font-mono); font-size: 14px; color: var(--text-dark-2); margin: 0 0 20px; }
|
||||
.e404 .h1 { margin-bottom: 24px; }
|
||||
.e404 .lead { color: var(--text-dark-2); margin-bottom: 32px; }
|
||||
.e404__ctas { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
.e404__art { width: 100%; max-width: 560px; justify-self: center; }
|
||||
.e404__foot { padding: 20px var(--gutter); font-size: 14px; color: var(--text-dark-2); border-top: 1px solid var(--line-dark); }
|
||||
.drip { animation: drip 1.8s cubic-bezier(.55, 0, .9, .45) infinite; }
|
||||
.drip-2 { animation-delay: .9s; }
|
||||
.splash { transform-box: fill-box; transform-origin: center; animation: splash 1.8s ease-out infinite; animation-delay: 1.05s; }
|
||||
.splash-2 { animation-delay: 1.95s; }
|
||||
@keyframes drip { 0% { transform: translateY(0) scale(.4); opacity: 0; } 12% { transform: translateY(0) scale(1); opacity: 1; } 62% { transform: translateY(104px); opacity: 1; } 64%, 100% { transform: translateY(104px); opacity: 0; } }
|
||||
@keyframes splash { 0% { transform: scale(.3); opacity: 0; } 8% { opacity: 1; } 60%, 100% { transform: scale(2.6); opacity: 0; } }
|
||||
@media (max-width: 860px) { .e404__main { grid-template-columns: minmax(0, 1fr); } .e404__art { order: -1; max-width: 420px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="e404">
|
||||
<header class="e404__top">
|
||||
<a class="logo" href="/" aria-label="Olzinger Haustechnik – zur Startseite">
|
||||
<svg class="logo__mark" viewBox="0 0 40 40" aria-hidden="true">
|
||||
<defs><linearGradient id="g-logo" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#2E9BEF"/><stop offset=".55" stop-color="#C2456B"/><stop offset="1" stop-color="#FF6A2B"/></linearGradient></defs>
|
||||
<path d="M11.97 31.47A14 14 0 1 1 28.03 31.47" fill="none" stroke="url(#g-logo)" stroke-width="5.5" stroke-linecap="round"/>
|
||||
<circle cx="20" cy="20" r="3.4" fill="currentColor"/>
|
||||
</svg>
|
||||
<span class="logo__text"><span class="logo__word">Olzinger</span><span class="logo__sub">Haustechnik</span></span>
|
||||
</a>
|
||||
<a class="btn btn--sm btn--warm" href="tel:+4987172617"><span class="btn__label">0871 72617</span></a>
|
||||
</header>
|
||||
|
||||
<main class="e404__main" id="main">
|
||||
<div>
|
||||
<p class="e404__code">Fehler 404</p>
|
||||
<h1 class="h1">Kein Anschluss <em class="accent" style="color:var(--amber)">unter dieser Adresse.</em></h1>
|
||||
<p class="lead">Diese Seite gibt es nicht – oder nicht mehr. Kein Grund zur Sorge: Wir bringen Sie zurück auf den richtigen Weg.</p>
|
||||
<div class="e404__ctas">
|
||||
<a class="btn btn--light" href="/">
|
||||
<span class="btn__label">Zur Startseite</span>
|
||||
<span class="btn__icon" aria-hidden="true"><svg class="icon" viewBox="0 0 24 24"><path d="M4 12h15M13 6l6 6-6 6"/></svg></span>
|
||||
</a>
|
||||
<a class="btn btn--ghost btn--ghost-light" href="/#kontakt"><span class="btn__label">Kontakt aufnehmen</span></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg class="e404__art" viewBox="0 0 520 300" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="g-pipe" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#2E9BEF"/><stop offset=".6" stop-color="#C2456B"/><stop offset="1" stop-color="#FF6A2B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g-pipe2" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#FF6A2B"/><stop offset="1" stop-color="#FFB547"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g-shine" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".35"/><stop offset=".5" stop-color="#fff" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M0 40V260" stroke="rgba(255,255,255,.14)" stroke-width="2" stroke-dasharray="4 8"/>
|
||||
<rect x="14" y="70" width="20" height="76" rx="5" fill="#1B2C3B" stroke="rgba(255,255,255,.25)"/>
|
||||
<path d="M34 86H312l-10 9 12 7-11 8 11 8-12 8 10 8H34Z" fill="url(#g-pipe)"/>
|
||||
<path d="M34 86H312l-10 9H34Z" fill="url(#g-shine)"/>
|
||||
<rect x="150" y="76" width="26" height="66" rx="7" fill="#0F1D29" stroke="rgba(255,255,255,.3)"/>
|
||||
<rect x="236" y="76" width="26" height="66" rx="7" fill="#0F1D29" stroke="rgba(255,255,255,.3)"/>
|
||||
<g transform="translate(398 112) rotate(14)">
|
||||
<path d="M-8 -26h86v52H-8l10-8-12-8 11-8-11-8 12-7z" fill="url(#g-pipe2)" opacity=".9"/>
|
||||
</g>
|
||||
<text x="300" y="58" fill="rgba(255,255,255,.55)" font-family="ui-monospace, Menlo, Consolas, monospace" font-size="13" letter-spacing="1.5">ANSCHLUSS FEHLT</text>
|
||||
<path d="M296 64v14" stroke="rgba(255,255,255,.35)" stroke-width="1.5"/>
|
||||
<g>
|
||||
<path class="drip" d="M305 140c0 0 9 11 9 17a9 9 0 0 1-18 0c0-6 9-17 9-17z" fill="#8ED9FA"/>
|
||||
<path class="drip drip-2" d="M305 140c0 0 9 11 9 17a9 9 0 0 1-18 0c0-6 9-17 9-17z" fill="#8ED9FA"/>
|
||||
</g>
|
||||
<ellipse cx="305" cy="270" rx="78" ry="10" fill="rgba(46,155,239,.22)"/>
|
||||
<ellipse class="splash" cx="305" cy="270" rx="16" ry="4" fill="none" stroke="#8ED9FA" stroke-width="1.6"/>
|
||||
<ellipse class="splash splash-2" cx="305" cy="270" rx="16" ry="4" fill="none" stroke="#8ED9FA" stroke-width="1.6"/>
|
||||
</svg>
|
||||
</main>
|
||||
|
||||
<footer class="e404__foot">© Olzinger GmbH · Ahornstraße 112 · 84030 Ergolding · <a href="/impressum.html">Impressum</a> · <a href="/datenschutz.html">Datenschutz</a></footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,93 @@
|
||||
# Olzinger Haustechnik – Website
|
||||
|
||||
Maßgeschneiderter Neuauftritt für die **Olzinger GmbH** (Heizung · Sanitär · Lüftung · Regenerative Energien, Ergolding bei Landshut, seit 1985).
|
||||
|
||||
Statische Website ohne Build-Schritt: HTML, CSS und JavaScript, alle Bibliotheken und Schriften lokal eingebunden (kein CDN, keine Cookies, kein Tracking).
|
||||
|
||||
## Gestaltungsidee: „Wärmebild“
|
||||
|
||||
Haustechnik sieht man nicht – man spürt sie. Deshalb zeigt der Einstieg ein **live berechnetes Wärmebild** (WebGL) mit Isothermen wie bei einer Thermografie-Kamera. Die Maus wirkt als Wärmequelle, hinterlässt eine Wärmespur, und ein Messfadenkreuz zeigt die „Temperatur“ an. Durch die ganze Seite zieht sich das Motiv **warm trifft kalt** (Vorlauf/Rücklauf): Farbverlauf Blau → Magenta → Orange, das Logo als Thermostat-Skala, Bänder in Rücklauf-Blau und Vorlauf-Orange.
|
||||
|
||||
## Bereiche der Startseite
|
||||
|
||||
| Bereich | Besonderheit |
|
||||
|---|---|
|
||||
| Preloader | Thermostat-Bogen „dreht hoch“, Jahreszahl zählt von 1985 bis heute (nur beim ersten Aufruf) |
|
||||
| Hero | WebGL-Wärmebild, Live-Öffnungsstatus (inkl. bayerischer Feiertage) |
|
||||
| Bänder | Endlos-Laufschrift, reagiert auf Scroll-Tempo und -Richtung |
|
||||
| Intro | Text „heizt sich“ Wort für Wort beim Scrollen auf |
|
||||
| Leistungen | 5 gestapelte Karten (Sticky-Stack) mit animierten Illustrationen: Wärmepumpe, Heizkörper, Waschtisch, Wärmetauscher, Manometer |
|
||||
| Energie-Haus | Interaktiver Hausschnitt: Wärmepumpe, Solarthermie, Speicher, Fußbodenheizung, Bad, Lüftung – mit fließenden Medien in den Leitungen |
|
||||
| Heizungs-Finder | 4 Fragen → Empfehlung (Wärmepumpe / Pellets / Hybrid) mit Passungsbalken; Ergebnis lässt sich ins Kontaktformular übernehmen |
|
||||
| Ablauf | Rohrleitung füllt sich beim Scrollen |
|
||||
| Über uns | Zähler, Zeitleiste mit Zählwerk-Jahreszahl (1985 → heute), Geschäftsführung |
|
||||
| Karriere | Ausbildung Anlagenmechaniker/in SHK, Initiativbewerbung |
|
||||
| Kontakt | Kontaktkarten, Öffnungszeiten mit „Heute“-Markierung, Formular |
|
||||
| Footer | Schriftzug „heizt auf“ beim Scrollen |
|
||||
|
||||
Außerdem: `impressum.html`, `datenschutz.html`, `404.html` („Kein Anschluss unter dieser Adresse“), weicher Seitenwechsel, SEO-Daten (Schema.org `HVACBusiness`, Open-Graph-Bild, Sitemap).
|
||||
|
||||
## Lokal ansehen
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000
|
||||
# dann http://localhost:8000 öffnen
|
||||
```
|
||||
|
||||
`index.html` lässt sich auch direkt per Doppelklick öffnen; das Kontaktformular nutzt dann den E-Mail-Fallback.
|
||||
|
||||
## Veröffentlichen
|
||||
|
||||
Den kompletten Ordnerinhalt auf den Webspace laden (z. B. per FTP). Voraussetzungen:
|
||||
|
||||
- Beliebiger Webserver; für den direkten Formularversand **PHP 7.4+** mit funktionierender `mail()`-Funktion (`kontakt.php`).
|
||||
- Ohne PHP funktioniert das Formular trotzdem: Es öffnet dann das E-Mail-Programm des Besuchers mit vorbereiteter Nachricht.
|
||||
- `.htaccess` (Apache) setzt die 404-Seite, Caching und Sicherheits-Header. Bei anderen Servern einfach ignorieren.
|
||||
- `404.html` nutzt absolute Pfade (`/assets/...`) – liegt die Seite in einem Unterordner, Pfade dort anpassen.
|
||||
|
||||
## Vor dem Livegang prüfen (TODO)
|
||||
|
||||
Die Firmendaten stammen aus öffentlich zugänglichen Quellen. Bitte bestätigen bzw. ergänzen:
|
||||
|
||||
- [ ] **Impressum:** Umsatzsteuer-ID und berufsrechtliche Angaben (Kammer, Berufsbezeichnung) – als Kommentar in `impressum.html` vorbereitet.
|
||||
- [ ] **Datenschutz:** Hosting-Anbieter, Speicherdauer der Server-Logs, Vertrag zur Auftragsverarbeitung – als `TODO` in `datenschutz.html` markiert. Rechtstexte sind Entwürfe und ersetzen keine Rechtsberatung.
|
||||
- [ ] **Inhalte:** Mitarbeiterzahl (≈ 25), Ausbildungsstart (September), Leistungsliste, Texte zur Karriere.
|
||||
- [ ] **Formular:** In `kontakt.php` Empfänger/Absender prüfen (`RECIPIENT`, `SENDER`); einmal testweise absenden.
|
||||
- [ ] Optional: echte Fotos (Team, Projekte, Bäder) ergänzen – die Illustrationen sind so gestaltet, dass sie auch ohne Fotos tragen.
|
||||
|
||||
## Struktur
|
||||
|
||||
```
|
||||
index.html Startseite
|
||||
impressum.html Impressum
|
||||
datenschutz.html Datenschutzerklärung
|
||||
404.html Fehlerseite
|
||||
kontakt.php Formularversand (optional, PHP)
|
||||
assets/css/main.css Design-System und alle Styles
|
||||
assets/js/thermal.js WebGL-Wärmebild
|
||||
assets/js/main.js Preloader, Smooth-Scroll, Navigation, Scroll-Animationen
|
||||
assets/js/house.js Energie-Haus (Tabs, Hotspots, Autoplay)
|
||||
assets/js/finder.js Heizungs-Finder (Bewertungslogik + Ergebnis)
|
||||
assets/js/contact.js Öffnungszeiten live, Feiertage, Kontaktformular
|
||||
assets/vendor/ GSAP, ScrollTrigger, SplitText, Lenis (lokal)
|
||||
assets/fonts/ Instrument Sans & Instrument Serif (OFL, lokal)
|
||||
assets/img/ Favicon, Logo, Open-Graph-Bild
|
||||
tools/og-template.html Vorlage zum Neu-Rendern des Open-Graph-Bilds
|
||||
```
|
||||
|
||||
## Anpassen
|
||||
|
||||
- **Farben, Abstände, Schriften:** Design-Tokens am Anfang von `assets/css/main.css` (`--warm`, `--cool`, `--ink`, `--paper` …).
|
||||
- **Öffnungszeiten:** in `assets/js/contact.js` (`SCHEDULE`) sowie in `index.html` (Text und Schema.org-Daten).
|
||||
- **Heizungs-Finder:** Gewichtung in `assets/js/finder.js` (`evaluate`), Texte in `TEXTS`.
|
||||
- **Open-Graph-Bild:** `tools/og-template.html` im Browser öffnen (über einen lokalen Server) und einen Screenshot mit 1200 × 630 px als `assets/img/og-image.jpg` speichern.
|
||||
|
||||
## Qualität
|
||||
|
||||
- Barrierefreiheit: geprüft mit axe-core (WCAG 2.1 AA) – keine Befunde; Tastaturbedienung für Menü, Tabs und Finder; `prefers-reduced-motion` wird respektiert (keine Smooth-Scroll-, Preloader- oder Scroll-Animationen).
|
||||
- Ohne JavaScript bleiben alle Inhalte sichtbar und nutzbar.
|
||||
- Getestet in Chromium: Desktop (1366, 1440, 1920 px), Tablet (834 px) und Smartphone (390 px).
|
||||
|
||||
## Lizenzen
|
||||
|
||||
GSAP 3.15 (Standard „No Charge“ License), Lenis (MIT), Instrument Sans/Serif (SIL OFL 1.1) – Details in `assets/vendor/LICENSES.md` und `assets/fonts/`.
|
||||
+1466
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) InstrumentSans-Italic[wdth,wght].ttf: Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2022 The Instrument Serif Project Authors (https://github.com/Instrument/instrument-serif) InstrumentSerif-Italic.ttf: Copyright 2022 The Instrument Serif Project Authors (https://github.com/Instrument/instrument-serif)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 7.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#2E9BEF"/>
|
||||
<stop offset=".55" stop-color="#C2456B"/>
|
||||
<stop offset="1" stop-color="#FF6A2B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="64" height="64" rx="16" fill="#0B151E"/>
|
||||
<path d="M21.1 47.6A19 19 0 1 1 42.9 47.6" fill="none" stroke="url(#g)" stroke-width="7.5" stroke-linecap="round"/>
|
||||
<circle cx="32" cy="32" r="4.6" fill="#F3EFE8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 530 B |
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 56" role="img" aria-label="Olzinger Haustechnik">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0" stop-color="#2E9BEF"/>
|
||||
<stop offset=".55" stop-color="#C2456B"/>
|
||||
<stop offset="1" stop-color="#FF6A2B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M17.1 43.6A19 19 0 1 1 38.9 43.6" fill="none" stroke="url(#g)" stroke-width="7" stroke-linecap="round"/>
|
||||
<circle cx="28" cy="28" r="4.4" fill="#0B151E"/>
|
||||
<text x="62" y="31" font-family="Instrument Sans, Arial, sans-serif" font-weight="700" font-size="26" letter-spacing="1" fill="#0B151E">OLZINGER</text>
|
||||
<text x="63" y="47" font-family="Instrument Sans, Arial, sans-serif" font-weight="500" font-size="10.5" letter-spacing="3.2" fill="#4D5963">HAUSTECHNIK</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 833 B |
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
@@ -0,0 +1,233 @@
|
||||
/* ==========================================================================
|
||||
Olzinger – Öffnungszeiten (live, inkl. bayerischer Feiertage) & Kontaktformular
|
||||
========================================================================== */
|
||||
(function (w, d) {
|
||||
'use strict';
|
||||
|
||||
/* ---------- Öffnungszeiten ---------- */
|
||||
// Minuten seit Mitternacht; Index = Wochentag (0 = Sonntag)
|
||||
var SCHEDULE = {
|
||||
0: [],
|
||||
1: [[420, 720], [780, 990]],
|
||||
2: [[420, 720], [780, 990]],
|
||||
3: [[420, 720], [780, 990]],
|
||||
4: [[420, 720], [780, 990]],
|
||||
5: [[420, 690]],
|
||||
6: []
|
||||
};
|
||||
var DAYS = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||
|
||||
function fmt(min) {
|
||||
var h = Math.floor(min / 60), m = min % 60;
|
||||
return h + ':' + (m < 10 ? '0' : '') + m;
|
||||
}
|
||||
|
||||
function easter(y) {
|
||||
var a = y % 19, b = Math.floor(y / 100), c = y % 100, dd = Math.floor(b / 4), e = b % 4;
|
||||
var f = Math.floor((b + 8) / 25), g = Math.floor((b - f + 1) / 3);
|
||||
var h = (19 * a + b - dd - g + 15) % 30, i = Math.floor(c / 4), k = c % 4;
|
||||
var l = (32 + 2 * e + 2 * i - h - k) % 7, m = Math.floor((a + 11 * h + 22 * l) / 451);
|
||||
var month = Math.floor((h + l - 7 * m + 114) / 31), day = ((h + l - 7 * m + 114) % 31) + 1;
|
||||
return Date.UTC(y, month - 1, day);
|
||||
}
|
||||
|
||||
// Gesetzliche Feiertage in Bayern (Gemeinde mit überwiegend katholischer Bevölkerung)
|
||||
var holidayCache = {};
|
||||
function holidays(y) {
|
||||
if (holidayCache[y]) return holidayCache[y];
|
||||
var DAY = 864e5, e = easter(y), map = {};
|
||||
var add = function (t, name) { var dt = new Date(t); map[dt.getUTCMonth() + 1 + '-' + dt.getUTCDate()] = name; };
|
||||
add(Date.UTC(y, 0, 1), 'Neujahr');
|
||||
add(Date.UTC(y, 0, 6), 'Heilige Drei Könige');
|
||||
add(e - 2 * DAY, 'Karfreitag');
|
||||
add(e + 1 * DAY, 'Ostermontag');
|
||||
add(Date.UTC(y, 4, 1), 'Tag der Arbeit');
|
||||
add(e + 39 * DAY, 'Christi Himmelfahrt');
|
||||
add(e + 50 * DAY, 'Pfingstmontag');
|
||||
add(e + 60 * DAY, 'Fronleichnam');
|
||||
add(Date.UTC(y, 7, 15), 'Mariä Himmelfahrt');
|
||||
add(Date.UTC(y, 9, 3), 'Tag der Deutschen Einheit');
|
||||
add(Date.UTC(y, 10, 1), 'Allerheiligen');
|
||||
add(Date.UTC(y, 11, 25), '1. Weihnachtstag');
|
||||
add(Date.UTC(y, 11, 26), '2. Weihnachtstag');
|
||||
return (holidayCache[y] = map);
|
||||
}
|
||||
function holidayName(y, m, day) { return holidays(y)[m + '-' + day] || null; }
|
||||
|
||||
function berlinNow() {
|
||||
var now = new Date();
|
||||
try {
|
||||
var parts = new Intl.DateTimeFormat('de-DE', {
|
||||
timeZone: 'Europe/Berlin', year: 'numeric', month: 'numeric', day: 'numeric',
|
||||
hour: 'numeric', minute: 'numeric', hourCycle: 'h23'
|
||||
}).formatToParts(now);
|
||||
var get = function (t) { for (var i = 0; i < parts.length; i++) if (parts[i].type === t) return parseInt(parts[i].value, 10); return 0; };
|
||||
var y = get('year'), mo = get('month'), da = get('day');
|
||||
return { y: y, m: mo, d: da, mins: get('hour') * 60 + get('minute'), wd: new Date(Date.UTC(y, mo - 1, da)).getUTCDay() };
|
||||
} catch (e) {
|
||||
return { y: now.getFullYear(), m: now.getMonth() + 1, d: now.getDate(), mins: now.getHours() * 60 + now.getMinutes(), wd: now.getDay() };
|
||||
}
|
||||
}
|
||||
|
||||
function computeStatus() {
|
||||
var n = berlinNow();
|
||||
var hol = holidayName(n.y, n.m, n.d);
|
||||
var slots = hol ? [] : SCHEDULE[n.wd];
|
||||
var i;
|
||||
for (i = 0; i < slots.length; i++) {
|
||||
if (n.mins >= slots[i][0] && n.mins < slots[i][1]) {
|
||||
return { state: 'open', text: 'Jetzt geöffnet · bis ' + fmt(slots[i][1]) + ' Uhr', wd: n.wd };
|
||||
}
|
||||
}
|
||||
if (slots.length > 1 && n.mins >= slots[0][1] && n.mins < slots[1][0]) {
|
||||
return { state: 'break', text: 'Mittagspause · ab ' + fmt(slots[1][0]) + ' Uhr wieder erreichbar', wd: n.wd };
|
||||
}
|
||||
if (slots.length && n.mins < slots[0][0]) {
|
||||
return { state: 'closed', text: 'Geschlossen · öffnet heute um ' + fmt(slots[0][0]) + ' Uhr', wd: n.wd };
|
||||
}
|
||||
var base = Date.UTC(n.y, n.m - 1, n.d);
|
||||
for (i = 1; i <= 14; i++) {
|
||||
var dt = new Date(base + i * 864e5);
|
||||
var wd = dt.getUTCDay();
|
||||
var s = SCHEDULE[wd];
|
||||
if (!s.length || holidayName(dt.getUTCFullYear(), dt.getUTCMonth() + 1, dt.getUTCDate())) continue;
|
||||
var when = i === 1 ? 'morgen' : (i < 7 ? 'am ' + DAYS[wd] : 'am ' + dt.getUTCDate() + '.' + (dt.getUTCMonth() + 1) + '.');
|
||||
var prefix = hol ? 'Heute Feiertag (' + hol + ')' : 'Geschlossen';
|
||||
return { state: 'closed', text: prefix + ' · öffnet ' + when + ' um ' + fmt(s[0][0]) + ' Uhr', wd: n.wd };
|
||||
}
|
||||
return { state: 'closed', text: 'Geschlossen', wd: n.wd };
|
||||
}
|
||||
|
||||
var statusEls = d.querySelectorAll('[data-status]');
|
||||
var dayRows = d.querySelectorAll('[data-days]');
|
||||
|
||||
function renderStatus() {
|
||||
var st = computeStatus();
|
||||
Array.prototype.forEach.call(statusEls, function (el) {
|
||||
el.classList.remove('is-open', 'is-break', 'is-closed');
|
||||
el.classList.add('is-' + st.state);
|
||||
var t = el.querySelector('.status__text');
|
||||
if (t) t.textContent = st.text;
|
||||
});
|
||||
Array.prototype.forEach.call(dayRows, function (row) {
|
||||
var days = row.getAttribute('data-days').split(',').map(Number);
|
||||
row.classList.toggle('is-today', days.indexOf(st.wd) > -1);
|
||||
});
|
||||
}
|
||||
renderStatus();
|
||||
w.setInterval(renderStatus, 60000);
|
||||
|
||||
/* ---------- Kontaktformular ---------- */
|
||||
var form = d.querySelector('[data-contact-form]');
|
||||
if (!form) return;
|
||||
|
||||
var statusBox = form.querySelector('[data-form-status]');
|
||||
var ts = form.querySelector('[data-ts]');
|
||||
if (ts) ts.value = String(Date.now());
|
||||
|
||||
var RECIPIENT = 'kontakt@olzinger.de';
|
||||
var fields = {
|
||||
name: form.querySelector('[name="name"]'),
|
||||
email: form.querySelector('[name="email"]'),
|
||||
message: form.querySelector('[name="message"]'),
|
||||
privacy: form.querySelector('[name="privacy"]')
|
||||
};
|
||||
|
||||
Object.keys(fields).forEach(function (k) {
|
||||
var el = fields[k];
|
||||
if (!el) return;
|
||||
el.setAttribute('aria-describedby', 'e-' + k);
|
||||
el.addEventListener(el.type === 'checkbox' ? 'change' : 'input', function () {
|
||||
if (el.closest('.field').classList.contains('is-invalid')) validateField(k);
|
||||
});
|
||||
});
|
||||
|
||||
function validateField(k) {
|
||||
var el = fields[k], ok = true;
|
||||
if (k === 'email') ok = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(el.value.trim());
|
||||
else if (k === 'privacy') ok = el.checked;
|
||||
else if (k === 'message') ok = el.value.trim().length >= 5;
|
||||
else ok = el.value.trim().length > 0;
|
||||
el.closest('.field').classList.toggle('is-invalid', !ok);
|
||||
el.setAttribute('aria-invalid', ok ? 'false' : 'true');
|
||||
return ok;
|
||||
}
|
||||
|
||||
function setStatus(type, html) {
|
||||
statusBox.className = 'form__status' + (type ? ' is-' + type : '');
|
||||
statusBox.innerHTML = html || '';
|
||||
}
|
||||
|
||||
function selectTopic(topic) {
|
||||
if (!topic) return;
|
||||
var radios = form.querySelectorAll('[name="topic"]');
|
||||
Array.prototype.forEach.call(radios, function (r) { r.checked = (r.value === topic); });
|
||||
}
|
||||
|
||||
w.addEventListener('olz:topic', function (e) { selectTopic(e.detail); });
|
||||
w.addEventListener('olz:prefill', function (e) {
|
||||
var det = e.detail || {};
|
||||
selectTopic(det.topic);
|
||||
if (det.message && fields.message) {
|
||||
fields.message.value = det.message;
|
||||
fields.message.closest('.field').classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
function buildMail(data) {
|
||||
var topic = data.get('topic') || 'Allgemeine Anfrage';
|
||||
var lines = [
|
||||
'Anliegen: ' + topic,
|
||||
'Name: ' + (data.get('name') || ''),
|
||||
'E-Mail: ' + (data.get('email') || ''),
|
||||
'Telefon: ' + (data.get('phone') || '–'),
|
||||
'PLZ / Ort: ' + (data.get('place') || '–'),
|
||||
'',
|
||||
String(data.get('message') || '')
|
||||
];
|
||||
return 'mailto:' + RECIPIENT +
|
||||
'?subject=' + encodeURIComponent('Anfrage über olzinger.de – ' + topic) +
|
||||
'&body=' + encodeURIComponent(lines.join('\n'));
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var valid = ['name', 'email', 'message', 'privacy'].map(validateField);
|
||||
if (valid.indexOf(false) > -1) {
|
||||
var firstBad = form.querySelector('.is-invalid input, .is-invalid textarea');
|
||||
if (firstBad) firstBad.focus();
|
||||
setStatus('error', 'Bitte prüfen Sie die markierten Felder.');
|
||||
return;
|
||||
}
|
||||
var data = new FormData(form);
|
||||
form.classList.add('is-busy');
|
||||
setStatus('', '');
|
||||
|
||||
var done = function (ok, serverValidation) {
|
||||
form.classList.remove('is-busy');
|
||||
if (ok) {
|
||||
form.reset();
|
||||
if (ts) ts.value = String(Date.now());
|
||||
setStatus('success', '<strong>Vielen Dank!</strong> Ihre Anfrage ist bei uns angekommen. Wir melden uns schnellstmöglich bei Ihnen.');
|
||||
} else if (serverValidation) {
|
||||
setStatus('error', 'Bitte prüfen Sie Ihre Angaben – einige Felder sind unvollständig.');
|
||||
} else {
|
||||
w.location.href = buildMail(data);
|
||||
setStatus('info', '<strong>Fast geschafft:</strong> Ihr E-Mail-Programm öffnet sich mit Ihrer vorbereiteten Anfrage – bitte dort nur noch auf „Senden“ klicken. Alternativ erreichen Sie uns unter <a href="tel:+4987172617">0871 72617</a>.');
|
||||
}
|
||||
};
|
||||
|
||||
if (!/^https?:$/.test(w.location.protocol) || !w.fetch) { done(false); return; }
|
||||
|
||||
w.fetch(form.getAttribute('action'), { method: 'POST', body: data, headers: { Accept: 'application/json' } })
|
||||
.then(function (res) {
|
||||
return res.text().then(function (txt) {
|
||||
var json = null;
|
||||
try { json = JSON.parse(txt); } catch (err) { json = null; }
|
||||
if (res.status === 422 && json) return done(false, true);
|
||||
done(!!(res.ok && json && json.ok === true));
|
||||
});
|
||||
})
|
||||
.catch(function () { done(false); });
|
||||
});
|
||||
})(window, document);
|
||||
@@ -0,0 +1,232 @@
|
||||
/* ==========================================================================
|
||||
Olzinger – Heizungs-Finder
|
||||
Vier Fragen → erste, unverbindliche Empfehlung (Wärmepumpe / Pellets / Hybrid).
|
||||
Das Ergebnis kann direkt ins Kontaktformular übernommen werden.
|
||||
========================================================================== */
|
||||
(function (w, d) {
|
||||
'use strict';
|
||||
|
||||
var app = d.querySelector('[data-finder]');
|
||||
if (!app) return;
|
||||
|
||||
var form = app.querySelector('[data-finder-form]');
|
||||
var steps = Array.prototype.slice.call(form.querySelectorAll('.finder__step'));
|
||||
var result = form.querySelector('.finder__result');
|
||||
var countEl = app.querySelector('[data-finder-count]');
|
||||
var barEl = app.querySelector('[data-finder-bar]');
|
||||
var backBtn = app.querySelector('[data-finder-back]');
|
||||
var nextBtn = app.querySelector('[data-finder-next]');
|
||||
var reduce = w.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var total = steps.length;
|
||||
var index = 0;
|
||||
var answers = {};
|
||||
var pointerSelect = false;
|
||||
var advanceTimer = 0;
|
||||
|
||||
var LABELS = {
|
||||
gebaeude: { neubau: 'Neubau', gedaemmt: 'Bestand, gut gedämmt', unsaniert: 'Bestand, wenig gedämmt' },
|
||||
verteilung: { fbh: 'Fußbodenheizung', hk: 'Heizkörper', mix: 'Fußbodenheizung & Heizkörper' },
|
||||
lager: { ja: 'vorhanden', nein: 'nicht vorhanden' },
|
||||
dach: { ja: 'ja', nein: 'nein / unbekannt' }
|
||||
};
|
||||
|
||||
var SYSTEMS = {
|
||||
wp: { name: 'Wärmepumpe', title: 'Luft-Wasser-Wärmepumpe', topic: 'Wärmepumpe & Regenerativ' },
|
||||
pellet: { name: 'Pelletheizung', title: 'Pelletheizung', topic: 'Wärmepumpe & Regenerativ' },
|
||||
hybrid: { name: 'Hybridheizung', title: 'Hybridheizung', topic: 'Heizung' }
|
||||
};
|
||||
|
||||
var TEXTS = {
|
||||
wp: {
|
||||
neubau: 'Für Ihren Neubau ist die Wärmepumpe die naheliegende Wahl: Sie heizt mit Umweltwärme, arbeitet mit niedrigen Vorlauftemperaturen besonders effizient und harmoniert perfekt mit einer Fußbodenheizung.',
|
||||
gedaemmt: 'Ihr Haus bringt gute Voraussetzungen mit: Dank Dämmung reichen niedrige Vorlauftemperaturen – genau dort arbeitet eine Wärmepumpe besonders effizient. Ob einzelne Heizkörper getauscht werden sollten, prüfen wir vor Ort.',
|
||||
unsaniert: 'Mit Flächenheizung kann eine Wärmepumpe auch im älteren Haus gut funktionieren. Eine Heizlastberechnung zeigt, ob vorher kleinere Maßnahmen sinnvoll sind.'
|
||||
},
|
||||
pellet: 'Holzpellets heizen nahezu CO<sub>2</sub>-neutral und liefern auch die hohen Vorlauftemperaturen, die klassische Heizkörper in älteren Häusern brauchen. Den nötigen Lagerraum haben Sie bereits.',
|
||||
hybrid: 'Wärmepumpe und Brennwertkessel im Team: Die Wärmepumpe übernimmt den Großteil des Jahres, der Kessel springt nur an sehr kalten Tagen ein. Oft ist auch eine Hochtemperatur-Wärmepumpe möglich – das klären wir mit einer Heizlastberechnung.'
|
||||
};
|
||||
|
||||
/* Bewertung – bewusst einfach und nachvollziehbar gehalten */
|
||||
function evaluate(a) {
|
||||
var s = { wp: 0, pellet: 0, hybrid: 0 };
|
||||
var G = { neubau: [7, 1, 0], gedaemmt: [6, 2, 2], unsaniert: [1, 4, 4] }[a.gebaeude] || [0, 0, 0];
|
||||
s.wp += G[0]; s.pellet += G[1]; s.hybrid += G[2];
|
||||
|
||||
if (a.verteilung === 'fbh') { s.wp += 4 + (a.gebaeude === 'unsaniert' ? 3 : 0); s.pellet += 1; s.hybrid += 1; }
|
||||
if (a.verteilung === 'mix') { s.wp += 2; s.pellet += 2; s.hybrid += 2; }
|
||||
if (a.verteilung === 'hk') { s.wp += (a.gebaeude === 'unsaniert' ? 0 : 3); s.pellet += 3; s.hybrid += 3; }
|
||||
|
||||
if (a.lager === 'ja') s.pellet += 3; else { s.pellet -= 6; s.hybrid += 1; }
|
||||
if (a.dach === 'ja') { s.wp += 1; s.pellet += 1; s.hybrid += 1; }
|
||||
|
||||
var max = { wp: 12, pellet: 11, hybrid: 9 };
|
||||
var pct = {};
|
||||
Object.keys(s).forEach(function (k) { pct[k] = Math.round(Math.max(0, Math.min(1, s[k] / max[k])) * 100); });
|
||||
var priority = ['wp', 'hybrid', 'pellet'];
|
||||
var order = Object.keys(s).sort(function (x, y) {
|
||||
return (pct[y] - pct[x]) || (priority.indexOf(x) - priority.indexOf(y));
|
||||
});
|
||||
return { pct: pct, order: order, top: order[0] };
|
||||
}
|
||||
|
||||
function addons(a, top) {
|
||||
var list = [];
|
||||
if (a.dach === 'ja') list.push(['i-sun', 'Solarthermie für Warmwasser']);
|
||||
if (a.gebaeude === 'neubau') list.push(['i-wind', 'Wohnraumlüftung mit Wärmerückgewinnung']);
|
||||
if (top === 'wp' && a.verteilung !== 'fbh') list.push(['i-gauge', 'Heizkörper-Check & hydraulischer Abgleich']);
|
||||
if (top === 'pellet') list.push(['i-tank', 'Pufferspeicher für mehr Effizienz']);
|
||||
if (top === 'hybrid') list.push(['i-gauge', 'Heizlastberechnung vor Ort']);
|
||||
list.push(['i-check', 'Beratung zu Fördermöglichkeiten']);
|
||||
return list;
|
||||
}
|
||||
|
||||
function icon(id) {
|
||||
return '<svg class="icon" aria-hidden="true"><use href="#' + id + '"/></svg>';
|
||||
}
|
||||
|
||||
function updateChrome() {
|
||||
var isResult = index >= total;
|
||||
countEl.textContent = isResult ? 'Ihr Ergebnis' : 'Frage ' + (index + 1) + ' von ' + total;
|
||||
barEl.style.transform = 'scaleX(' + (isResult ? 1 : (index + 1) / total) + ')';
|
||||
backBtn.disabled = index === 0;
|
||||
if (!isResult) {
|
||||
var name = steps[index].querySelector('input').name;
|
||||
nextBtn.disabled = !answers[name];
|
||||
nextBtn.textContent = '';
|
||||
nextBtn.insertAdjacentHTML('beforeend', (index === total - 1 ? 'Ergebnis zeigen' : 'Weiter') + icon('i-arrow'));
|
||||
}
|
||||
app.classList.toggle('is-done', isResult);
|
||||
}
|
||||
|
||||
function panel(i) { return i >= total ? result : steps[i]; }
|
||||
|
||||
function go(to, focus) {
|
||||
if (to === index) return;
|
||||
var from = panel(index);
|
||||
var target = panel(to);
|
||||
var dir = to > index ? 1 : -1;
|
||||
index = to;
|
||||
if (to >= total) renderResult();
|
||||
updateChrome();
|
||||
|
||||
var reveal = function () {
|
||||
from.hidden = true;
|
||||
from.classList.remove('is-active');
|
||||
target.hidden = false;
|
||||
target.classList.add('is-active');
|
||||
if (to >= total) animateBars();
|
||||
if (focus) {
|
||||
var f = to >= total ? result : target.querySelector('input:checked') || target.querySelector('input');
|
||||
if (f) f.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (w.gsap && !reduce) {
|
||||
w.gsap.to(from, {
|
||||
x: -28 * dir, opacity: 0, duration: 0.28, ease: 'power2.in',
|
||||
onComplete: function () {
|
||||
w.gsap.set(from, { clearProps: 'transform,opacity' });
|
||||
reveal();
|
||||
w.gsap.fromTo(target, { x: 36 * dir, opacity: 0 }, { x: 0, opacity: 1, duration: 0.7, ease: 'expo.out', clearProps: 'transform,opacity' });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reveal();
|
||||
}
|
||||
}
|
||||
|
||||
function renderResult() {
|
||||
var r = evaluate(answers);
|
||||
var sys = SYSTEMS[r.top];
|
||||
result.querySelector('[data-result-title]').textContent = sys.title;
|
||||
var text = r.top === 'wp' ? TEXTS.wp[answers.gebaeude] || TEXTS.wp.gedaemmt : TEXTS[r.top];
|
||||
result.querySelector('[data-result-text]').innerHTML = text;
|
||||
|
||||
var bars = result.querySelector('[data-result-bars]');
|
||||
bars.innerHTML = r.order.map(function (k, i) {
|
||||
return '<li class="' + (i === 0 ? 'is-top' : '') + '"><span>' + SYSTEMS[k].name + '</span>' +
|
||||
'<span class="bar" role="img" aria-label="' + SYSTEMS[k].name + ': ' + r.pct[k] + ' Prozent Übereinstimmung"><i data-v="' + (r.pct[k] / 100) + '"></i></span>' +
|
||||
'<b>' + r.pct[k] + ' %</b></li>';
|
||||
}).join('');
|
||||
|
||||
result.querySelector('[data-result-addons]').innerHTML = addons(answers, r.top).map(function (a) {
|
||||
return '<li>' + icon(a[0]) + a[1] + '</li>';
|
||||
}).join('');
|
||||
|
||||
var summary = 'Heizungs-Finder – meine Angaben: Gebäude: ' + LABELS.gebaeude[answers.gebaeude] +
|
||||
' · Wärmeverteilung: ' + LABELS.verteilung[answers.verteilung] +
|
||||
' · Lagerraum: ' + LABELS.lager[answers.lager] +
|
||||
' · Sonnige Dachfläche: ' + LABELS.dach[answers.dach] +
|
||||
'. Erste Empfehlung: ' + sys.title + '. Ich hätte gern eine persönliche Beratung.';
|
||||
var apply = result.querySelector('[data-finder-apply]');
|
||||
apply.setAttribute('data-topic', sys.topic);
|
||||
apply.setAttribute('data-message', summary);
|
||||
}
|
||||
|
||||
function animateBars() {
|
||||
var bars = result.querySelectorAll('.bar i');
|
||||
// Zwei Frames warten, damit die Transition sicher greift
|
||||
w.requestAnimationFrame(function () {
|
||||
w.requestAnimationFrame(function () {
|
||||
Array.prototype.forEach.call(bars, function (b, i) {
|
||||
b.style.transitionDelay = (0.12 * i) + 's';
|
||||
b.style.transform = 'scaleX(' + b.getAttribute('data-v') + ')';
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restart() {
|
||||
answers = {};
|
||||
Array.prototype.forEach.call(form.querySelectorAll('input[type="radio"]'), function (r) { r.checked = false; });
|
||||
var current = panel(index);
|
||||
current.hidden = true;
|
||||
current.classList.remove('is-active');
|
||||
index = 0;
|
||||
steps[0].hidden = false;
|
||||
steps[0].classList.add('is-active');
|
||||
updateChrome();
|
||||
var first = steps[0].querySelector('input');
|
||||
if (first) first.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
form.addEventListener('pointerdown', function (e) {
|
||||
if (e.target.closest('.opt')) pointerSelect = true;
|
||||
});
|
||||
form.addEventListener('keydown', function () { pointerSelect = false; });
|
||||
|
||||
form.addEventListener('change', function (e) {
|
||||
var input = e.target;
|
||||
if (input.type !== 'radio') return;
|
||||
answers[input.name] = input.value;
|
||||
updateChrome();
|
||||
if (pointerSelect) {
|
||||
w.clearTimeout(advanceTimer);
|
||||
advanceTimer = w.setTimeout(function () { go(index + 1, false); }, 320);
|
||||
}
|
||||
});
|
||||
|
||||
nextBtn.addEventListener('click', function () {
|
||||
if (nextBtn.disabled) return;
|
||||
w.clearTimeout(advanceTimer);
|
||||
go(index + 1, true);
|
||||
});
|
||||
backBtn.addEventListener('click', function () {
|
||||
if (index > 0) { w.clearTimeout(advanceTimer); go(index - 1, true); }
|
||||
});
|
||||
form.addEventListener('submit', function (e) { e.preventDefault(); });
|
||||
|
||||
var restartBtn = result.querySelector('[data-finder-restart]');
|
||||
if (restartBtn) restartBtn.addEventListener('click', restart);
|
||||
|
||||
var applyBtn = result.querySelector('[data-finder-apply]');
|
||||
if (applyBtn) {
|
||||
applyBtn.addEventListener('click', function () {
|
||||
w.dispatchEvent(new CustomEvent('olz:prefill', {
|
||||
detail: { topic: applyBtn.getAttribute('data-topic'), message: applyBtn.getAttribute('data-message') }
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
updateChrome();
|
||||
})(window, document);
|
||||
@@ -0,0 +1,121 @@
|
||||
/* ==========================================================================
|
||||
Olzinger – Energie-Haus
|
||||
Tabs + Hotspots steuern, welches System im Hausschnitt hervorgehoben wird.
|
||||
Solange niemand klickt, wechseln die Komponenten automatisch.
|
||||
========================================================================== */
|
||||
(function (w, d) {
|
||||
'use strict';
|
||||
|
||||
var root = d.querySelector('[data-house]');
|
||||
if (!root) return;
|
||||
|
||||
var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
|
||||
var parts = Array.prototype.slice.call(root.querySelectorAll('.part'));
|
||||
var hotspots = Array.prototype.slice.call(root.querySelectorAll('.hotspot'));
|
||||
var systems = Array.prototype.slice.call(root.querySelectorAll('.sys'));
|
||||
var progress = root.querySelector('[data-house-progress]');
|
||||
var order = tabs.map(function (t) { return t.getAttribute('data-part'); });
|
||||
var reduce = w.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
var DURATION = 6500;
|
||||
var current = null;
|
||||
var autoplay = !reduce;
|
||||
var inView = false;
|
||||
var hovering = false;
|
||||
var elapsed = 0, last = 0, raf = 0;
|
||||
|
||||
hotspots.forEach(function (h) {
|
||||
h.setAttribute('aria-controls', 'part-' + h.getAttribute('data-part'));
|
||||
});
|
||||
|
||||
function activate(id, focusTab) {
|
||||
if (id === current) return;
|
||||
current = id;
|
||||
root.setAttribute('data-active', id);
|
||||
tabs.forEach(function (t) {
|
||||
var on = t.getAttribute('data-part') === id;
|
||||
t.setAttribute('aria-selected', on ? 'true' : 'false');
|
||||
t.tabIndex = on ? 0 : -1;
|
||||
if (on && focusTab) t.focus();
|
||||
});
|
||||
parts.forEach(function (p) {
|
||||
var on = p.getAttribute('data-part') === id;
|
||||
p.hidden = !on;
|
||||
p.classList.toggle('is-active', on);
|
||||
});
|
||||
hotspots.forEach(function (h) {
|
||||
var on = h.getAttribute('data-part') === id;
|
||||
h.classList.toggle('is-active', on);
|
||||
h.setAttribute('aria-pressed', on ? 'true' : 'false');
|
||||
});
|
||||
systems.forEach(function (s) { s.classList.toggle('is-on', s.getAttribute('data-sys') === id); });
|
||||
elapsed = 0;
|
||||
}
|
||||
|
||||
function stopAutoplay() {
|
||||
autoplay = false;
|
||||
if (progress) progress.style.transform = 'scaleX(0)';
|
||||
if (raf) w.cancelAnimationFrame(raf);
|
||||
raf = 0;
|
||||
}
|
||||
|
||||
function tick(now) {
|
||||
raf = 0;
|
||||
if (!autoplay || !inView || hovering) return;
|
||||
var dt = last ? now - last : 16;
|
||||
last = now;
|
||||
elapsed += Math.min(dt, 100);
|
||||
if (progress) progress.style.transform = 'scaleX(' + Math.min(elapsed / DURATION, 1) + ')';
|
||||
if (elapsed >= DURATION) {
|
||||
var next = order[(order.indexOf(current) + 1) % order.length];
|
||||
activate(next, false);
|
||||
}
|
||||
raf = w.requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (!autoplay || !inView || hovering || raf) return;
|
||||
last = 0;
|
||||
raf = w.requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
tabs.forEach(function (t) {
|
||||
t.addEventListener('click', function () { stopAutoplay(); activate(t.getAttribute('data-part'), false); });
|
||||
});
|
||||
hotspots.forEach(function (h) {
|
||||
h.addEventListener('click', function () { stopAutoplay(); activate(h.getAttribute('data-part'), false); });
|
||||
});
|
||||
|
||||
// Tastatursteuerung nach WAI-ARIA Tabs-Pattern
|
||||
var tablist = root.querySelector('[role="tablist"]');
|
||||
if (tablist) {
|
||||
tablist.addEventListener('keydown', function (e) {
|
||||
var i = order.indexOf(current), n = order.length, to = -1;
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') to = (i + 1) % n;
|
||||
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') to = (i - 1 + n) % n;
|
||||
else if (e.key === 'Home') to = 0;
|
||||
else if (e.key === 'End') to = n - 1;
|
||||
if (to < 0) return;
|
||||
e.preventDefault();
|
||||
stopAutoplay();
|
||||
activate(order[to], true);
|
||||
});
|
||||
}
|
||||
|
||||
// Pausieren, solange der Nutzer mit der Maus im Bereich ist
|
||||
root.addEventListener('pointerenter', function (e) {
|
||||
if (e.pointerType === 'mouse') { hovering = true; if (raf) { w.cancelAnimationFrame(raf); raf = 0; } }
|
||||
});
|
||||
root.addEventListener('pointerleave', function (e) {
|
||||
if (e.pointerType === 'mouse') { hovering = false; run(); }
|
||||
});
|
||||
|
||||
if ('IntersectionObserver' in w) {
|
||||
new IntersectionObserver(function (entries) {
|
||||
inView = entries[0].isIntersecting;
|
||||
if (inView) run();
|
||||
}, { threshold: 0.35 }).observe(root);
|
||||
}
|
||||
|
||||
activate(order[0], false);
|
||||
})(window, document);
|
||||
@@ -0,0 +1,504 @@
|
||||
/* ==========================================================================
|
||||
Olzinger – Hauptskript
|
||||
Preloader, Hero-Intro, Smooth-Scroll, Navigation und Scroll-Animationen.
|
||||
========================================================================== */
|
||||
(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 finePointer = w.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
var fallback = !!w.__olzFallback;
|
||||
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;
|
||||
el.setAttribute('data-count', nowYear - 1985);
|
||||
});
|
||||
|
||||
/* ---------- Wärmebild ---------- */
|
||||
var thermal = null;
|
||||
var canvas = $('[data-thermal]');
|
||||
if (canvas && w.OlzThermal) {
|
||||
thermal = w.OlzThermal.init(canvas, {
|
||||
hero: $('[data-hero]'),
|
||||
probe: $('[data-probe]'),
|
||||
probeVal: $('[data-probe-val]'),
|
||||
legendMarker: $('[data-legend-marker]'),
|
||||
calm: $('.hero__aside'),
|
||||
reduceMotion: reduce
|
||||
});
|
||||
} else {
|
||||
html.classList.add('no-webgl');
|
||||
}
|
||||
|
||||
/* ---------- Mobiles Menü (auch ohne GSAP funktionsfähig) ---------- */
|
||||
var menu = $('[data-menu]');
|
||||
var toggle = $('[data-menu-toggle]');
|
||||
var menuOpen = false;
|
||||
var lenis = null;
|
||||
|
||||
if (menu) { menu.hidden = false; menu.setAttribute('inert', ''); menu.setAttribute('aria-hidden', 'true'); }
|
||||
|
||||
function openMenu() {
|
||||
if (!menu || menuOpen) return;
|
||||
menuOpen = true;
|
||||
menu.classList.add('is-open');
|
||||
menu.removeAttribute('inert');
|
||||
menu.setAttribute('aria-hidden', 'false');
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
toggle.querySelector('.sr-only').textContent = 'Menü schließen';
|
||||
html.classList.add('menu-open');
|
||||
if (lenis) lenis.stop();
|
||||
if (w.gsap && !reduce) {
|
||||
w.gsap.fromTo($$('.menu__links a, .menu__foot > *'), { y: 40, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.9, ease: 'expo.out', stagger: 0.045, delay: 0.25 });
|
||||
}
|
||||
}
|
||||
function closeMenu() {
|
||||
if (!menu || !menuOpen) return;
|
||||
menuOpen = false;
|
||||
menu.classList.remove('is-open');
|
||||
menu.setAttribute('inert', '');
|
||||
menu.setAttribute('aria-hidden', 'true');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.querySelector('.sr-only').textContent = 'Menü öffnen';
|
||||
html.classList.remove('menu-open');
|
||||
if (lenis) lenis.start();
|
||||
}
|
||||
if (toggle) toggle.addEventListener('click', function () { if (menuOpen) closeMenu(); else openMenu(); });
|
||||
d.addEventListener('keydown', function (e) { if (e.key === 'Escape' && menuOpen) { closeMenu(); toggle.focus(); } });
|
||||
|
||||
/* ---------- Ohne GSAP: alles statisch sichtbar lassen ---------- */
|
||||
if (!w.gsap || !w.ScrollTrigger) {
|
||||
html.classList.remove('has-loader', 'js');
|
||||
html.classList.add('no-js');
|
||||
if (thermal) thermal.heatUp();
|
||||
d.addEventListener('click', function (e) {
|
||||
var a = e.target.closest('a[href^="#"]');
|
||||
if (a && a.dataset.topic) w.dispatchEvent(new CustomEvent('olz:topic', { detail: a.dataset.topic }));
|
||||
if (a) closeMenu();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var gsap = w.gsap, ScrollTrigger = w.ScrollTrigger;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
if (w.SplitText) gsap.registerPlugin(w.SplitText);
|
||||
gsap.config({ nullTargetWarn: false });
|
||||
|
||||
/* ---------- Smooth Scroll ---------- */
|
||||
if (!reduce && typeof w.Lenis === 'function') {
|
||||
lenis = new w.Lenis({ lerp: 0.105, smoothWheel: true, wheelMultiplier: 1, touchMultiplier: 1.4 });
|
||||
lenis.on('scroll', ScrollTrigger.update);
|
||||
gsap.ticker.add(function (t) { lenis.raf(t * 1000); });
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
if (html.classList.contains('has-loader')) lenis.stop();
|
||||
}
|
||||
w.olzLenis = lenis;
|
||||
|
||||
function navHeight() {
|
||||
var bar = $('.nav__bar');
|
||||
return bar ? bar.getBoundingClientRect().height + 12 : 76;
|
||||
}
|
||||
|
||||
// Natürliche Position berechnen (auch für „sticky“-Elemente)
|
||||
function targetY(el) {
|
||||
if (el.id === 'top') return 0;
|
||||
var stack = el.closest('[data-stack]');
|
||||
if (stack && el.classList.contains('svc')) {
|
||||
var cards = $$('.svc', stack);
|
||||
var gap = parseFloat(getComputedStyle(stack).rowGap) || 0;
|
||||
var y = stack.getBoundingClientRect().top + w.scrollY;
|
||||
for (var i = 0; i < cards.length && cards[i] !== el; i++) y += cards[i].offsetHeight + gap;
|
||||
var stickyTop = parseFloat(getComputedStyle(el).top);
|
||||
return y - (isNaN(stickyTop) ? navHeight() : stickyTop);
|
||||
}
|
||||
return el.getBoundingClientRect().top + w.scrollY;
|
||||
}
|
||||
|
||||
function scrollToEl(el) {
|
||||
var y = targetY(el);
|
||||
if (lenis) lenis.scrollTo(y, { duration: 1.4, easing: function (t) { return t === 1 ? 1 : 1 - Math.pow(2, -10 * t); } });
|
||||
else w.scrollTo({ top: y, behavior: reduce ? 'auto' : 'smooth' });
|
||||
}
|
||||
|
||||
d.addEventListener('click', function (e) {
|
||||
var a = e.target.closest('a[href^="#"]');
|
||||
if (!a) return;
|
||||
var id = a.getAttribute('href');
|
||||
if (id.length < 2) return;
|
||||
var target = d.getElementById(id.slice(1));
|
||||
if (!target) return;
|
||||
e.preventDefault();
|
||||
if (a.dataset.topic) w.dispatchEvent(new CustomEvent('olz:topic', { detail: a.dataset.topic }));
|
||||
var wasOpen = menuOpen;
|
||||
closeMenu();
|
||||
setTimeout(function () { scrollToEl(target); }, wasOpen ? 380 : 0);
|
||||
if (history.replaceState) history.replaceState(null, '', id === '#top' ? location.pathname : id);
|
||||
});
|
||||
|
||||
/* ---------- Seitenwechsel mit Vorhang ---------- */
|
||||
var curtain = d.createElement('div');
|
||||
curtain.className = 'curtain';
|
||||
curtain.setAttribute('aria-hidden', 'true');
|
||||
d.body.appendChild(curtain);
|
||||
d.addEventListener('click', function (e) {
|
||||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
var a = e.target.closest('a[href]');
|
||||
if (!a || a.target === '_blank' || a.hasAttribute('download')) return;
|
||||
var url;
|
||||
try { url = new URL(a.getAttribute('href'), location.href); } catch (err) { return; }
|
||||
if (!/^https?:$/.test(url.protocol) || url.origin !== location.origin) return;
|
||||
if (url.pathname === location.pathname) return;
|
||||
e.preventDefault();
|
||||
closeMenu();
|
||||
html.classList.add('is-leaving');
|
||||
w.setTimeout(function () { location.href = url.href; }, reduce ? 0 : 560);
|
||||
});
|
||||
w.addEventListener('pageshow', function () { html.classList.remove('is-leaving'); });
|
||||
|
||||
/* ---------- Preloader & Hero-Intro ---------- */
|
||||
function heroIn() {
|
||||
var tl = gsap.timeline({ defaults: { ease: 'expo.out' } });
|
||||
tl.fromTo('.hero__word', { y: 0, yPercent: 112 }, { yPercent: 0, duration: 1.5, stagger: 0.1 }, 0)
|
||||
.fromTo('.hero__dot', { scale: 0 }, { scale: 1, duration: 1.1, ease: 'back.out(3)', stagger: 0.1 }, 0.55)
|
||||
.fromTo('.hero__tag', { opacity: 0, x: -12 }, { opacity: 1, x: 0, duration: 1.2, stagger: 0.1 }, 0.7)
|
||||
.fromTo('[data-hero-fade]', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 1.3, stagger: 0.12 }, 0.35)
|
||||
.fromTo('.nav__bar', { opacity: 0, y: -16 }, { opacity: 1, y: 0, duration: 1.2 }, 0.2)
|
||||
.add(function () { if (thermal) thermal.measure(); });
|
||||
return tl;
|
||||
}
|
||||
|
||||
function intro() {
|
||||
if (fallback) { gsap.set('.nav__bar', { opacity: 1 }); if (thermal) thermal.heatUp(); return; }
|
||||
var loader = $('.loader');
|
||||
var useLoader = html.classList.contains('has-loader') && loader && !reduce;
|
||||
|
||||
if (reduce) {
|
||||
html.classList.remove('has-loader');
|
||||
gsap.set('.hero__word', { yPercent: 0, y: 0 });
|
||||
gsap.set('.hero__dot', { scale: 1 });
|
||||
gsap.set('.hero__tag, [data-hero-fade], .nav__bar', { opacity: 1 });
|
||||
if (thermal) thermal.heatUp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!useLoader) {
|
||||
html.classList.remove('has-loader');
|
||||
if (loader) loader.remove();
|
||||
if (thermal) thermal.heatUp();
|
||||
heroIn();
|
||||
return;
|
||||
}
|
||||
|
||||
var arc = $('.loader__arc', loader);
|
||||
var yearEl = $('[data-loader-year]', loader);
|
||||
var len = arc.getTotalLength();
|
||||
var counter = { v: 1985 };
|
||||
gsap.set(arc, { strokeDasharray: len, strokeDashoffset: len });
|
||||
|
||||
var tl = gsap.timeline();
|
||||
tl.from('.loader__center', { opacity: 0, scale: 0.92, duration: 0.6, ease: 'power2.out' }, 0)
|
||||
.from('.loader__caption', { opacity: 0, y: 10, duration: 0.6, stagger: 0.08 }, 0.1)
|
||||
.to(arc, { strokeDashoffset: 0, duration: 1.35, ease: 'power2.inOut' }, 0.15)
|
||||
.to(counter, {
|
||||
v: nowYear, duration: 1.35, ease: 'power2.inOut',
|
||||
onUpdate: function () { yearEl.textContent = Math.round(counter.v); }
|
||||
}, 0.15)
|
||||
.to('.loader__center, .loader__caption', { opacity: 0, y: -20, duration: 0.5, ease: 'power2.in', stagger: 0.03 }, 1.65)
|
||||
.add(function () { if (thermal) thermal.heatUp(); }, 1.7)
|
||||
.to(loader, { clipPath: 'inset(0% 0% 100% 0%)', duration: 1.05, ease: 'expo.inOut' }, 1.85)
|
||||
.add(heroIn(), 2.25)
|
||||
.add(function () {
|
||||
html.classList.remove('has-loader');
|
||||
loader.remove();
|
||||
if (lenis) lenis.start();
|
||||
ScrollTrigger.refresh();
|
||||
}, 2.95);
|
||||
}
|
||||
|
||||
/* ---------- Navigation ---------- */
|
||||
var nav = $('[data-nav]');
|
||||
ScrollTrigger.create({
|
||||
start: 0, end: 'max',
|
||||
onUpdate: function (self) {
|
||||
var y = self.scroll();
|
||||
nav.classList.toggle('is-scrolled', y > 40);
|
||||
if (menuOpen) return;
|
||||
if (y > 420 && self.direction === 1) nav.classList.add('is-hidden');
|
||||
else if (self.direction === -1 || y <= 420) nav.classList.remove('is-hidden');
|
||||
}
|
||||
});
|
||||
nav.addEventListener('focusin', function () { nav.classList.remove('is-hidden'); });
|
||||
|
||||
var navLinks = $$('[data-nav-link]');
|
||||
navLinks.forEach(function (link) {
|
||||
var sec = d.getElementById(link.getAttribute('href').slice(1));
|
||||
if (!sec) return;
|
||||
ScrollTrigger.create({
|
||||
trigger: sec, start: 'top 45%', end: 'bottom 45%',
|
||||
onToggle: function (self) {
|
||||
if (self.isActive) navLinks.forEach(function (l) { l.classList.toggle('is-active', l === link); });
|
||||
else link.classList.remove('is-active');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ---------- Animationen, die Bewegung voraussetzen ---------- */
|
||||
function motion() {
|
||||
// Hero: Inhalte beim Scrollen leicht anheben und ausblenden
|
||||
gsap.to('.hero__inner', {
|
||||
yPercent: -10, opacity: 0.15, ease: 'none',
|
||||
scrollTrigger: { trigger: '.hero', start: 'top top', end: 'bottom top', scrub: true }
|
||||
});
|
||||
|
||||
// Überschriften: Zeilen-Reveal
|
||||
if (w.SplitText) {
|
||||
$$('[data-split]').forEach(function (el) {
|
||||
w.SplitText.create(el, {
|
||||
type: 'lines', mask: 'lines', linesClass: 'split-line', autoSplit: true,
|
||||
onSplit: function (self) {
|
||||
return gsap.from(self.lines, {
|
||||
yPercent: 110, duration: 1.25, ease: 'expo.out', stagger: 0.09,
|
||||
scrollTrigger: { trigger: el, start: 'top 86%', once: true }
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Intro-Text: Wort für Wort „aufheizen“
|
||||
var introText = $('[data-scrub-words]');
|
||||
if (introText) {
|
||||
var split = w.SplitText.create(introText, { type: 'words', wordsClass: 'scrub-word' });
|
||||
gsap.fromTo(split.words, { opacity: 0.13 }, {
|
||||
opacity: 1, ease: 'none', stagger: 0.1,
|
||||
scrollTrigger: { trigger: introText, start: 'top 82%', end: 'bottom 50%', scrub: true }
|
||||
});
|
||||
gsap.from('.ichip', {
|
||||
scale: 0, rotate: -25, duration: 0.9, ease: 'back.out(2.4)', stagger: 0.18,
|
||||
scrollTrigger: { trigger: introText, start: 'top 70%', once: true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Allgemeine Reveals
|
||||
$$('[data-reveal]').forEach(function (el) {
|
||||
gsap.from(el, {
|
||||
y: 44, opacity: 0, duration: 1.25, ease: 'expo.out',
|
||||
scrollTrigger: { trigger: el, start: 'top 90%', once: true }
|
||||
});
|
||||
});
|
||||
$$('[data-reveal-stagger]').forEach(function (el) {
|
||||
gsap.from(el.children, {
|
||||
y: 40, opacity: 0, duration: 1.15, ease: 'expo.out', stagger: 0.09,
|
||||
scrollTrigger: { trigger: el, start: 'top 90%', once: true }
|
||||
});
|
||||
});
|
||||
|
||||
// Vorlauf/Rücklauf-Bänder
|
||||
$$('.tape').forEach(function (tape) {
|
||||
var track = $('.tape__track', tape);
|
||||
var unit = track.innerHTML;
|
||||
var unitWidth = track.scrollWidth;
|
||||
if (!unitWidth) return;
|
||||
var copies = Math.max(2, Math.ceil((tape.offsetWidth * 1.2) / unitWidth) + 1);
|
||||
track.innerHTML = new Array(copies + 1).join(unit);
|
||||
var dir = parseFloat(tape.getAttribute('data-tape')) || 1;
|
||||
var tween = gsap.fromTo(track,
|
||||
{ x: dir > 0 ? 0 : -unitWidth },
|
||||
{ x: dir > 0 ? -unitWidth : 0, duration: unitWidth / 70, ease: 'none', repeat: -1 });
|
||||
tween.totalTime(tween.duration() * 500);
|
||||
var boost = { v: 1 };
|
||||
ScrollTrigger.create({
|
||||
trigger: tape, start: 'top bottom', end: 'bottom top',
|
||||
onUpdate: function (self) {
|
||||
var v = Math.min(Math.abs(self.getVelocity()) / 250, 7);
|
||||
var sign = self.direction === 1 ? 1 : -1;
|
||||
gsap.to(boost, {
|
||||
v: (1 + v) * sign, duration: 0.25, overwrite: true,
|
||||
onUpdate: function () { tween.timeScale(boost.v); }
|
||||
});
|
||||
gsap.to(boost, {
|
||||
v: sign, duration: 1.2, delay: 0.25, ease: 'power2.out',
|
||||
onUpdate: function () { tween.timeScale(boost.v); }
|
||||
});
|
||||
}
|
||||
});
|
||||
gsap.fromTo(tape, { xPercent: dir * -4 }, {
|
||||
xPercent: dir * 4, ease: 'none',
|
||||
scrollTrigger: { trigger: '.tapes', start: 'top bottom', end: 'bottom top', scrub: true }
|
||||
});
|
||||
});
|
||||
|
||||
// Leistungskarten: gestapelt, die hinteren treten zurück
|
||||
var cards = $$('.svc');
|
||||
var mm = gsap.matchMedia();
|
||||
mm.add('(min-width: 900px) and (min-height: 660px)', function () {
|
||||
cards.forEach(function (card, i) {
|
||||
var next = cards[i + 1];
|
||||
if (!next) return;
|
||||
var inner = $('.svc__inner', card);
|
||||
gsap.to(inner, {
|
||||
scale: 0.9 + i * 0.012, '--shade': 0.55, ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: next, start: 'top 82%',
|
||||
end: function () { return 'top ' + (parseFloat(getComputedStyle(next).top) || 100) + 'px'; },
|
||||
scrub: true, invalidateOnRefresh: true
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Illustrationen: leichter Parallax
|
||||
$$('.svc__art .art').forEach(function (art) {
|
||||
gsap.fromTo(art, { yPercent: 4 }, {
|
||||
yPercent: -4, ease: 'none',
|
||||
scrollTrigger: { trigger: art.closest('.svc'), start: 'top bottom', end: 'bottom top', scrub: true }
|
||||
});
|
||||
});
|
||||
|
||||
// Ablauf: Rohr füllt sich
|
||||
var steps = $('[data-steps]');
|
||||
if (steps) {
|
||||
var fill = $('[data-steps-fill]', steps);
|
||||
var pipe = $('.steps__pipe', steps);
|
||||
var items = $$('[data-step-item]', steps);
|
||||
var mmSteps = gsap.matchMedia();
|
||||
mmSteps.add({ desktop: '(min-width: 901px)', mobile: '(max-width: 900px)' }, function (ctx) {
|
||||
var desktop = ctx.conditions.desktop;
|
||||
if (!desktop) {
|
||||
var nodes = items.map(function (it) { return $('.step__node', it); });
|
||||
var placePipe = function () {
|
||||
var top0 = steps.getBoundingClientRect().top;
|
||||
var a = nodes[0].getBoundingClientRect(), b = nodes[nodes.length - 1].getBoundingClientRect();
|
||||
pipe.style.top = (a.top - top0 + a.height / 2) + 'px';
|
||||
pipe.style.height = (b.top - a.top) + 'px';
|
||||
pipe.style.bottom = 'auto';
|
||||
};
|
||||
placePipe();
|
||||
ScrollTrigger.addEventListener('refreshInit', placePipe);
|
||||
}
|
||||
gsap.fromTo(fill, desktop ? { scaleX: 0, scaleY: 1 } : { scaleY: 0, scaleX: 1 }, {
|
||||
scaleX: 1, scaleY: 1, ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: steps, start: 'top 72%', end: desktop ? 'bottom 62%' : 'bottom 68%', scrub: 0.6,
|
||||
onUpdate: function (self) {
|
||||
var p = self.progress, n = items.length;
|
||||
items.forEach(function (it, i) { it.classList.toggle('is-on', p > (i / (n - 1)) * 0.97 + 0.01); });
|
||||
}
|
||||
}
|
||||
});
|
||||
return function () {
|
||||
pipe.style.top = pipe.style.height = pipe.style.bottom = '';
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Footer-Schriftzug „heizt auf“
|
||||
var word = $('[data-footer-word] span');
|
||||
if (word) {
|
||||
gsap.fromTo(word, { '--heat': '0%' }, {
|
||||
'--heat': '100%', ease: 'none',
|
||||
scrollTrigger: { trigger: '.footer', start: 'top 80%', end: 'bottom bottom', scrub: true }
|
||||
});
|
||||
gsap.from(word, {
|
||||
yPercent: 60, opacity: 0, duration: 1.4, ease: 'expo.out',
|
||||
scrollTrigger: { trigger: '[data-footer-word]', start: 'top 95%', once: true }
|
||||
});
|
||||
}
|
||||
|
||||
// Magnetische Buttons
|
||||
if (finePointer) {
|
||||
$$('[data-magnetic]').forEach(function (btn) {
|
||||
var xTo = gsap.quickTo(btn, 'x', { duration: 0.6, ease: 'power3' });
|
||||
var yTo = gsap.quickTo(btn, 'y', { duration: 0.6, ease: 'power3' });
|
||||
btn.addEventListener('pointermove', function (e) {
|
||||
var r = btn.getBoundingClientRect();
|
||||
xTo((e.clientX - r.left - r.width / 2) * 0.22);
|
||||
yTo((e.clientY - r.top - r.height / 2) * 0.32);
|
||||
});
|
||||
btn.addEventListener('pointerleave', function () { xTo(0); yTo(0); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Funktionen, die immer laufen (auch bei reduzierter Bewegung) ---------- */
|
||||
function essentials() {
|
||||
// Illustrationen nur animieren, wenn sichtbar
|
||||
if ('IntersectionObserver' in w) {
|
||||
var io = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (en) { en.target.classList.toggle('is-inview', en.isIntersecting); });
|
||||
}, { threshold: 0.15 });
|
||||
$$('.svc').forEach(function (c) { io.observe(c); });
|
||||
} else {
|
||||
$$('.svc').forEach(function (c) { c.classList.add('is-inview'); });
|
||||
}
|
||||
|
||||
// Zähler
|
||||
$$('[data-count]').forEach(function (el) {
|
||||
var target = parseInt(el.getAttribute('data-count') || el.textContent, 10);
|
||||
if (isNaN(target)) return;
|
||||
if (reduce) { el.textContent = target; return; }
|
||||
var from = parseInt(el.getAttribute('data-count-from') || '0', 10);
|
||||
var obj = { v: from };
|
||||
el.textContent = from;
|
||||
gsap.to(obj, {
|
||||
v: target, duration: 2.2, ease: 'power3.out',
|
||||
scrollTrigger: { trigger: el, start: 'top 92%', once: true },
|
||||
onUpdate: function () { el.textContent = Math.round(obj.v); }
|
||||
});
|
||||
});
|
||||
|
||||
// Historie: Zählwerk mit Jahreszahl
|
||||
var hist = $('[data-history]');
|
||||
if (hist) {
|
||||
var strips = $$('[data-odo-digit] .odo__strip', hist);
|
||||
var caption = $('[data-odo-caption]', hist);
|
||||
var entries = $$('.tl', hist);
|
||||
var setYear = function (year, title) {
|
||||
String(year).split('').forEach(function (ch, i) {
|
||||
if (!strips[i]) return;
|
||||
gsap.to(strips[i], { yPercent: -10 * parseInt(ch, 10), duration: reduce ? 0 : 1.1, ease: 'expo.inOut', delay: reduce ? 0 : i * 0.06, overwrite: true });
|
||||
});
|
||||
if (caption) caption.textContent = title;
|
||||
};
|
||||
entries.forEach(function (entry) {
|
||||
var y = entry.getAttribute('data-year');
|
||||
var year = y === 'now' ? nowYear : parseInt(y, 10);
|
||||
var title = ($('.tl__title', entry) || {}).textContent || '';
|
||||
ScrollTrigger.create({
|
||||
trigger: entry, start: 'top 62%', end: 'bottom 62%',
|
||||
onToggle: function (self) {
|
||||
if (!self.isActive) return;
|
||||
entries.forEach(function (e2) { e2.classList.toggle('is-active', e2 === entry); });
|
||||
setYear(year, title);
|
||||
}
|
||||
});
|
||||
});
|
||||
entries[0].classList.add('is-active');
|
||||
setYear(1985, ($('.tl__title', entries[0]) || {}).textContent || '');
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Start ---------- */
|
||||
essentials();
|
||||
if (!reduce) motion();
|
||||
intro();
|
||||
|
||||
// Nach dem Laden der Schriften Positionen neu berechnen
|
||||
if (d.fonts && d.fonts.ready) d.fonts.ready.then(function () { ScrollTrigger.refresh(); });
|
||||
w.addEventListener('load', function () { ScrollTrigger.refresh(); });
|
||||
|
||||
// Deep-Link (#kontakt etc.) nach dem Aufbau korrekt anspringen
|
||||
if (location.hash && location.hash.length > 1) {
|
||||
var deep = d.getElementById(location.hash.slice(1));
|
||||
if (deep) w.setTimeout(function () { w.scrollTo(0, targetY(deep)); }, 60);
|
||||
}
|
||||
})(window, document);
|
||||
@@ -0,0 +1,402 @@
|
||||
/* ==========================================================================
|
||||
Olzinger – „Wärmebild“ (WebGL)
|
||||
Ein thermografisches Feld mit Isothermen. Die Maus wirkt als Wärmequelle
|
||||
und hinterlässt eine Wärmespur, die langsam abkühlt und sich ausbreitet.
|
||||
Hinter Fließtext wird die Wärme gedämpft, damit alles lesbar bleibt.
|
||||
========================================================================== */
|
||||
(function (w) {
|
||||
'use strict';
|
||||
|
||||
var MAX_HEAT = 14;
|
||||
var T_MIN = 14; // °C bei Feldwert 0
|
||||
var T_MAX = 28; // °C bei Feldwert 1
|
||||
|
||||
var VERT = 'attribute vec2 aPos; void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }';
|
||||
|
||||
var FRAG = [
|
||||
'#ifdef GL_OES_standard_derivatives',
|
||||
'#extension GL_OES_standard_derivatives : enable',
|
||||
'#endif',
|
||||
'precision highp float;',
|
||||
'#define MAXH ' + MAX_HEAT,
|
||||
'uniform vec2 uRes;',
|
||||
'uniform float uTime;',
|
||||
'uniform float uIntro;',
|
||||
'uniform vec4 uCalm;',
|
||||
'uniform vec4 uHeat[MAXH];',
|
||||
'float hash(vec2 p){ p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }',
|
||||
'float noise(vec2 p){ vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f);',
|
||||
' return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x), mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y); }',
|
||||
'float fbm(vec2 p){ float v = 0.0; float a = 0.5; mat2 m = mat2(1.6, 1.2, -1.2, 1.6);',
|
||||
' for (int i = 0; i < 5; i++){ v += a * noise(p); p = m * p; a *= 0.5; } return v; }',
|
||||
'vec3 pal(float t){',
|
||||
' t = clamp(t, 0.0, 1.0) * 8.0;',
|
||||
' vec3 c0 = vec3(0.020, 0.043, 0.071); vec3 c1 = vec3(0.043, 0.106, 0.180); vec3 c2 = vec3(0.078, 0.200, 0.353);',
|
||||
' vec3 c3 = vec3(0.231, 0.141, 0.400); vec3 c4 = vec3(0.541, 0.137, 0.314); vec3 c5 = vec3(0.878, 0.282, 0.165);',
|
||||
' vec3 c6 = vec3(1.000, 0.541, 0.165); vec3 c7 = vec3(1.000, 0.780, 0.420); vec3 c8 = vec3(1.000, 0.910, 0.700);',
|
||||
' if (t < 1.0) return mix(c0, c1, t);',
|
||||
' if (t < 2.0) return mix(c1, c2, t - 1.0);',
|
||||
' if (t < 3.0) return mix(c2, c3, t - 2.0);',
|
||||
' if (t < 4.0) return mix(c3, c4, t - 3.0);',
|
||||
' if (t < 5.0) return mix(c4, c5, t - 4.0);',
|
||||
' if (t < 6.0) return mix(c5, c6, t - 5.0);',
|
||||
' if (t < 7.0) return mix(c6, c7, t - 6.0);',
|
||||
' return mix(c7, c8, t - 7.0);',
|
||||
'}',
|
||||
'float calm(vec2 p){',
|
||||
' vec2 a = smoothstep(uCalm.xy - 0.08, uCalm.xy + 0.02, p);',
|
||||
' vec2 b = 1.0 - smoothstep(uCalm.zw - 0.02, uCalm.zw + 0.08, p);',
|
||||
' return a.x * a.y * b.x * b.y;',
|
||||
'}',
|
||||
'void main(){',
|
||||
' vec2 uv = gl_FragCoord.xy / uRes;',
|
||||
' float asp = uRes.x / uRes.y;',
|
||||
' vec2 p = vec2(uv.x * asp, uv.y);',
|
||||
' float t = uTime;',
|
||||
' vec2 s = p * 1.35;',
|
||||
' vec2 q = vec2(fbm(s + vec2(0.0, t * 0.05)), fbm(s + vec2(5.2, -t * 0.04)));',
|
||||
' vec2 r = vec2(fbm(s + 2.4 * q + vec2(1.7, 9.2) + t * 0.06), fbm(s + 2.4 * q + vec2(8.3, 2.8) - t * 0.05));',
|
||||
' float f = fbm(s + 2.2 * r);',
|
||||
' float base = smoothstep(0.18, 0.86, f) * 0.46;',
|
||||
' vec2 dz = p - vec2(asp * 0.8, 0.76);',
|
||||
' base += 0.36 * exp(-dot(dz, dz) / 0.11) * (0.8 + 0.2 * sin(t * 0.45));',
|
||||
' vec2 dc = p - vec2(asp * 0.16, 0.12);',
|
||||
' base -= 0.12 * exp(-dot(dc, dc) / 0.2);',
|
||||
' base *= mix(0.5, 1.0, smoothstep(0.0, 0.95, uv.x * 0.85 + uv.y * 0.55));',
|
||||
' float heat = 0.0;',
|
||||
' for (int i = 0; i < MAXH; i++){ vec4 h = uHeat[i]; vec2 d = p - h.xy; heat += h.z * exp(-dot(d, d) / max(h.w * h.w, 0.0001)); }',
|
||||
' heat *= 1.0 - 0.62 * calm(p);',
|
||||
' float v = max(base, 0.0) + heat * 0.66;',
|
||||
' v = 1.0 - exp(-v * 1.35);',
|
||||
' v *= uIntro;',
|
||||
' vec3 col = pal(v);',
|
||||
' float k = v * 15.0;',
|
||||
'#ifdef GL_OES_standard_derivatives',
|
||||
' float fw = fwidth(k) * 1.15;',
|
||||
'#else',
|
||||
' float fw = 0.05;',
|
||||
'#endif',
|
||||
' float dist = abs(fract(k + 0.5) - 0.5);',
|
||||
' float line = 1.0 - smoothstep(0.0, fw, dist);',
|
||||
' col += line * (0.03 + 0.2 * v) * vec3(1.0, 0.93, 0.86);',
|
||||
' float g = hash(gl_FragCoord.xy + fract(t * 7.0) * 91.7) - 0.5;',
|
||||
' col += g * 0.032;',
|
||||
' vec2 vc = uv - 0.5;',
|
||||
' col *= 1.0 - dot(vc, vc) * 0.5;',
|
||||
' gl_FragColor = vec4(col, 1.0);',
|
||||
'}'
|
||||
].join('\n');
|
||||
|
||||
/* ---------- CPU-Gegenstück des Feldes (für die Temperaturanzeige) ---------- */
|
||||
function fract(x) { return x - Math.floor(x); }
|
||||
function smoothstep(a, b, x) { var t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); }
|
||||
function hash(x, y) {
|
||||
x = fract(x * 123.34); y = fract(y * 456.21);
|
||||
var dd = x * (x + 45.32) + y * (y + 45.32);
|
||||
x += dd; y += dd;
|
||||
return fract(x * y);
|
||||
}
|
||||
function noise(x, y) {
|
||||
var ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
|
||||
var ux = fx * fx * (3 - 2 * fx), uy = fy * fy * (3 - 2 * fy);
|
||||
var a = hash(ix, iy), b = hash(ix + 1, iy), c = hash(ix, iy + 1), d = hash(ix + 1, iy + 1);
|
||||
var ab = a + (b - a) * ux, cd = c + (d - c) * ux;
|
||||
return ab + (cd - ab) * uy;
|
||||
}
|
||||
function fbm(x, y) {
|
||||
var v = 0, a = 0.5;
|
||||
for (var i = 0; i < 5; i++) {
|
||||
v += a * noise(x, y);
|
||||
var nx = 1.6 * x - 1.2 * y, ny = 1.2 * x + 1.6 * y;
|
||||
x = nx; y = ny; a *= 0.5;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function compile(gl, type, src) {
|
||||
var sh = gl.createShader(type);
|
||||
gl.shaderSource(sh, src);
|
||||
gl.compileShader(sh);
|
||||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
|
||||
if (w.console) console.warn('[Wärmebild] Shader-Fehler:', gl.getShaderInfoLog(sh));
|
||||
gl.deleteShader(sh);
|
||||
return null;
|
||||
}
|
||||
return sh;
|
||||
}
|
||||
|
||||
function init(canvas, opts) {
|
||||
opts = opts || {};
|
||||
var root = document.documentElement;
|
||||
var hero = opts.hero || canvas.parentElement;
|
||||
var gl = null;
|
||||
try {
|
||||
gl = canvas.getContext('webgl', { antialias: false, alpha: false, depth: false, stencil: false, premultipliedAlpha: false, powerPreference: 'low-power' }) ||
|
||||
canvas.getContext('experimental-webgl');
|
||||
} catch (e) { gl = null; }
|
||||
if (!gl) { root.classList.add('no-webgl'); return null; }
|
||||
|
||||
gl.getExtension('OES_standard_derivatives');
|
||||
var vs = compile(gl, gl.VERTEX_SHADER, VERT);
|
||||
var fs = compile(gl, gl.FRAGMENT_SHADER, FRAG);
|
||||
if (!vs || !fs) { root.classList.add('no-webgl'); return null; }
|
||||
var prog = gl.createProgram();
|
||||
gl.attachShader(prog, vs);
|
||||
gl.attachShader(prog, fs);
|
||||
gl.linkProgram(prog);
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { root.classList.add('no-webgl'); return null; }
|
||||
gl.useProgram(prog);
|
||||
|
||||
var buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
||||
var aPos = gl.getAttribLocation(prog, 'aPos');
|
||||
gl.enableVertexAttribArray(aPos);
|
||||
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
var uRes = gl.getUniformLocation(prog, 'uRes');
|
||||
var uTime = gl.getUniformLocation(prog, 'uTime');
|
||||
var uIntro = gl.getUniformLocation(prog, 'uIntro');
|
||||
var uCalm = gl.getUniformLocation(prog, 'uCalm');
|
||||
var uHeat = gl.getUniformLocation(prog, 'uHeat');
|
||||
|
||||
var reduce = !!opts.reduceMotion;
|
||||
var finePointer = w.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
||||
var probe = opts.probe, probeVal = opts.probeVal, marker = opts.legendMarker;
|
||||
var calmEl = opts.calm || null;
|
||||
|
||||
var heatData = new Float32Array(MAX_HEAT * 4);
|
||||
var calmRect = [-1, -1, -1, -1];
|
||||
var trail = [];
|
||||
var quality = (w.devicePixelRatio || 1) > 1.5 ? 0.62 : 0.85;
|
||||
var width = 0, height = 0, rect = null;
|
||||
var running = false, visible = true, raf = 0;
|
||||
var time = Math.random() * 40, last = 0, frame = 0;
|
||||
var intro = reduce ? 1 : 0, introTarget = reduce ? 1 : 0;
|
||||
var frameTimes = [], tuned = false;
|
||||
|
||||
// Cursor in Feldkoordinaten: x ∈ [0, Seitenverhältnis], y ∈ [0, 1] (y nach oben)
|
||||
var cur = { x: 0, y: 0, tx: 0, ty: 0, inside: false, lastMove: -1e9, heat: 0, clientX: 0, clientY: 0 };
|
||||
var auto = { heat: 0 };
|
||||
var lastEmit = 0;
|
||||
|
||||
function measureCalm() {
|
||||
if (!calmEl || !rect) return;
|
||||
var r = calmEl.getBoundingClientRect();
|
||||
if (!r.width) { calmRect = [-1, -1, -1, -1]; return; }
|
||||
var hgt = rect.height, pad = 18;
|
||||
calmRect = [
|
||||
(r.left - pad - rect.left) / hgt,
|
||||
1 - (r.bottom + pad - rect.top) / hgt,
|
||||
(r.right + pad - rect.left) / hgt,
|
||||
1 - (r.top - pad - rect.top) / hgt
|
||||
];
|
||||
}
|
||||
|
||||
function resize() {
|
||||
rect = canvas.getBoundingClientRect();
|
||||
var dpr = Math.min(w.devicePixelRatio || 1, 2);
|
||||
var nw = Math.max(1, Math.round(rect.width * dpr * quality));
|
||||
var nh = Math.max(1, Math.round(rect.height * dpr * quality));
|
||||
if (nw !== width || nh !== height) {
|
||||
width = canvas.width = nw;
|
||||
height = canvas.height = nh;
|
||||
gl.viewport(0, 0, width, height);
|
||||
}
|
||||
measureCalm();
|
||||
}
|
||||
|
||||
function toField(clientX, clientY) {
|
||||
return { x: (clientX - rect.left) / rect.height, y: 1 - (clientY - rect.top) / rect.height };
|
||||
}
|
||||
|
||||
function emit(x, y, i, r) {
|
||||
trail.unshift({ x: x, y: y, i: i, r: r });
|
||||
if (trail.length > MAX_HEAT - 2) trail.pop();
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
if (e.pointerType && e.pointerType !== 'mouse') return;
|
||||
rect = canvas.getBoundingClientRect();
|
||||
var f = toField(e.clientX, e.clientY);
|
||||
cur.tx = f.x; cur.ty = f.y;
|
||||
cur.clientX = e.clientX - rect.left;
|
||||
cur.clientY = e.clientY - rect.top;
|
||||
if (!cur.inside) { cur.x = f.x; cur.y = f.y; }
|
||||
cur.inside = true;
|
||||
cur.lastMove = performance.now();
|
||||
if (probe) probe.classList.add('is-on');
|
||||
}
|
||||
function onLeave() {
|
||||
cur.inside = false;
|
||||
if (probe) probe.classList.remove('is-on');
|
||||
}
|
||||
function onTouch(e) {
|
||||
var t = e.touches && e.touches[0];
|
||||
if (!t) return;
|
||||
rect = canvas.getBoundingClientRect();
|
||||
var f = toField(t.clientX, t.clientY);
|
||||
emit(f.x, f.y, 0.85, 0.09);
|
||||
}
|
||||
|
||||
if (finePointer) {
|
||||
hero.addEventListener('pointermove', onMove, { passive: true });
|
||||
hero.addEventListener('pointerleave', onLeave, { passive: true });
|
||||
}
|
||||
hero.addEventListener('touchstart', onTouch, { passive: true });
|
||||
|
||||
function calmAt(px, py) {
|
||||
var a = smoothstep(calmRect[0] - 0.08, calmRect[0] + 0.02, px) * smoothstep(calmRect[1] - 0.08, calmRect[1] + 0.02, py);
|
||||
var b = (1 - smoothstep(calmRect[2] - 0.02, calmRect[2] + 0.08, px)) * (1 - smoothstep(calmRect[3] - 0.02, calmRect[3] + 0.08, py));
|
||||
return a * b;
|
||||
}
|
||||
|
||||
// Exakt dieselbe Rechnung wie im Shader – nur für einen Punkt
|
||||
function fieldAt(px, py) {
|
||||
var asp = width / height, t = time;
|
||||
var sx = px * 1.35, sy = py * 1.35;
|
||||
var qx = fbm(sx, sy + t * 0.05), qy = fbm(sx + 5.2, sy - t * 0.04);
|
||||
var rx = fbm(sx + 2.4 * qx + 1.7 + t * 0.06, sy + 2.4 * qy + 9.2 + t * 0.06);
|
||||
var ry = fbm(sx + 2.4 * qx + 8.3 - t * 0.05, sy + 2.4 * qy + 2.8 - t * 0.05);
|
||||
var f = fbm(sx + 2.2 * rx, sy + 2.2 * ry);
|
||||
var base = smoothstep(0.18, 0.86, f) * 0.46;
|
||||
var zx = px - asp * 0.8, zy = py - 0.76;
|
||||
base += 0.36 * Math.exp(-(zx * zx + zy * zy) / 0.11) * (0.8 + 0.2 * Math.sin(t * 0.45));
|
||||
var cx = px - asp * 0.16, cy = py - 0.12;
|
||||
base -= 0.12 * Math.exp(-(cx * cx + cy * cy) / 0.2);
|
||||
base *= 0.5 + 0.5 * smoothstep(0, 0.95, (px / asp) * 0.85 + py * 0.55);
|
||||
var heat = 0;
|
||||
for (var i = 0; i < MAX_HEAT; i++) {
|
||||
var o = i * 4, dx = px - heatData[o], dy = py - heatData[o + 1];
|
||||
heat += heatData[o + 2] * Math.exp(-(dx * dx + dy * dy) / Math.max(heatData[o + 3] * heatData[o + 3], 0.0001));
|
||||
}
|
||||
heat *= 1 - 0.62 * calmAt(px, py);
|
||||
var v = Math.max(base, 0) + heat * 0.66;
|
||||
v = 1 - Math.exp(-v * 1.35);
|
||||
return v * intro;
|
||||
}
|
||||
|
||||
function readTemperature() {
|
||||
if (!probeVal || !cur.inside) return;
|
||||
var v = Math.max(0, Math.min(1, fieldAt(cur.tx, cur.ty)));
|
||||
var temp = T_MIN + v * (T_MAX - T_MIN);
|
||||
probeVal.textContent = temp.toFixed(1).replace('.', ',') + ' °C';
|
||||
if (marker) marker.style.left = (v * 100).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
function step(now) {
|
||||
raf = 0;
|
||||
if (!running) return;
|
||||
var dt = last ? Math.min((now - last) / 1000, 0.05) : 0.016;
|
||||
last = now;
|
||||
if (!reduce) time += dt;
|
||||
|
||||
// Auflösung automatisch an die Leistung anpassen
|
||||
if (!tuned) {
|
||||
frameTimes.push(dt);
|
||||
if (frameTimes.length >= 80) {
|
||||
var avg = frameTimes.reduce(function (a, b) { return a + b; }, 0) / frameTimes.length;
|
||||
if (avg > 0.024 && quality > 0.38) { quality *= 0.75; resize(); frameTimes = []; }
|
||||
else tuned = true;
|
||||
}
|
||||
}
|
||||
|
||||
intro += (introTarget - intro) * (1 - Math.exp(-dt * 1.8));
|
||||
var asp = width / height;
|
||||
|
||||
// Maus
|
||||
var k = 1 - Math.exp(-dt * 9);
|
||||
cur.x += (cur.tx - cur.x) * k;
|
||||
cur.y += (cur.ty - cur.y) * k;
|
||||
var idle = (now - cur.lastMove) > 3200;
|
||||
cur.heat += ((cur.inside ? 1 : 0) - cur.heat) * (1 - Math.exp(-dt * 3));
|
||||
if (cur.inside && !idle && now - lastEmit > 55) {
|
||||
emit(cur.x, cur.y, 0.42, 0.075);
|
||||
lastEmit = now;
|
||||
}
|
||||
|
||||
// Autopilot: wandernde Wärmequelle oben rechts, solange niemand interagiert
|
||||
var autoOn = (!cur.inside || idle) && !reduce;
|
||||
auto.heat += ((autoOn ? 0.8 : 0) - auto.heat) * (1 - Math.exp(-dt * 1.2));
|
||||
var ax = asp * (0.7 + 0.16 * Math.sin(time * 0.21)) + 0.05 * Math.sin(time * 0.53);
|
||||
var ay = 0.66 + 0.16 * Math.sin(time * 0.29 + 1.3);
|
||||
if (autoOn && now - lastEmit > 140) {
|
||||
emit(ax, ay, 0.3 * auto.heat, 0.09);
|
||||
lastEmit = now;
|
||||
}
|
||||
|
||||
// Abkühlen & Ausbreiten
|
||||
for (var i = 0; i < trail.length; i++) {
|
||||
trail[i].i *= Math.exp(-dt * 0.85);
|
||||
trail[i].r += dt * 0.032;
|
||||
}
|
||||
|
||||
heatData[0] = cur.x; heatData[1] = cur.y; heatData[2] = cur.heat * 0.8; heatData[3] = 0.095;
|
||||
heatData[4] = ax; heatData[5] = ay; heatData[6] = auto.heat * 0.7; heatData[7] = 0.13;
|
||||
for (var j = 0; j < MAX_HEAT - 2; j++) {
|
||||
var o = (j + 2) * 4, tp = trail[j];
|
||||
if (tp) { heatData[o] = tp.x; heatData[o + 1] = tp.y; heatData[o + 2] = tp.i; heatData[o + 3] = tp.r; }
|
||||
else { heatData[o + 2] = 0; heatData[o + 3] = 0.1; }
|
||||
}
|
||||
|
||||
gl.uniform2f(uRes, width, height);
|
||||
gl.uniform1f(uTime, time);
|
||||
gl.uniform1f(uIntro, intro);
|
||||
gl.uniform4f(uCalm, calmRect[0], calmRect[1], calmRect[2], calmRect[3]);
|
||||
gl.uniform4fv(uHeat, heatData);
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
|
||||
frame++;
|
||||
if (frame % 4 === 0) readTemperature();
|
||||
if (probe && cur.inside) {
|
||||
probe.style.transform = 'translate3d(' + cur.clientX.toFixed(1) + 'px,' + cur.clientY.toFixed(1) + 'px,0)';
|
||||
}
|
||||
|
||||
raf = w.requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (running || !visible || document.hidden) return;
|
||||
running = true;
|
||||
last = 0;
|
||||
raf = w.requestAnimationFrame(step);
|
||||
}
|
||||
function stop() {
|
||||
running = false;
|
||||
if (raf) w.cancelAnimationFrame(raf);
|
||||
raf = 0;
|
||||
}
|
||||
|
||||
resize();
|
||||
if ('ResizeObserver' in w) {
|
||||
var ro = new ResizeObserver(function () { resize(); });
|
||||
ro.observe(canvas);
|
||||
if (calmEl) ro.observe(calmEl);
|
||||
} else {
|
||||
w.addEventListener('resize', resize);
|
||||
}
|
||||
if (d().fonts && d().fonts.ready) d().fonts.ready.then(function () { resize(); });
|
||||
|
||||
if ('IntersectionObserver' in w) {
|
||||
new IntersectionObserver(function (entries) {
|
||||
visible = entries[0].isIntersecting;
|
||||
if (visible) start(); else stop();
|
||||
}, { threshold: 0 }).observe(hero);
|
||||
}
|
||||
document.addEventListener('visibilitychange', function () { if (document.hidden) stop(); else start(); });
|
||||
canvas.addEventListener('webglcontextlost', function (e) { e.preventDefault(); stop(); root.classList.add('no-webgl'); });
|
||||
|
||||
start();
|
||||
|
||||
return {
|
||||
heatUp: function () { introTarget = 1; },
|
||||
measure: function () { rect = canvas.getBoundingClientRect(); measureCalm(); },
|
||||
start: start,
|
||||
stop: stop
|
||||
};
|
||||
}
|
||||
|
||||
function d() { return document; }
|
||||
|
||||
w.OlzThermal = { init: init };
|
||||
})(window);
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2024 darkroom.engineering
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
# Eingebundene Bibliotheken
|
||||
|
||||
Alle Bibliotheken werden lokal ausgeliefert (kein CDN, keine Datenübertragung an Dritte).
|
||||
|
||||
| Datei | Bibliothek | Version | Lizenz |
|
||||
|---|---|---|---|
|
||||
| `gsap.min.js`, `ScrollTrigger.min.js`, `SplitText.min.js` | [GSAP](https://gsap.com) | 3.15.0 | GSAP Standard „No Charge“ License – kostenlos, auch kommerziell (https://gsap.com/standard-license) |
|
||||
| `lenis.min.js` | [Lenis](https://github.com/darkroomengineering/lenis) | 1.3.26 | MIT (siehe `LICENSE-lenis.txt`) |
|
||||
|
||||
Schriften in `assets/fonts/`: Instrument Sans & Instrument Serif – SIL Open Font License 1.1 (siehe `OFL-*.txt`).
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* ScrollTrigger 3.15.0
|
||||
* https://gsap.com
|
||||
*
|
||||
* @license Copyright 2026, GreenSock. All rights reserved.
|
||||
* Subject to the terms at https://gsap.com/standard-license.
|
||||
* @author: Jack Doyle, jack@greensock.com
|
||||
*/
|
||||
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function _defineProperties(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,n.key,n)}}function r(){return Se||"undefined"!=typeof window&&(Se=window.gsap)&&Se.registerPlugin&&Se}function z(e,t){return~Le.indexOf(e)&&Le[Le.indexOf(e)+1][t]}function A(e){return!!~t.indexOf(e)}function B(e,t,r,n,i){return e.addEventListener(t,r,{passive:!1!==n,capture:!!i})}function C(e,t,r,n){return e.removeEventListener(t,r,!!n)}function F(){return Re&&Re.isPressed||Ie.cache++}function G(r,n){function fd(e){if(e||0===e){i&&(Ce.history.scrollRestoration="manual");var t=Re&&Re.isPressed;e=fd.v=Math.round(e)||(Re&&Re.iOS?1:0),r(e),fd.cacheID=Ie.cache,t&&o("ss",e)}else(n||Ie.cache!==fd.cacheID||o("ref"))&&(fd.cacheID=Ie.cache,fd.v=r());return fd.v+fd.offset}return fd.offset=0,r&&fd}function J(e,t){return(t&&t._ctx&&t._ctx.selector||Se.utils.toArray)(e)[0]||("string"==typeof e&&!1!==Se.config().nullTargetWarn?console.warn("Element not found:",e):null)}function L(t,e){var r=e.s,n=e.sc;A(t)&&(t=Me.scrollingElement||ke);var i=Ie.indexOf(t),o=n===Xe.sc?1:2;~i||(i=Ie.push(t)-1),Ie[i+o]||B(t,"scroll",F);var a=Ie[i+o],s=a||(Ie[i+o]=G(z(t,r),!0)||(A(t)?n:G(function(e){return arguments.length?t[r]=e:t[r]})));return s.target=t,a||(s.smooth="smooth"===Se.getProperty(t,"scrollBehavior")),s}function M(e,t,i){function Hd(e,t){var r=Ye();t||n<r-s?(a=o,o=e,l=s,s=r):i?o+=e:o=a+(e-a)/(r-l)*(s-l)}var o=e,a=e,s=Ye(),l=s,n=t||50,c=Math.max(500,3*n);return{update:Hd,reset:function reset(){a=o=i?0:o,l=s=0},getVelocity:function getVelocity(e){var t=l,r=a,n=Ye();return!e&&0!==e||e===o||Hd(e),s===l||c<n-l?0:(o+(i?r:-r))/((i?n:s)-t)*1e3}}}function N(e,t){return t&&!e._gsapAllow&&!1!==e.cancelable&&e.preventDefault(),e.changedTouches?e.changedTouches[0]:e}function O(e){var t=Math.max.apply(Math,e),r=Math.min.apply(Math,e);return Math.abs(t)>=Math.abs(r)?t:r}function P(){(Ae=Se.core.globals().ScrollTrigger)&&Ae.core&&function _integrate(){var e=Ae.core,r=e.bridge||{},t=e._scrollers,n=e._proxies;t.push.apply(t,Ie),n.push.apply(n,Le),Ie=t,Le=n,o=function _bridge(e,t){return r[e](t)}}()}function Q(e){return Se=e||r(),!Te&&Se&&"undefined"!=typeof document&&document.body&&(Ce=window,ke=(Me=document).documentElement,Ee=Me.body,t=[Ce,Me,ke,Ee],Se.utils.clamp,Be=Se.core.context||function(){},Oe="onpointerenter"in Ee?"pointer":"mouse",Pe=k.isTouch=Ce.matchMedia&&Ce.matchMedia("(hover: none), (pointer: coarse)").matches?1:"ontouchstart"in Ce||0<navigator.maxTouchPoints||0<navigator.msMaxTouchPoints?2:0,De=k.eventTypes=("ontouchstart"in ke?"touchstart,touchmove,touchcancel,touchend":"onpointerdown"in ke?"pointerdown,pointermove,pointercancel,pointerup":"mousedown,mousemove,mouseup,mouseup").split(","),setTimeout(function(){return i=0},500),Te=1),Ae||P(),Te}var Se,Te,Ce,Me,ke,Ee,Pe,Oe,Ae,t,Re,De,Be,i=1,ze=[],Ie=[],Le=[],Ye=Date.now,o=function _bridge(e,t){return t},n="scrollLeft",a="scrollTop",Ne={s:n,p:"left",p2:"Left",os:"right",os2:"Right",d:"width",d2:"Width",a:"x",sc:G(function(e){return arguments.length?Ce.scrollTo(e,Xe.sc()):Ce.pageXOffset||Me[n]||ke[n]||Ee[n]||0})},Xe={s:a,p:"top",p2:"Top",os:"bottom",os2:"Bottom",d:"height",d2:"Height",a:"y",op:Ne,sc:G(function(e){return arguments.length?Ce.scrollTo(Ne.sc(),e):Ce.pageYOffset||Me[a]||ke[a]||Ee[a]||0})};Ne.op=Xe,Ie.cache=0;var k=(Observer.prototype.init=function init(e){Te||Q(Se)||console.warn("Please gsap.registerPlugin(Observer)"),Ae||P();var i=e.tolerance,a=e.dragMinimum,t=e.type,o=e.target,r=e.lineHeight,n=e.debounce,s=e.preventDefault,l=e.onStop,c=e.onStopDelay,u=e.ignore,f=e.wheelSpeed,d=e.event,p=e.onDragStart,g=e.onDragEnd,h=e.onDrag,v=e.onPress,b=e.onRelease,m=e.onRight,y=e.onLeft,x=e.onUp,_=e.onDown,w=e.onChangeX,S=e.onChangeY,T=e.onChange,k=e.onToggleX,E=e.onToggleY,R=e.onHover,D=e.onHoverEnd,z=e.onMove,I=e.ignoreCheck,Y=e.isNormalizer,X=e.onGestureStart,U=e.onGestureEnd,q=e.onWheel,H=e.onEnable,W=e.onDisable,V=e.onClick,G=e.scrollSpeed,j=e.capture,K=e.allowClicks,$=e.lockAxis,Z=e.onLockAxis;function hf(){return xe=Ye()}function jf(e,t){return(se.event=e)&&u&&function _isWithin(e,t){for(var r=t.length;r--;)if(t[r]===e||t[r].contains(e))return!0;return!1}(e.target,u)||t&&he&&"touch"!==e.pointerType||I&&I(e,t)}function lf(){var e=se.deltaX=O(me),t=se.deltaY=O(ye),r=Math.abs(e)>=i,n=Math.abs(t)>=i;T&&(r||n)&&T(se,e,t,me,ye),r&&(m&&0<se.deltaX&&m(se),y&&se.deltaX<0&&y(se),w&&w(se),k&&se.deltaX<0!=le<0&&k(se),le=se.deltaX,me[0]=me[1]=me[2]=0),n&&(_&&0<se.deltaY&&_(se),x&&se.deltaY<0&&x(se),S&&S(se),E&&se.deltaY<0!=ce<0&&E(se),ce=se.deltaY,ye[0]=ye[1]=ye[2]=0),(ne||re)&&(z&&z(se),re&&(p&&1===re&&p(se),h&&h(se),re=0),ne=!1),oe&&!(oe=!1)&&Z&&Z(se),ie&&(q(se),ie=!1),ee=0}function mf(e,t,r){me[r]+=e,ye[r]+=t,se._vx.Line truncated
|
||||
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* SplitText 3.15.0
|
||||
* https://gsap.com
|
||||
*
|
||||
* @license Copyright 2026, GreenSock. All rights reserved. Subject to the terms at https://gsap.com/standard-license.
|
||||
* @author: Jack Doyle
|
||||
*/
|
||||
|
||||
(function(k,A){typeof exports=="object"&&typeof module!="undefined"?A(exports):typeof define=="function"&&define.amd?define(["exports"],A):(k=typeof globalThis!="undefined"?globalThis:k||self,A(k.window=k.window||{}))})(this,function(k){"use strict";let A,D,Z=typeof Symbol=="function"?Symbol():"_split",K,he=()=>K||Y.register(window.gsap),$=typeof Intl!="undefined"&&"Segmenter"in Intl?new Intl.Segmenter:0,I=e=>e?typeof e=="string"?I(document.querySelectorAll(e)):"length"in e?Array.from(e).reduce((t,i)=>(typeof i=="string"?t.push(...I(i)):t.push(i),t),[]):[e]:[],ee=e=>I(e).filter(t=>t&&t.nodeType===1),Q=[],U=function(){},de={add:e=>e()},pe=/\s+/g,te=new RegExp("\\p{RI}\\p{RI}|\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?(\\u{200D}\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?)*|.","gu"),J={left:0,top:0,width:0,height:0},ue=(e,t)=>{for(;++t<e.length&&e[t]===J;);return e[t]||J},ie=({element:e,html:t,ariaL:i,ariaH:n})=>{e.innerHTML=t,i?e.setAttribute("aria-label",i):e.removeAttribute("aria-label"),n?e.setAttribute("aria-hidden",n):e.removeAttribute("aria-hidden")},ne=(e,t)=>{if(t){let i=new Set(e.join("").match(t)||Q),n=e.length,a,c,l,o;if(i.size)for(;--n>-1;){c=e[n];for(l of i)if(l.startsWith(c)&&l.length>c.length){for(a=0,o=c;l.startsWith(o+=e[n+ ++a])&&o.length<l.length;);if(a&&o.length===l.length){e[n]=l,e.splice(n+1,a);break}}}}return e},le=e=>window.getComputedStyle(e).display==="inline"&&(e.style.display="inline-block"),M=(e,t,i)=>t.insertBefore(typeof e=="string"?document.createTextNode(e):e,i),X=(e,t,i)=>{let n=t[e+"sClass"]||"",{tag:a="div",aria:c="auto",propIndex:l=!1}=t,o=e==="line"?"block":"inline-block",d=n.indexOf("++")>-1,_=y=>{let m=document.createElement(a),E=i.length+1;return n&&(m.className=n+(d?" "+n+E:"")),l&&m.style.setProperty("--"+e,E+""),c!=="none"&&m.setAttribute("aria-hidden","true"),a!=="span"&&(m.style.position="relative",m.style.display=o),m.textContent=y,i.push(m),m};return d&&(n=n.replace("++","")),_.collection=i,_},fe=(e,t,i,n)=>{let a=X("line",i,n),c=window.getComputedStyle(e).textAlign||"left";return(l,o)=>{let d=a("");for(d.style.textAlign=c,e.insertBefore(d,t[l]);l<o;l++)d.appendChild(t[l]);d.normalize()}},se=(e,t,i,n,a,c,l,o,d,_)=>{var y;let m=Array.from(e.childNodes),E=0,{wordDelimiter:R,reduceWhiteSpace:B=!0,prepareText:V}=t,G=e.getBoundingClientRect(),j=G,q=!B&&window.getComputedStyle(e).whiteSpace.substring(0,3)==="pre",C=0,b=i.collection,r,f,O,s,g,x,z,h,p,W,S,T,N,F,w,u,L,v;for(typeof R=="object"?(O=R.delimiter||R,f=R.replaceWith||""):f=R===""?"":R||" ",r=f!==" ";E<m.length;E++)if(s=m[E],s.nodeType===3){for(w=s.textContent||"",B?w=w.replace(pe," "):q&&(w=w.replace(/\n/g,f+`
|
||||
`)),V&&(w=V(w,e)),s.textContent=w,g=f||O?w.split(O||f):w.match(o)||Q,L=g[g.length-1],h=r?L.slice(-1)===" ":!L,L||g.pop(),j=G,z=r?g[0].charAt(0)===" ":!g[0],z&&M(" ",e,s),g[0]||g.shift(),ne(g,d),c&&_||(s.textContent=""),p=1;p<=g.length;p++)if(u=g[p-1],!B&&q&&u.charAt(0)===`
|
||||
`&&((y=s.previousSibling)==null||y.remove(),M(document.createElement("br"),e,s),u=u.slice(1)),!B&&u==="")M(f,e,s);else if(u===" ")e.insertBefore(document.createTextNode(" "),s);else{if(r&&u.charAt(0)===" "&&M(" ",e,s),C&&p===1&&!z&&b.indexOf(C.parentNode)>-1?(x=b[b.length-1],x.appendChild(document.createTextNode(n?"":u))):(x=i(n?"":u),M(x,e,s),C&&p===1&&!z&&x.insertBefore(C,x.firstChild)),n)for(S=$?ne([...$.segment(u)].map(H=>H.segment),d):u.match(o)||Q,v=0;v<S.length;v++)x.appendChild(S[v]===" "?document.createTextNode(" "):n(S[v]));if(c&&_){if(w=s.textContent=w.substring(u.length+1,w.length),W=x.getBoundingClientRect(),W.top>j.top&&W.left<=j.left){for(T=e.cloneNode(),N=e.childNodes[0];N&&N!==x;)F=N,N=N.nextSibling,T.appendChild(F);e.parentNode.insertBefore(T,e),a&&le(T)}j=W}(p<g.length||h)&&M(p>=g.length?" ":r&&u.slice(-1)===" "?" "+f:f,e,s)}e.removeChild(s),C=0}else s.nodeType===1&&(l&&l.indexOf(s)>-1?(b.indexOf(s.previousSibling)>-1&&b[b.length-1].appendChild(s),C=s):(se(s,t,i,n,a,c,l,o,d,!0),C=0),a&&le(s))};const re=class ae{constructor(t,i){this.isSplit=!1,he(),this.elements=ee(t),this.chars=[],this.words=[],this.lines=[],this.masks=[],this.vars=i,this.elements.forEach(l=>{var o;i.overwrite!==!1&&((o=l[Z])==null||o._data.orig.filter(({element:d})=>d===l).forEach(ie)),l[Z]=this}),this._split=()=>this.isSplit&&this.split(this.vars);let n=[],a,c=()=>{let l=n.length,o;for(;l--;){o=n[l];let d=o.element.offsetWidth;if(d!==o.width){o.width=d,this._split();return}}};this._data={orig:n,obs:typeof ResizeObserver!="undefined"&&new ResizeObserver(()=>{clearTimeout(a),a=setTimeout(c,200)})},U(this),this.split(i)}split(t){return(this._ctx||de).add(()=>{this.isSplit&&this.revert(),this.vars=t=t||this.vars||{};let{type:i="chars,words,lines",aria:n="auto",deepSlice:a=!0,smartWrap:c,onSplit:l,autoSplit:o=!1,specialChars:d,mask:_}=this.vars,y=i.indexOf("lines")>-1,m=i.indexOf("chars")>-1,E=i.indexOf("words")>-1,R=m&&!E&&!y,B=d&&("push"in d?new RegExp("(?:"+d.join("|")+")","gu"):d),V=B?new RegExp(B.source+"|"+te.source,"gu"):te,G=!!t.ignore&&ee(t.ignore),{orig:j,animTime:q,obs:C}=this._data,b;(m||E||y)&&(this.elements.forEach((r,f)=>{j[f]={element:r,html:r.innerHTML,ariaL:r.getAttribute("aria-label"),ariaH:r.getAttribute("aria-hidden")},n==="auto"?r.setAttribute("aria-label",(r.textContent||"").trim()):n==="hidden"&&r.setAttribute("aria-hidden","true");let O=[],s=[],g=[],x=m?X("char",t,O):null,z=X("word",t,s),h,p,W,S;if(se(r,t,z,x,R,a&&(y||R),G,V,B,!1),y){let T=I(r.childNodes),N=fe(r,T,t,g),F,w=[],u=0,L=T.map(P=>P.nodeType===1?P.getBoundingClientRect():J),v=J,H;for(h=0;h<T.length;h++)F=T[h],F.nodeType===1&&(F.nodeName==="BR"?((!h||T[h-1].nodeName!=="BR")&&(w.push(F),N(u,h+1)),u=h+1,v=ue(L,h)):(H=L[h],h&&H.top>v.top&&H.left<v.left+v.width-1&&(N(u,h),u=h),v=H));u<h&&N(u,h),w.forEach(P=>{var oe;return(oe=P.parentNode)==null?void 0:oe.removeChild(P)})}if(!E){for(h=0;h<s.length;h++)if(p=s[h],m||!p.nextSibling||p.nextSibling.nodeType!==3)if(c&&!y){for(W=document.createElement("span"),W.style.whiteSpace="nowrap";p.firstChild;)W.appendChild(p.firstChild);p.replaceWith(W)}else p.replaceWith(...p.childNodes);else S=p.nextSibling,S&&S.nodeType===3&&(S.textContent=(p.textContent||"")+(S.textContent||""),p.remove());s.length=0,r.normalize()}this.lines.push(...g),this.words.push(...s),this.chars.push(...O)}),_&&this[_]&&this.masks.push(...this[_].map(r=>{let f=r.cloneNode();return r.replaceWith(f),f.appendChild(r),r.className&&(f.className=r.className.trim().split(" ").map(O=>O+"-mask").join(" ")),f.style.overflow="clip",f}))),this.isSplit=!0,D&&y&&o&&D.addEventListener("loadingdone",this._split),(b=l&&l(this))&&b.totalTime&&(this._data.anim=q?b.totalTime(q):b),y&&o&&this.elements.forEach((r,f)=>{j[f].width=r.offsetWidth,C&&C.observe(r)})}),this}kill(){let{obs:t}=this._data;t&&t.disconnect(),D==null||D.removeEventListener("loadingdone",this._split)}revert(){var t,i;if(this.isSplit){let{orig:n,anim:a}=this._data;this.kill(),n.forEach(ie),this.chars.length=this.words.length=this.lines.length=n.length=this.masks.length=0,this.isSplit=!1,a&&(this._data.animTime=a.totalTime(),a.revert()),(i=(t=this.vars).onRevert)==null||i.call(t,this)}return this}static create(t,i){return new ae(t,i)}static register(t){A=A||t||window.gsap,A&&(I=A.utils.toArray,U=A.core.context||U),!K&&window.innerWidth>0&&(D=document.fonts,K=!0)}};re.version="3.15.0";let Y=re;k.SplitText=Y,k.default=Y;if (typeof(window)==="undefined"||window!==k){Object.defineProperty(k,"__esModule",{value:!0})} else {delete k.default}});
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* GSAP 3.15.0
|
||||
* https://gsap.com
|
||||
*
|
||||
* @license Copyright 2026, GreenSock. All rights reserved.
|
||||
* Subject to the terms at https://gsap.com/standard-license.
|
||||
* @author: Jack Doyle, jack@greensock.com
|
||||
*/
|
||||
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function R(t){return(i=bt(t,ht))&&Fe}function S(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function T(t,e){return!e&&console.warn(t)}function U(t,e){return t&&(ht[t]=e)&&i&&(i[t]=e)||ht}function V(){return 0}function ga(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=yt.length;r--&&!yt[r].targetTest(i););e=yt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Xt(t[r],e)))||t.splice(r,1);return t}function ha(t){return t._gsap||ga(Pt(t))[0]._gsap}function ia(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ja(t,e){return(t=t.split(",")).forEach(e)||t}function ka(t){return Math.round(1e5*t)/1e5||0}function la(t){return Math.round(1e7*t)/1e7||0}function ma(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function na(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++i<r;);return i<r}function oa(){var t,e,r=pt.length,i=pt.slice(0);for(_t={},t=pt.length=0;t<r;t++)(e=i[t])&&e._lazy&&(e.render(e._lazy[0],e._lazy[1],!0)._lazy=0)}function pa(t){return!!(t._initted||t._startAt||t.add)}function qa(t,e,r,i){pt.length&&!I&&oa(),t.render(e,r,i||!!(I&&e<0&&pa(t))),pt.length&&!I&&oa()}function ra(t){var e=parseFloat(t);return(e||0===e)&&(t+"").match(ot).length<2?e:r(t)?t.trim():t}function sa(t){return t}function ta(t,e){for(var r in e)r in t||(t[r]=e[r]);return t}function wa(t,e){for(var r in e)"__proto__"!==r&&"constructor"!==r&&"prototype"!==r&&(t[r]=v(e[r])?wa(t[r]||(t[r]={}),e[r]):e[r]);return t}function xa(t,e){var r,i={};for(r in t)r in e||(i[r]=t[r]);return i}function ya(t){var e=t.parent||L,r=t.keyframes?function _setKeyframeDefaults(i){return function(t,e){for(var r in e)r in t||"duration"===r&&i||"ease"===r||(t[r]=e[r])}}(K(t.keyframes)):ta;if(w(t.inherit))for(;e;)r(t,e.vars.defaults),e=e.parent||e._dp;return t}function Aa(t,e,r,i,n){void 0===r&&(r="_first"),void 0===i&&(i="_last");var a,s=t[i];if(n)for(a=e[n];s&&s[n]>a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function Ba(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function Ca(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Da(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Fa(t,e,r,i){return t._startAt&&(I?t._startAt.revert(ft):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ha(t){return t._repeat?wt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ja(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ka(t){return t._end=la(t._start+(t._tDur/Math.abs(t._ts||t._rts||q)||0))}function La(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=la(r._time-(0<t._ts?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),Ka(t),r._dirty||Da(r,t)),t}function Ma(t,e){var r;if((e._time||!e._dur&&e._initted||e._start<t._time&&(e._dur||!e.add))&&(r=Ja(t.rawTime(),e),(!e._dur||Mt(0,e.totalDuration(),r)-e._tTime>q)&&e.render(r,!0)),Da(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur<t.duration())for(r=t;r._dp;)0<=r.rawTime()&&r.totalTime(r._tTime),r=r._dp;t._zTime=-q}}function Na(e,r,i,n){return r.parent&&Ca(r),r._start=la((t(i)?i:i||e!==L?Ot(e,i,r):e._time)+r._delay),r._end=la(r._start+(r.totalDuration()/Math.abs(r.timeScale())||0)),Aa(e,r,"_first","_last",e._sort?"_start":0),xt(r)||(e._recent=r),n||Ma(e,r),e._ts<0&&La(e,e._tTime),e}function Oa(t,e){return(ht.ScrollTrigger||S("scrollTrigger",e))&&ht.ScrollTrigger.create(e,t)}function Pa(t,e,r,i,n){return Ht(t,e,n),t._initted?!r&&t._pt&&!I&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&f!==It.frame?(pt.push(t),t._lazy=[n,i],1):void 0:1}function Ua(t,e,r,i){var n=t._repeat,a=la(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=a/t._dur),t._dur=a,t._tDur=n?n<0?1e10:la(a*(n+1)+t._rDelay*n):a,0<s&&!i&&La(t,t._tTime=t._tDur*s),t.parent&&Ka(t),r||Da(t.parent,t),t}function Va(t){return t instanceof Gt?Da(t)Line truncated
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
(function(){var e=`1.3.26`;function t(e,t,n){return Math.max(e,Math.min(t,n))}function n(e,t,n){return(1-n)*e+n*t}function r(e,t,r,i){return n(e,t,1-Math.exp(-r*i))}function i(e,t){return(e%t+t)%t}var a=class{isRunning=!1;value=0;from=0;to=0;currentTime=0;lerp;duration;easing;onUpdate;advance(e){if(!this.isRunning)return;let n=!1;if(this.duration&&this.easing){this.currentTime+=e;let r=t(0,this.currentTime/this.duration,1);n=r>=1;let i=n?1:this.easing(r);this.value=this.from+(this.to-this.from)*i}else this.lerp?(this.value=r(this.value,this.to,this.lerp*60,e),Math.round(this.value)===Math.round(this.to)&&(this.value=this.to,n=!0)):(this.value=this.to,n=!0);n&&this.stop(),this.onUpdate?.(this.value,n)}stop(){this.isRunning=!1}fromTo(e,t,{lerp:n,duration:r,easing:i,onStart:a,onUpdate:o}){this.from=this.value=e,this.to=t,this.lerp=n,this.duration=r,this.easing=i,this.currentTime=0,this.isRunning=!0,a?.(),this.onUpdate=o}};function o(e,t){let n;return function(...r){clearTimeout(n),n=setTimeout(()=>{n=void 0,e.apply(this,r)},t)}}var s=class{width=0;height=0;scrollHeight=0;scrollWidth=0;debouncedResize;wrapperResizeObserver;contentResizeObserver;constructor(e,t,{autoResize:n=!0,debounce:r=250}={}){this.wrapper=e,this.content=t,n&&(this.debouncedResize=o(this.resize,r),this.wrapper instanceof Window?window.addEventListener(`resize`,this.debouncedResize):(this.wrapperResizeObserver=new ResizeObserver(this.debouncedResize),this.wrapperResizeObserver.observe(this.wrapper)),this.contentResizeObserver=new ResizeObserver(this.debouncedResize),this.contentResizeObserver.observe(this.content)),this.resize()}destroy(){this.wrapperResizeObserver?.disconnect(),this.contentResizeObserver?.disconnect(),this.wrapper===window&&this.debouncedResize&&window.removeEventListener(`resize`,this.debouncedResize)}resize=()=>{this.onWrapperResize(),this.onContentResize()};onWrapperResize=()=>{this.wrapper instanceof Window?(this.width=window.innerWidth,this.height=window.innerHeight):(this.width=this.wrapper.clientWidth,this.height=this.wrapper.clientHeight)};onContentResize=()=>{this.wrapper instanceof Window?(this.scrollHeight=this.content.scrollHeight,this.scrollWidth=this.content.scrollWidth):(this.scrollHeight=this.wrapper.scrollHeight,this.scrollWidth=this.wrapper.scrollWidth)};get limit(){return{x:this.scrollWidth-this.width,y:this.scrollHeight-this.height}}},c=class{events={};emit(e,...t){let n=this.events[e]||[];for(let e=0,r=n.length;e<r;e++)n[e]?.(...t)}on(e,t){return this.events[e]?this.events[e].push(t):this.events[e]=[t],()=>{this.events[e]=this.events[e]?.filter(e=>t!==e)}}off(e,t){this.events[e]=this.events[e]?.filter(e=>t!==e)}destroy(){this.events={}}};let l={passive:!1};function u(e,t){return e===1?16.666666666666668:e===2?t:1}var d=class{touchStart={x:0,y:0};lastDelta={x:0,y:0};window={width:0,height:0};emitter=new c;constructor(e,t={wheelMultiplier:1,touchMultiplier:1}){this.element=e,this.options=t,window.addEventListener(`resize`,this.onWindowResize),this.onWindowResize(),this.element.addEventListener(`wheel`,this.onWheel,l),this.element.addEventListener(`touchstart`,this.onTouchStart,l),this.element.addEventListener(`touchmove`,this.onTouchMove,l),this.element.addEventListener(`touchend`,this.onTouchEnd,l)}on(e,t){return this.emitter.on(e,t)}destroy(){this.emitter.destroy(),window.removeEventListener(`resize`,this.onWindowResize),this.element.removeEventListener(`wheel`,this.onWheel,l),this.element.removeEventListener(`touchstart`,this.onTouchStart,l),this.element.removeEventListener(`touchmove`,this.onTouchMove,l),this.element.removeEventListener(`touchend`,this.onTouchEnd,l)}onTouchStart=e=>{let{clientX:t,clientY:n}=e.targetTouches?e.targetTouches[0]:e;this.touchStart.x=t,this.touchStart.y=n,this.lastDelta={x:0,y:0},this.emitter.emit(`scroll`,{deltaX:0,deltaY:0,event:e})};onTouchMove=e=>{let{clientX:t,clientY:n}=e.targetTouches?e.targetTouches[0]:e,r=-(t-this.touchStart.x)*this.options.touchMultiplier,i=-(n-this.touchStart.y)*this.options.touchMultiplier;this.touchStart.x=t,this.touchStart.y=n,this.lastDelta={x:r,y:i},this.emitter.emit(`scroll`,{deltaX:r,deltaY:i,event:e})};onTouchEnd=e=>{this.emitter.emit(`scroll`,{deltaX:this.lastDelta.x,deltaY:this.lastDelta.y,event:e})};onWheel=e=>{let{deltaX:t,deltaY:n,deltaMode:r}=e,i=u(r,this.window.width),a=u(r,this.window.height);t*=i,n*=a,t*=this.options.wheelMultiplier,n*=this.options.wheelMultiplier,this.emitter.emit(`scroll`,{deltaX:t,deltaY:n,event:e})};onWindowResize=()=>{this.window={width:window.innerWidth,height:window.innerHeight}}};let f=e=>Math.min(1,1.001-2**(-10*e));var p=class{_isScrolling=!1;_isStopped=!1;_isLocked=!1;_preventNextNativeScrollEvent=!1;_resetVelocityTimeout=null;_rafId=null;_isDraggingSelection=!1;reducedMotionMediaQuery=window.matchMedia(`(prefers-reduced-motion: reduce)`);isTouching;isIos;time=0;userData={};lastVelocity=0;velocity=0;direction=0;options;targetScroll;animatedScroll;animate=new a;emitter=new c;dimensions;virtualScroll;conLine truncated
|
||||
//# sourceMappingURL=lenis.min.js.map
|
||||
@@ -0,0 +1,214 @@
|
||||
<!doctype html>
|
||||
<html lang="de" class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Datenschutz – Olzinger Haustechnik</title>
|
||||
<meta name="description" content="Datenschutzerklärung der Olzinger GmbH – Haustechnik in Ergolding bei Landshut.">
|
||||
<meta name="robots" content="noindex, follow">
|
||||
<link rel="canonical" href="https://www.olzinger.de/datenschutz.html">
|
||||
<meta name="theme-color" content="#0B151E">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="icon" href="assets/img/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="assets/img/favicon-32.png" sizes="32x32" type="image/png">
|
||||
<link rel="apple-touch-icon" href="assets/img/apple-touch-icon.png">
|
||||
<link rel="preload" href="assets/fonts/instrument-sans-var.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="assets/css/main.css">
|
||||
<script>
|
||||
(function (d, w) {
|
||||
var h = d.documentElement;
|
||||
h.className = h.className.replace('no-js', 'js');
|
||||
w.setTimeout(function () { if (!w.__olzReady) { h.classList.remove('js'); h.classList.add('no-js'); } }, 4500);
|
||||
})(document, window);
|
||||
</script>
|
||||
<script src="assets/vendor/gsap.min.js" defer></script>
|
||||
<script src="assets/vendor/ScrollTrigger.min.js" defer></script>
|
||||
<script src="assets/vendor/SplitText.min.js" defer></script>
|
||||
<script src="assets/vendor/lenis.min.js" defer></script>
|
||||
<script src="assets/js/contact.js" defer></script>
|
||||
<script src="assets/js/main.js" defer></script>
|
||||
</head>
|
||||
<body class="page">
|
||||
<!--
|
||||
HINWEIS: Diese Datenschutzerklärung ist auf die technische Umsetzung dieser Website
|
||||
zugeschnitten (keine Cookies, kein Tracking, lokal eingebundene Schriften und Skripte).
|
||||
Sie ersetzt keine Rechtsberatung – bitte vor dem Livegang prüfen lassen und den
|
||||
Hosting-Anbieter (TODO) ergänzen.
|
||||
-->
|
||||
<a class="skip-link" href="#main">Zum Inhalt springen</a>
|
||||
|
||||
<svg class="sprite" aria-hidden="true" focusable="false">
|
||||
<symbol id="i-phone" viewBox="0 0 24 24"><path d="M6.5 3.5h3l1.6 4.4-2.1 1.3a11.5 11.5 0 0 0 5.8 5.8l1.3-2.1 4.4 1.6v3a2 2 0 0 1-2.2 2A17 17 0 0 1 4.5 5.7a2 2 0 0 1 2-2.2z"/></symbol>
|
||||
<symbol id="i-mail" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="m4 7 8 6 8-6"/></symbol>
|
||||
<symbol id="i-arrow" viewBox="0 0 24 24"><path d="M4 12h15M13 6l6 6-6 6"/></symbol>
|
||||
<symbol id="i-arrow-up" viewBox="0 0 24 24"><path d="M12 20V5M6 11l6-6 6 6"/></symbol>
|
||||
<symbol id="i-arrow-left" viewBox="0 0 24 24"><path d="M20 12H5M11 6l-6 6 6 6"/></symbol>
|
||||
</svg>
|
||||
|
||||
<header class="nav" data-nav>
|
||||
<div class="nav__bar">
|
||||
<a class="logo" href="index.html" aria-label="Olzinger Haustechnik – zur Startseite">
|
||||
<svg class="logo__mark" viewBox="0 0 40 40" aria-hidden="true">
|
||||
<defs><linearGradient id="g-logo" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#2E9BEF"/><stop offset=".55" stop-color="#C2456B"/><stop offset="1" stop-color="#FF6A2B"/></linearGradient></defs>
|
||||
<path d="M11.97 31.47A14 14 0 1 1 28.03 31.47" fill="none" stroke="url(#g-logo)" stroke-width="5.5" stroke-linecap="round"/>
|
||||
<circle cx="20" cy="20" r="3.4" fill="currentColor"/>
|
||||
</svg>
|
||||
<span class="logo__text"><span class="logo__word">Olzinger</span><span class="logo__sub">Haustechnik</span></span>
|
||||
</a>
|
||||
<nav class="nav__links" aria-label="Hauptnavigation">
|
||||
<a href="index.html#leistungen">Leistungen</a>
|
||||
<a href="index.html#energie-haus">Energie-Haus</a>
|
||||
<a href="index.html#heizungs-finder">Heizungs-Finder</a>
|
||||
<a href="index.html#ueber-uns">Über uns</a>
|
||||
<a href="index.html#karriere">Karriere</a>
|
||||
</nav>
|
||||
<div class="nav__actions">
|
||||
<a class="nav__phone" href="tel:+4987172617"><svg class="icon" aria-hidden="true"><use href="#i-phone"/></svg><span>0871 72617</span></a>
|
||||
<a class="btn btn--sm btn--warm" href="index.html#kontakt"><span class="btn__label">Anfrage starten</span></a>
|
||||
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="menu" data-menu-toggle>
|
||||
<span class="nav__toggle-lines" aria-hidden="true"><i></i><i></i></span>
|
||||
<span class="sr-only">Menü öffnen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="menu" id="menu" data-menu hidden>
|
||||
<div class="menu__glow" aria-hidden="true"></div>
|
||||
<nav class="menu__links" aria-label="Mobile Navigation">
|
||||
<a href="index.html#leistungen"><span>01</span>Leistungen</a>
|
||||
<a href="index.html#energie-haus"><span>02</span>Energie-Haus</a>
|
||||
<a href="index.html#heizungs-finder"><span>03</span>Heizungs-Finder</a>
|
||||
<a href="index.html#ablauf"><span>04</span>Ablauf</a>
|
||||
<a href="index.html#ueber-uns"><span>05</span>Über uns</a>
|
||||
<a href="index.html#karriere"><span>06</span>Karriere</a>
|
||||
<a href="index.html#kontakt"><span>07</span>Kontakt</a>
|
||||
</nav>
|
||||
<div class="menu__foot">
|
||||
<a href="tel:+4987172617"><svg class="icon" aria-hidden="true"><use href="#i-phone"/></svg>0871 72617</a>
|
||||
<a href="mailto:kontakt@olzinger.de"><svg class="icon" aria-hidden="true"><use href="#i-mail"/></svg>kontakt@olzinger.de</a>
|
||||
<p class="status status--dark" data-status><span class="status__dot" aria-hidden="true"></span><span class="status__text">Mo–Do 7–16:30 Uhr · Fr 7–11:30 Uhr</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
<section class="page-hero" aria-labelledby="page-title">
|
||||
<div class="container">
|
||||
<p class="label"><a href="index.html">Startseite</a> / Rechtliches</p>
|
||||
<h1 class="h1" id="page-title" data-split>Datenschutz­erklärung</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="legal sheet">
|
||||
<div class="container legal__grid">
|
||||
<nav class="legal__toc" aria-label="Inhalt">
|
||||
<a href="#verantwortlicher">Verantwortlicher</a>
|
||||
<a href="#ueberblick">Auf einen Blick</a>
|
||||
<a href="#hosting">Hosting & Server-Logfiles</a>
|
||||
<a href="#cookies">Cookies & Tracking</a>
|
||||
<a href="#kontakt">Kontaktaufnahme</a>
|
||||
<a href="#finder">Heizungs-Finder</a>
|
||||
<a href="#bewerbung">Bewerbungen</a>
|
||||
<a href="#links">Externe Links</a>
|
||||
<a href="#rechte">Ihre Rechte</a>
|
||||
</nav>
|
||||
|
||||
<div class="legal__body" data-reveal>
|
||||
<h2 id="verantwortlicher">1. Verantwortlicher</h2>
|
||||
<p>Verantwortlich für die Datenverarbeitung auf dieser Website im Sinne der Datenschutz-Grundverordnung (DSGVO) ist:</p>
|
||||
<div class="legal__card">
|
||||
<p><strong>Olzinger GmbH</strong></p>
|
||||
<p>Ahornstraße 112, 84030 Ergolding</p>
|
||||
<p>Vertreten durch die Geschäftsführer Franz Ramsauer und Robert Straub</p>
|
||||
<p>Telefon: <a href="tel:+4987172617">0871 72617</a> · E-Mail: <a href="mailto:kontakt@olzinger.de">kontakt@olzinger.de</a></p>
|
||||
</div>
|
||||
|
||||
<h2 id="ueberblick">2. Datenschutz auf einen Blick</h2>
|
||||
<p>Wir haben diese Website bewusst datensparsam gebaut:</p>
|
||||
<ul>
|
||||
<li>Wir setzen <strong>keine Cookies</strong> und verwenden <strong>keine Analyse- oder Tracking-Werkzeuge</strong>.</li>
|
||||
<li>Schriften, Skripte und Grafiken werden <strong>lokal von unserem Server</strong> geladen – es werden beim Seitenaufruf keine Daten an Drittanbieter (z. B. Google Fonts oder Content-Delivery-Netzwerke) übertragen.</li>
|
||||
<li>Personenbezogene Daten verarbeiten wir nur, wenn Sie uns aktiv kontaktieren.</li>
|
||||
</ul>
|
||||
|
||||
<h2 id="hosting">3. Hosting und Server-Logfiles</h2>
|
||||
<p>Diese Website wird bei einem externen Dienstleister gehostet <!-- TODO: Name und Anschrift des Hosting-Anbieters ergänzen -->. Beim Aufruf der Website werden durch den Webserver automatisch Informationen erfasst, die Ihr Browser übermittelt (sogenannte Server-Logfiles):</p>
|
||||
<ul>
|
||||
<li>IP-Adresse des anfragenden Geräts</li>
|
||||
<li>Datum und Uhrzeit des Zugriffs</li>
|
||||
<li>aufgerufene Seite bzw. Datei und übertragene Datenmenge</li>
|
||||
<li>Referrer-URL (zuvor besuchte Seite)</li>
|
||||
<li>verwendeter Browser und Betriebssystem</li>
|
||||
</ul>
|
||||
<p>Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der technisch fehlerfreien und sicheren Bereitstellung der Website. Die Logfiles werden gelöscht, sobald sie für diesen Zweck nicht mehr erforderlich sind, sofern sie nicht zur Aufklärung von Sicherheitsvorfällen benötigt werden. <!-- TODO: konkrete Speicherdauer des Hosters ergänzen und Vertrag zur Auftragsverarbeitung (Art. 28 DSGVO) mit dem Hoster bestätigen. --></p>
|
||||
|
||||
<h2 id="cookies">4. Cookies, Tracking und lokale Speicherung</h2>
|
||||
<p>Diese Website verwendet keine Cookies, keine Analyse-Dienste, keine Social-Media-Plugins und keine Werbe-Tracker. Es werden auch keine Informationen im lokalen Speicher Ihres Browsers abgelegt. Die Animationen und das „Wärmebild“ werden vollständig in Ihrem Browser berechnet.</p>
|
||||
|
||||
<h2 id="kontakt">5. Kontaktaufnahme per Telefon, E-Mail und Kontaktformular</h2>
|
||||
<p>Wenn Sie uns kontaktieren, verarbeiten wir die von Ihnen gemachten Angaben (z. B. Name, E-Mail-Adresse, Telefonnummer, Ort sowie den Inhalt Ihrer Nachricht), um Ihre Anfrage zu bearbeiten und Ihnen zu antworten.</p>
|
||||
<p>Die Angaben aus dem Kontaktformular werden über unseren Webserver als E-Mail an uns übermittelt. Ist dies technisch nicht möglich, öffnet sich stattdessen Ihr eigenes E-Mail-Programm mit einer vorbereiteten Nachricht; der Versand erfolgt dann über Ihren E-Mail-Anbieter.</p>
|
||||
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, sofern Ihre Anfrage mit der Erfüllung eines Vertrags oder der Durchführung vorvertraglicher Maßnahmen (z. B. einem Angebot) zusammenhängt, im Übrigen Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an der Beantwortung von Anfragen). Wir löschen Ihre Daten, sobald sie für die Bearbeitung nicht mehr erforderlich sind; gesetzliche Aufbewahrungspflichten (insbesondere nach Handels- und Steuerrecht) bleiben unberührt.</p>
|
||||
|
||||
<h2 id="finder">6. Heizungs-Finder</h2>
|
||||
<p>Der Heizungs-Finder läuft vollständig in Ihrem Browser. Ihre Antworten werden weder gespeichert noch an uns übertragen. Nur wenn Sie das Ergebnis über die Schaltfläche „Ergebnis mit uns besprechen“ in das Kontaktformular übernehmen und dieses absenden, erhalten wir die Angaben im Rahmen Ihrer Anfrage (siehe Abschnitt 5).</p>
|
||||
|
||||
<h2 id="bewerbung">7. Bewerbungen</h2>
|
||||
<p>Bewerbungsunterlagen, die Sie uns per E-Mail senden, verarbeiten wir zur Durchführung des Bewerbungsverfahrens auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO. Kommt kein Arbeits- oder Ausbildungsverhältnis zustande, löschen wir Ihre Unterlagen spätestens sechs Monate nach Abschluss des Verfahrens, sofern Sie nicht in eine längere Speicherung eingewilligt haben.</p>
|
||||
|
||||
<h2 id="links">8. Externe Links</h2>
|
||||
<p>Für die Routenplanung verlinken wir auf Google Maps. Erst wenn Sie den Link anklicken, werden Sie auf die Seite der Google Ireland Limited weitergeleitet; dort gelten deren Datenschutzbestimmungen. Beim bloßen Besuch unserer Website werden keine Daten an Google übertragen.</p>
|
||||
|
||||
<h2 id="rechte">9. Ihre Rechte</h2>
|
||||
<p>Sie haben jederzeit das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16 DSGVO), Löschung (Art. 17 DSGVO), Einschränkung der Verarbeitung (Art. 18 DSGVO), Datenübertragbarkeit (Art. 20 DSGVO) sowie das Recht, einer Verarbeitung auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO zu widersprechen (Art. 21 DSGVO). Eine erteilte Einwilligung können Sie jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3 DSGVO). Wenden Sie sich dazu einfach an die oben genannten Kontaktdaten.</p>
|
||||
<p>Außerdem haben Sie das Recht, sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Für uns zuständig ist das Bayerische Landesamt für Datenschutzaufsicht (BayLDA), Promenade 18, 91522 Ansbach.</p>
|
||||
|
||||
<h2>10. Verschlüsselung</h2>
|
||||
<p>Diese Website nutzt aus Sicherheitsgründen eine SSL- bzw. TLS-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie am Schloss-Symbol in der Adresszeile Ihres Browsers und daran, dass die Adresse mit „https://“ beginnt.</p>
|
||||
|
||||
<p><small>Stand: Oktober 2026</small></p>
|
||||
<p><a class="link-arrow" href="index.html"><svg class="icon" aria-hidden="true"><use href="#i-arrow-left"/></svg> Zurück zur Startseite</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer sheet sheet--dark">
|
||||
<div class="container">
|
||||
<div class="footer__grid">
|
||||
<div class="footer__col">
|
||||
<h2 class="footer__h">Olzinger GmbH</h2>
|
||||
<address>Ahornstraße 112<br>84030 Ergolding</address>
|
||||
</div>
|
||||
<div class="footer__col">
|
||||
<h2 class="footer__h">Kontakt</h2>
|
||||
<p><a href="tel:+4987172617">Tel. 0871 72617</a><br>Fax 0871 74963<br><a href="mailto:kontakt@olzinger.de">kontakt@olzinger.de</a></p>
|
||||
</div>
|
||||
<div class="footer__col">
|
||||
<h2 class="footer__h">Öffnungszeiten</h2>
|
||||
<p>Mo – Do 7:00 – 12:00<br>und 13:00 – 16:30<br>Fr 7:00 – 11:30</p>
|
||||
</div>
|
||||
<nav class="footer__col" aria-label="Footer-Navigation">
|
||||
<h2 class="footer__h">Seite</h2>
|
||||
<ul>
|
||||
<li><a href="index.html#leistungen">Leistungen</a></li>
|
||||
<li><a href="index.html#energie-haus">Energie-Haus</a></li>
|
||||
<li><a href="index.html#heizungs-finder">Heizungs-Finder</a></li>
|
||||
<li><a href="index.html#ueber-uns">Über uns</a></li>
|
||||
<li><a href="index.html#kontakt">Kontakt</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="footer__word" aria-hidden="true" data-footer-word><span>Olzinger</span></div>
|
||||
<div class="footer__bottom">
|
||||
<p>© <span data-current-year>2026</span> Olzinger GmbH · Heizung · Sanitär · Lüftung · Regenerative Energien</p>
|
||||
<ul>
|
||||
<li><a href="impressum.html">Impressum</a></li>
|
||||
<li><a href="datenschutz.html" aria-current="page">Datenschutz</a></li>
|
||||
<li><a class="footer__top" href="#main">Nach oben <svg class="icon" aria-hidden="true"><use href="#i-arrow-up"/></svg></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
<!doctype html>
|
||||
<html lang="de" class="no-js">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Impressum – Olzinger Haustechnik</title>
|
||||
<meta name="description" content="Impressum der Olzinger GmbH – Haustechnik in Ergolding bei Landshut.">
|
||||
<meta name="robots" content="noindex, follow">
|
||||
<link rel="canonical" href="https://www.olzinger.de/impressum.html">
|
||||
<meta name="theme-color" content="#0B151E">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="icon" href="assets/img/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="assets/img/favicon-32.png" sizes="32x32" type="image/png">
|
||||
<link rel="apple-touch-icon" href="assets/img/apple-touch-icon.png">
|
||||
<link rel="preload" href="assets/fonts/instrument-sans-var.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="assets/css/main.css">
|
||||
<script>
|
||||
(function (d, w) {
|
||||
var h = d.documentElement;
|
||||
h.className = h.className.replace('no-js', 'js');
|
||||
w.setTimeout(function () { if (!w.__olzReady) { h.classList.remove('js'); h.classList.add('no-js'); } }, 4500);
|
||||
})(document, window);
|
||||
</script>
|
||||
<script src="assets/vendor/gsap.min.js" defer></script>
|
||||
<script src="assets/vendor/ScrollTrigger.min.js" defer></script>
|
||||
<script src="assets/vendor/SplitText.min.js" defer></script>
|
||||
<script src="assets/vendor/lenis.min.js" defer></script>
|
||||
<script src="assets/js/contact.js" defer></script>
|
||||
<script src="assets/js/main.js" defer></script>
|
||||
</head>
|
||||
<body class="page">
|
||||
<!--
|
||||
HINWEIS: Bitte vor dem Livegang prüfen und ergänzen.
|
||||
Die Angaben wurden aus öffentlich zugänglichen Quellen (u. a. dem bisherigen Impressum
|
||||
auf olzinger.de) übernommen. Mit „TODO“ markierte Punkte bitte aus dem bestehenden
|
||||
Impressum übernehmen bzw. bestätigen.
|
||||
-->
|
||||
<a class="skip-link" href="#main">Zum Inhalt springen</a>
|
||||
|
||||
<svg class="sprite" aria-hidden="true" focusable="false">
|
||||
<symbol id="i-phone" viewBox="0 0 24 24"><path d="M6.5 3.5h3l1.6 4.4-2.1 1.3a11.5 11.5 0 0 0 5.8 5.8l1.3-2.1 4.4 1.6v3a2 2 0 0 1-2.2 2A17 17 0 0 1 4.5 5.7a2 2 0 0 1 2-2.2z"/></symbol>
|
||||
<symbol id="i-mail" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2.5"/><path d="m4 7 8 6 8-6"/></symbol>
|
||||
<symbol id="i-arrow" viewBox="0 0 24 24"><path d="M4 12h15M13 6l6 6-6 6"/></symbol>
|
||||
<symbol id="i-arrow-up" viewBox="0 0 24 24"><path d="M12 20V5M6 11l6-6 6 6"/></symbol>
|
||||
<symbol id="i-arrow-left" viewBox="0 0 24 24"><path d="M20 12H5M11 6l-6 6 6 6"/></symbol>
|
||||
</svg>
|
||||
|
||||
<header class="nav" data-nav>
|
||||
<div class="nav__bar">
|
||||
<a class="logo" href="index.html" aria-label="Olzinger Haustechnik – zur Startseite">
|
||||
<svg class="logo__mark" viewBox="0 0 40 40" aria-hidden="true">
|
||||
<defs><linearGradient id="g-logo" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#2E9BEF"/><stop offset=".55" stop-color="#C2456B"/><stop offset="1" stop-color="#FF6A2B"/></linearGradient></defs>
|
||||
<path d="M11.97 31.47A14 14 0 1 1 28.03 31.47" fill="none" stroke="url(#g-logo)" stroke-width="5.5" stroke-linecap="round"/>
|
||||
<circle cx="20" cy="20" r="3.4" fill="currentColor"/>
|
||||
</svg>
|
||||
<span class="logo__text"><span class="logo__word">Olzinger</span><span class="logo__sub">Haustechnik</span></span>
|
||||
</a>
|
||||
<nav class="nav__links" aria-label="Hauptnavigation">
|
||||
<a href="index.html#leistungen">Leistungen</a>
|
||||
<a href="index.html#energie-haus">Energie-Haus</a>
|
||||
<a href="index.html#heizungs-finder">Heizungs-Finder</a>
|
||||
<a href="index.html#ueber-uns">Über uns</a>
|
||||
<a href="index.html#karriere">Karriere</a>
|
||||
</nav>
|
||||
<div class="nav__actions">
|
||||
<a class="nav__phone" href="tel:+4987172617"><svg class="icon" aria-hidden="true"><use href="#i-phone"/></svg><span>0871 72617</span></a>
|
||||
<a class="btn btn--sm btn--warm" href="index.html#kontakt"><span class="btn__label">Anfrage starten</span></a>
|
||||
<button class="nav__toggle" type="button" aria-expanded="false" aria-controls="menu" data-menu-toggle>
|
||||
<span class="nav__toggle-lines" aria-hidden="true"><i></i><i></i></span>
|
||||
<span class="sr-only">Menü öffnen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="menu" id="menu" data-menu hidden>
|
||||
<div class="menu__glow" aria-hidden="true"></div>
|
||||
<nav class="menu__links" aria-label="Mobile Navigation">
|
||||
<a href="index.html#leistungen"><span>01</span>Leistungen</a>
|
||||
<a href="index.html#energie-haus"><span>02</span>Energie-Haus</a>
|
||||
<a href="index.html#heizungs-finder"><span>03</span>Heizungs-Finder</a>
|
||||
<a href="index.html#ablauf"><span>04</span>Ablauf</a>
|
||||
<a href="index.html#ueber-uns"><span>05</span>Über uns</a>
|
||||
<a href="index.html#karriere"><span>06</span>Karriere</a>
|
||||
<a href="index.html#kontakt"><span>07</span>Kontakt</a>
|
||||
</nav>
|
||||
<div class="menu__foot">
|
||||
<a href="tel:+4987172617"><svg class="icon" aria-hidden="true"><use href="#i-phone"/></svg>0871 72617</a>
|
||||
<a href="mailto:kontakt@olzinger.de"><svg class="icon" aria-hidden="true"><use href="#i-mail"/></svg>kontakt@olzinger.de</a>
|
||||
<p class="status status--dark" data-status><span class="status__dot" aria-hidden="true"></span><span class="status__text">Mo–Do 7–16:30 Uhr · Fr 7–11:30 Uhr</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
<section class="page-hero" aria-labelledby="page-title">
|
||||
<div class="container">
|
||||
<p class="label"><a href="index.html">Startseite</a> / Rechtliches</p>
|
||||
<h1 class="h1" id="page-title" data-split>Impressum</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="legal sheet">
|
||||
<div class="container legal__grid">
|
||||
<nav class="legal__toc" aria-label="Inhalt">
|
||||
<a href="#anbieter">Anbieter</a>
|
||||
<a href="#kontakt">Kontakt</a>
|
||||
<a href="#register">Registereintrag</a>
|
||||
<a href="#streitbeilegung">Streitbeilegung</a>
|
||||
<a href="#haftung">Haftung</a>
|
||||
<a href="#urheberrecht">Urheberrecht</a>
|
||||
</nav>
|
||||
|
||||
<div class="legal__body" data-reveal>
|
||||
<h2 id="anbieter">Angaben gemäß § 5 DDG</h2>
|
||||
<div class="legal__card">
|
||||
<p><strong>Olzinger GmbH</strong></p>
|
||||
<p>Ahornstraße 112<br>84030 Ergolding</p>
|
||||
</div>
|
||||
<h3>Vertreten durch die Geschäftsführer</h3>
|
||||
<p>Franz Ramsauer<br>Robert Straub</p>
|
||||
|
||||
<h2 id="kontakt">Kontakt</h2>
|
||||
<p>
|
||||
Telefon: <a href="tel:+4987172617">0871 72617</a><br>
|
||||
Telefax: 0871 74963<br>
|
||||
E-Mail: <a href="mailto:kontakt@olzinger.de">kontakt@olzinger.de</a>
|
||||
</p>
|
||||
|
||||
<h2 id="register">Registereintrag</h2>
|
||||
<p>
|
||||
Eintragung im Handelsregister<br>
|
||||
Registergericht: Amtsgericht Landshut<br>
|
||||
Registernummer: HRB 3667
|
||||
</p>
|
||||
<!-- TODO: Umsatzsteuer-Identifikationsnummer gemäß § 27a UStG aus dem bisherigen Impressum ergänzen:
|
||||
<h3>Umsatzsteuer-ID</h3>
|
||||
<p>Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz: DE XXX XXX XXX</p>
|
||||
-->
|
||||
<!-- TODO: Berufsrechtliche Angaben bestätigen/ergänzen, z. B.:
|
||||
<h3>Berufsbezeichnung und berufsrechtliche Regelungen</h3>
|
||||
<p>Berufsbezeichnung: Installateur- und Heizungsbauer (verliehen in Deutschland)<br>
|
||||
Zuständige Kammer: Handwerkskammer Niederbayern-Oberpfalz<br>
|
||||
Es gilt die Handwerksordnung (HwO), einsehbar unter www.gesetze-im-internet.de</p>
|
||||
-->
|
||||
|
||||
<h2 id="streitbeilegung">Verbraucherstreitbeilegung</h2>
|
||||
<p>Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</p>
|
||||
|
||||
<h2 id="haftung">Haftung für Inhalte und Links</h2>
|
||||
<p>Die Inhalte dieser Website wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir jedoch keine Gewähr übernehmen. Die Angaben im Heizungs-Finder dienen ausschließlich der ersten Orientierung und ersetzen keine individuelle Beratung und Planung vor Ort.</p>
|
||||
<p>Unsere Website enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber verantwortlich. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.</p>
|
||||
|
||||
<h2 id="urheberrecht">Urheberrecht</h2>
|
||||
<p>Die durch uns erstellten Inhalte und Werke auf dieser Website unterliegen dem deutschen Urheberrecht. Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechts bedürfen der schriftlichen Zustimmung der Olzinger GmbH.</p>
|
||||
<p>Illustrationen und Grafiken: individuell für diese Website gestaltet. Schriften: Instrument Sans und Instrument Serif (SIL Open Font License), lokal eingebunden.</p>
|
||||
|
||||
<p><a class="link-arrow" href="index.html"><svg class="icon" aria-hidden="true"><use href="#i-arrow-left"/></svg> Zurück zur Startseite</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer sheet sheet--dark">
|
||||
<div class="container">
|
||||
<div class="footer__grid">
|
||||
<div class="footer__col">
|
||||
<h2 class="footer__h">Olzinger GmbH</h2>
|
||||
<address>Ahornstraße 112<br>84030 Ergolding</address>
|
||||
</div>
|
||||
<div class="footer__col">
|
||||
<h2 class="footer__h">Kontakt</h2>
|
||||
<p><a href="tel:+4987172617">Tel. 0871 72617</a><br>Fax 0871 74963<br><a href="mailto:kontakt@olzinger.de">kontakt@olzinger.de</a></p>
|
||||
</div>
|
||||
<div class="footer__col">
|
||||
<h2 class="footer__h">Öffnungszeiten</h2>
|
||||
<p>Mo – Do 7:00 – 12:00<br>und 13:00 – 16:30<br>Fr 7:00 – 11:30</p>
|
||||
</div>
|
||||
<nav class="footer__col" aria-label="Footer-Navigation">
|
||||
<h2 class="footer__h">Seite</h2>
|
||||
<ul>
|
||||
<li><a href="index.html#leistungen">Leistungen</a></li>
|
||||
<li><a href="index.html#energie-haus">Energie-Haus</a></li>
|
||||
<li><a href="index.html#heizungs-finder">Heizungs-Finder</a></li>
|
||||
<li><a href="index.html#ueber-uns">Über uns</a></li>
|
||||
<li><a href="index.html#kontakt">Kontakt</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="footer__word" aria-hidden="true" data-footer-word><span>Olzinger</span></div>
|
||||
<div class="footer__bottom">
|
||||
<p>© <span data-current-year>2026</span> Olzinger GmbH · Heizung · Sanitär · Lüftung · Regenerative Energien</p>
|
||||
<ul>
|
||||
<li><a href="impressum.html" aria-current="page">Impressum</a></li>
|
||||
<li><a href="datenschutz.html">Datenschutz</a></li>
|
||||
<li><a class="footer__top" href="#main">Nach oben <svg class="icon" aria-hidden="true"><use href="#i-arrow-up"/></svg></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
+1315
File diff suppressed because it is too large.
Load diff
+117
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* Olzinger Haustechnik – Versand des Kontaktformulars
|
||||
*
|
||||
* Wird per fetch() aus assets/js/contact.js aufgerufen und antwortet mit JSON.
|
||||
* Ist PHP auf dem Server nicht verfügbar, öffnet das Formular automatisch das
|
||||
* E-Mail-Programm des Besuchers (Fallback in contact.js).
|
||||
*
|
||||
* Schutzmaßnahmen: Honeypot-Feld, Mindest-Ausfüllzeit, Längenbegrenzung,
|
||||
* Validierung der E-Mail-Adresse (verhindert Header-Injection über Reply-To),
|
||||
* kodierter Betreff und feste Empfängeradresse.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
const RECIPIENT = 'kontakt@olzinger.de';
|
||||
// Absender sollte eine Adresse der eigenen Domain sein (SPF/DKIM), sonst landen Mails im Spam.
|
||||
const SENDER = 'kontakt@olzinger.de';
|
||||
const MIN_SECONDS = 3;
|
||||
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
function respond(int $status, array $payload): void
|
||||
{
|
||||
http_response_code($status);
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function field(string $key, int $max): string
|
||||
{
|
||||
$value = isset($_POST[$key]) && is_string($_POST[$key]) ? trim($_POST[$key]) : '';
|
||||
$value = str_replace(["\r\n", "\r"], "\n", $value);
|
||||
// Steuerzeichen außer Zeilenumbruch und Tab entfernen
|
||||
$value = preg_replace('/[^\P{C}\n\t]/u', '', $value) ?? '';
|
||||
return function_exists('mb_substr') ? mb_substr($value, 0, $max, 'UTF-8') : substr($value, 0, $max);
|
||||
}
|
||||
|
||||
function single_line(string $value): string
|
||||
{
|
||||
return trim(preg_replace('/\s+/u', ' ', $value) ?? '');
|
||||
}
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
header('Allow: POST');
|
||||
respond(405, ['ok' => false, 'error' => 'method_not_allowed']);
|
||||
}
|
||||
|
||||
// Honeypot: echte Besucher sehen dieses Feld nicht.
|
||||
if (field('website', 200) !== '') {
|
||||
respond(200, ['ok' => true]);
|
||||
}
|
||||
|
||||
// Zeitfalle: Bots senden Formulare meist sofort ab.
|
||||
$started = (int) ($_POST['ts'] ?? 0);
|
||||
if ($started > 0 && (microtime(true) * 1000 - $started) < MIN_SECONDS * 1000) {
|
||||
respond(200, ['ok' => true]);
|
||||
}
|
||||
|
||||
$name = single_line(field('name', 120));
|
||||
$email = single_line(field('email', 200));
|
||||
$phone = single_line(field('phone', 60));
|
||||
$place = single_line(field('place', 120));
|
||||
$topic = single_line(field('topic', 120));
|
||||
$message = field('message', 5000);
|
||||
$privacy = !empty($_POST['privacy']);
|
||||
|
||||
$errors = [];
|
||||
if ($name === '') {
|
||||
$errors[] = 'name';
|
||||
}
|
||||
if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
|
||||
$errors[] = 'email';
|
||||
}
|
||||
if ((function_exists('mb_strlen') ? mb_strlen($message, 'UTF-8') : strlen($message)) < 5) {
|
||||
$errors[] = 'message';
|
||||
}
|
||||
if (!$privacy) {
|
||||
$errors[] = 'privacy';
|
||||
}
|
||||
if ($errors) {
|
||||
respond(422, ['ok' => false, 'errors' => $errors]);
|
||||
}
|
||||
|
||||
$subject = 'Anfrage über olzinger.de' . ($topic !== '' ? ' – ' . $topic : '');
|
||||
$body = implode("\n", [
|
||||
'Neue Anfrage über das Kontaktformular auf olzinger.de',
|
||||
'',
|
||||
'Anliegen: ' . ($topic !== '' ? $topic : '–'),
|
||||
'Name: ' . $name,
|
||||
'E-Mail: ' . $email,
|
||||
'Telefon: ' . ($phone !== '' ? $phone : '–'),
|
||||
'PLZ / Ort: ' . ($place !== '' ? $place : '–'),
|
||||
'',
|
||||
'Nachricht:',
|
||||
$message,
|
||||
'',
|
||||
'—',
|
||||
'Gesendet am ' . date('d.m.Y') . ' um ' . date('H:i') . ' Uhr',
|
||||
]);
|
||||
|
||||
$headers = implode("\r\n", [
|
||||
'From: Website Olzinger <' . SENDER . '>',
|
||||
'Reply-To: ' . $email,
|
||||
'MIME-Version: 1.0',
|
||||
'Content-Type: text/plain; charset=UTF-8',
|
||||
'Content-Transfer-Encoding: 8bit',
|
||||
'X-Mailer: olzinger.de',
|
||||
]);
|
||||
|
||||
$encodedSubject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
|
||||
$sent = @mail(RECIPIENT, $encodedSubject, $body, $headers, '-f' . SENDER);
|
||||
|
||||
respond($sent ? 200 : 500, ['ok' => (bool) $sent]);
|
||||
@@ -0,0 +1,4 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://www.olzinger.de/sitemap.xml
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Olzinger Haustechnik",
|
||||
"short_name": "Olzinger",
|
||||
"icons": [
|
||||
{ "src": "assets/img/favicon.svg", "sizes": "any", "type": "image/svg+xml" },
|
||||
{ "src": "assets/img/apple-touch-icon.png", "sizes": "180x180", "type": "image/png" }
|
||||
],
|
||||
"theme_color": "#0B151E",
|
||||
"background_color": "#F3EFE8",
|
||||
"display": "browser",
|
||||
"lang": "de"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://www.olzinger.de/</loc>
|
||||
<lastmod>2026-10-03</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.olzinger.de/impressum.html</loc>
|
||||
<lastmod>2026-10-03</lastmod>
|
||||
<priority>0.2</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://www.olzinger.de/datenschutz.html</loc>
|
||||
<lastmod>2026-10-03</lastmod>
|
||||
<priority>0.2</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>OG-Vorlage</title>
|
||||
<link rel="stylesheet" href="../assets/css/main.css">
|
||||
<style>
|
||||
html, body { margin: 0; width: 1200px; height: 630px; overflow: hidden; background: #0B151E; }
|
||||
.og { position: relative; width: 1200px; height: 630px; color: #F3EFE8; isolation: isolate; overflow: hidden; }
|
||||
.og canvas { position: absolute; inset: 0; width: 100%; height: 100%; z-index: -2; }
|
||||
.og::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(90deg, rgba(5,11,18,.7), rgba(5,11,18,.2) 55%, rgba(5,11,18,0)); }
|
||||
.og__logo { position: absolute; left: 64px; top: 56px; }
|
||||
.og__title { position: absolute; left: 60px; bottom: 120px; margin: 0; font-size: 132px; line-height: .86; font-weight: 600; font-stretch: 78%; letter-spacing: -.052em; }
|
||||
.og__title span { display: block; }
|
||||
.og__title i { display: inline-block; width: .15em; height: .15em; margin-left: .045em; border-radius: 50%; }
|
||||
.og__sub { position: absolute; left: 64px; bottom: 58px; margin: 0; font-size: 26px; font-weight: 500; color: rgba(243,239,232,.86); }
|
||||
.og__url { position: absolute; right: 64px; bottom: 58px; margin: 0; font-family: var(--font-mono); font-size: 18px; color: rgba(243,239,232,.7); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="og" id="og">
|
||||
<canvas data-thermal></canvas>
|
||||
<a class="logo og__logo" style="color:#F3EFE8">
|
||||
<svg class="logo__mark" viewBox="0 0 40 40" style="width:52px;height:52px">
|
||||
<defs><linearGradient id="g-logo" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#2E9BEF"/><stop offset=".55" stop-color="#C2456B"/><stop offset="1" stop-color="#FF6A2B"/></linearGradient></defs>
|
||||
<path d="M11.97 31.47A14 14 0 1 1 28.03 31.47" fill="none" stroke="url(#g-logo)" stroke-width="5.5" stroke-linecap="round"/>
|
||||
<circle cx="20" cy="20" r="3.4" fill="currentColor"/>
|
||||
</svg>
|
||||
<span class="logo__text"><span class="logo__word" style="font-size:28px">Olzinger</span><span class="logo__sub" style="font-size:14px">Haustechnik</span></span>
|
||||
</a>
|
||||
<h1 class="og__title">
|
||||
<span>Wärme<i style="background:#FF6A2B;box-shadow:0 0 .28em rgba(255,106,43,.75)"></i></span>
|
||||
<span>Wasser<i style="background:#2E9BEF;box-shadow:0 0 .28em rgba(46,155,239,.75)"></i></span>
|
||||
<span>Luft<i style="background:#3CCFC0;box-shadow:0 0 .28em rgba(60,207,192,.7)"></i></span>
|
||||
</h1>
|
||||
<p class="og__sub">Heizung · Sanitär · Lüftung · Regenerativ – Ergolding bei Landshut, seit 1985</p>
|
||||
<p class="og__url">olzinger.de</p>
|
||||
</div>
|
||||
<script src="../assets/js/thermal.js"></script>
|
||||
<script>
|
||||
var t = OlzThermal.init(document.querySelector('[data-thermal]'), { hero: document.getElementById('og'), reduceMotion: false });
|
||||
if (t) t.heatUp();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user