This repository has been archived on 2026-07-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
lackwerk-kl/Lackwerk Hero-print-105nrtf.dc.html
2026-07-12 20:11:39 +02:00

1079 lines
69 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html>
<head>
<style data-omelette-print-defaults>
@media print {
@page { margin: 0.5in; }
h1, h2, h3, h4, h5, h6 { break-after: avoid; }
figure, table, pre, blockquote, img, svg, tr, [class~="card" i] { break-inside: avoid; }
img, svg { max-width: 100%; }
p { orphans: 3; widows: 3; }
}
</style>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@400;500;600;700;800;900&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;0,8..60,700;1,8..60,500;1,8..60,600&display=swap" rel="stylesheet">
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet">
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0d0f12; font-family: 'Archivo', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
a { color: #f0f0ec; text-decoration: none; }
a:hover { color: var(--accent, #F0912F); }
@keyframes lw-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
@keyframes lw-fadein { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 1080px) {
.lw-review { display: none !important; }
}
@media (max-width: 860px) {
.lw-navlinks { display: none !important; }
.lw-burger { display: inline-flex !important; }
.lw-benefits { grid-template-columns: repeat(2, 1fr) !important; row-gap: 26px !important; }
}
@media (max-width: 520px) {
.lw-benefits { grid-template-columns: 1fr !important; }
}
@media (max-width: 980px) {
.lw-services { grid-template-columns: repeat(2, 1fr) !important; }
}
@media (max-width: 620px) {
.lw-services { grid-template-columns: 1fr !important; }
}
@media (max-width: 980px) {
.lw-refs { grid-template-columns: repeat(2, 1fr) !important; grid-auto-rows: 200px !important; }
}
@media (max-width: 620px) {
.lw-refs { grid-template-columns: 1fr !important; grid-auto-rows: 220px !important; }
.lw-ref-wide { grid-column: auto !important; }
}
@media (max-width: 860px) {
.lw-about { grid-template-columns: 1fr !important; }
.lw-contact { grid-template-columns: 1fr !important; }
.lw-footer-cols { grid-template-columns: repeat(2, 1fr) !important; }
.lw-footer-bottom { flex-direction: column !important; align-items: flex-start !important; }
.lw-footer-bottom > div:last-child { text-align: left !important; }
.lw-footer-bottom > div:last-child > div { justify-content: flex-start !important; }
}
</style>
</helmet>
<div ref="{{ rootRef }}" style="width: 100%; --accent: #F0912F; background: #0d0f12;">
<!-- HERO -->
<div style="position: relative; min-height: 100vh; overflow: hidden;">
<!-- BACKGROUND -->
<div style="position: absolute; inset: 0; z-index: 0; background: radial-gradient(120% 90% at 78% 12%, #1c2530 0%, #12161b 42%, #0a0c0f 100%);"></div>
<div style="position: absolute; inset: 0; z-index: 1; filter: hue-rotate(85deg) saturate(1.15);">
<x-import component-from-global-scope="image-slot" from="./assets/image-slot.js" id="hero-wassertropfen" shape="rect" fit="cover" src="https://lack-box.de/wp-content/uploads/2020/07/startseite-01-tankklappe-squo-best.jpg" placeholder="Hintergrundbild hier ablegen" hint-size="100%,100%" style="display:block;width:100%;height:100%;"></x-import>
</div>
<!-- blue tint -->
<div style="position: absolute; inset: 0; z-index: 2; pointer-events: none; background: rgba(18,60,120,0.32); mix-blend-mode: multiply;"></div>
<!-- legibility overlay (click-through) -->
<div style="position: absolute; inset: 0; z-index: 2; pointer-events: none; background: linear-gradient(180deg, rgba(9,11,14,0.82) 0%, rgba(9,11,14,0.34) 30%, rgba(9,11,14,0.30) 55%, rgba(9,11,14,0.86) 100%);"></div>
<div style="position: absolute; inset: 0; z-index: 2; pointer-events: none; background: radial-gradient(90% 70% at 22% 42%, rgba(9,11,14,0.55) 0%, rgba(9,11,14,0) 60%);"></div>
<!-- thin accent glow top -->
<div style="position: absolute; top: 0; left: 0; right: 0; height: 3px; z-index: 4; background: linear-gradient(90deg, transparent, var(--accent, #F0912F), transparent); opacity: 0.5;"></div>
<!-- NAV -->
<nav ref="{{ navRef }}" style="position: fixed; top: 0; left: 0; right: 0; z-index: 40; transition: background .35s ease, backdrop-filter .35s ease, border-color .35s ease, padding .35s ease; padding: 22px clamp(20px, 4vw, 56px); border-bottom: 1px solid transparent;">
<div style="max-width: 1320px; margin: 0 auto; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 24px;">
<!-- left links -->
<div class="lw-navlinks" style="display: flex; gap: 34px; align-items: center;">
<a href="#services" style="font-size: 15px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase;">Services</a>
<a href="#referenzen" style="font-size: 15px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase;">Referenzen</a>
</div>
<!-- logo -->
<a href="#top" style="display: flex; align-items: center; justify-self: center;">
<img src="assets/logo-lackwerk.png" alt="Lackwerk" style="height: 26px; width: auto; display: block;">
</a>
<!-- right links -->
<div class="lw-navlinks" style="display: flex; gap: 34px; align-items: center; justify-content: flex-end;">
<a href="#ueber-uns" style="font-size: 15px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase;">Über uns</a>
<a href="#kontakt" style="font-size: 15px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase;">Kontakt</a>
</div>
<!-- mobile menu button -->
<button class="lw-burger" aria-label="Menü" style="display: none; grid-column: 3; justify-self: end; background: transparent; border: 1.5px solid rgba(255,255,255,.28); border-radius: 10px; padding: 9px 11px; cursor: pointer; color: #f4f3ef; align-items: center; gap: 8px;">
<span style="display: block; width: 18px; height: 2px; background: currentColor; box-shadow: 0 6px 0 currentColor, 0 -6px 0 currentColor;"></span>
</button>
</div>
</nav>
<!-- HERO CONTENT -->
<div style="position: relative; z-index: 10; display: flex; flex-direction: column; min-height: 100vh; padding: clamp(120px, 16vh, 190px) clamp(20px, 4vw, 56px) 0;">
<div style="max-width: 1320px; margin: 0 auto; width: 100%; display: flex; flex-direction: column; flex: 1;">
<!-- spacer -->
<div style="flex: 1; min-height: 40px;"></div>
<!-- headline block -->
<div style="max-width: 720px; margin: 0 auto; text-align: center; animation: lw-fadein .7s ease both;">
<h1 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; line-height: 1.08; letter-spacing: -0.005em; color: #f5f4f0; font-size: clamp(36px, 4.8vw, 64px); margin-bottom: 22px;">
<span style="display: block;">Rein mit
<span ref="{{ wordSlotRef }}" style="display: inline-block; position: relative; overflow: hidden; vertical-align: bottom; text-align: left;">
<span ref="{{ wordRef }}" style="display: inline-block; color: var(--accent, #F0912F); font-style: italic; will-change: transform, opacity;">Kratzer</span>
</span>
</span>
<span style="display: block;">Raus wie neu.</span>
</h1>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.5; color: #c5c9ce; max-width: 560px; margin: 0 auto; font-weight: 500;">
Wir reparieren alles! Mit Originalteilen und neuester Technik zu fairen Preisen. Als wäre nichts passiert.
</p>
</div>
<!-- spacer -->
<div style="flex: 1; min-height: 40px;"></div>
<!-- BENEFIT STRIP -->
<div class="lw-benefits" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(18px, 3vw, 48px); padding: 28px 0 46px;">
<div style="display: flex; flex-direction: column; justify-content: flex-end; gap: 12px;">
<p style="font-size: 15.5px; line-height: 1.4; color: #e7e9ec; font-weight: 600;">Fahrzeug-Reparatur wie in der Vertragswerkstatt — nur günstiger</p>
</div>
<div style="display: flex; flex-direction: column; justify-content: flex-end; gap: 12px;">
<img src="assets/benefit01.svg" alt="" width="22" height="27" style="opacity: .95;">
<p style="font-size: 15.5px; line-height: 1.4; color: #e7e9ec; font-weight: 600;">Wir nutzen Premium-Lacke</p>
</div>
<div style="display: flex; flex-direction: column; justify-content: flex-end; gap: 12px;">
<img src="assets/benefit02.svg" alt="" width="30" height="24" style="opacity: .95;">
<p style="font-size: 15.5px; line-height: 1.4; color: #e7e9ec; font-weight: 600;">Wir verbauen nur Originalteile</p>
</div>
<div style="display: flex; flex-direction: column; gap: 14px;">
<!-- Google badge -->
<a href="{{ googleUrl }}" target="_blank" rel="noopener" style="display: inline-flex; flex-direction: column; gap: 3px; align-self: flex-start; background: #000; border: 1px solid rgba(255,255,255,.12); border-radius: 44px; padding: 11px 20px; transition: transform .3s ease, box-shadow .3s ease;" style-hover="transform: scale(1.05); box-shadow: 0 6px 20px rgba(0,0,0,.4);">
<span style="color: #fff; font-weight: 700; font-size: 15px; letter-spacing: .01em;">Lackwerk</span>
<span style="display: inline-flex; align-items: center; gap: 7px;">
<span style="color: #DBDED9; font-weight: 600; font-size: 15px;">{{ ratingLabel }}</span>
<img src="assets/review.svg" alt="Bewertung" width="66" height="10">
<span style="color: #DBDED9; font-weight: 300; font-size: 15px;">{{ countLabel }}</span>
</span>
</a>
<img src="assets/benefit03.svg" alt="" width="20" height="30" style="opacity: .95;">
<p style="font-size: 15.5px; line-height: 1.4; color: #e7e9ec; font-weight: 600;">Wir arbeiten mit der gleichen Genauigkeit</p>
</div>
</div>
</div>
</div>
</div>
<!-- LEISTUNGEN SECTION -->
<section id="services" data-screen-label="Leistungen" style="position: relative; overflow: hidden; background: #0D2A46; padding: clamp(80px, 12vh, 140px) clamp(20px, 4vw, 56px);">
<svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true" style="position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;">
<path d="M-100,780 C300,620 500,930 900,760 C1180,640 1350,700 1540,560" fill="none" stroke="#164a7d" stroke-width="150" stroke-linecap="round" opacity="0.75"></path>
<path d="M-120,320 C260,480 620,120 980,300 C1240,430 1380,320 1560,400" fill="none" stroke="#123a63" stroke-width="190" stroke-linecap="round" opacity="0.8"></path>
<path d="M-80,90 C380,220 760,-40 1120,120 C1330,215 1450,140 1560,190" fill="none" stroke="#1d5c99" stroke-width="110" stroke-linecap="round" opacity="0.55"></path>
<path d="M-100,560 C340,420 700,660 1080,500 C1300,410 1420,470 1560,410" fill="none" stroke="#0f3154" stroke-width="230" stroke-linecap="round" opacity="0.9"></path>
</svg>
<div style="position: absolute; inset: 0; background: linear-gradient(180deg, rgba(13,42,70,.45) 0%, rgba(13,42,70,.10) 40%, rgba(13,42,70,.45) 100%); pointer-events: none;"></div>
<div style="position: relative; max-width: 1320px; margin: 0 auto;">
<div style="max-width: 760px; margin-bottom: clamp(40px, 6vh, 64px);">
<span style="display: inline-block; font-size: 13px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; color: #9FD1F5; margin-bottom: 16px;">Leistungen</span>
<h2 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; line-height: 1.1; color: #f5f4f0; font-size: clamp(30px, 3.6vw, 52px); margin-bottom: 18px;">Alles rund um den Lack aus einer Hand</h2>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.55; color: #c9d6e2; font-weight: 500;">Ob Neuwagen, Alltagsauto oder Klassiker: Wir bringen jede Oberfläche zurück in Bestform. Präzise vorbereitet, sauber lackiert, perfekt finiert.</p>
</div>
<div class="lw-services" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(18px, 2.4vw, 30px);">
<div style="background: #F1F0EB; border-radius: 24px; padding: 34px 30px; display: flex; flex-direction: column; gap: 12px; box-shadow: 2px 4px 14px rgba(0,0,0,.25);">
<h3 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; font-size: 23px; color: #0D2A46;">Lackierungen aller Art</h3>
<p style="font-size: 15.5px; line-height: 1.55; color: #45535f; font-weight: 500; flex: 1;">Vom Kotflügel bis zur Komplettlackierung markenunabhängig, farbtongenau und in dauerhafter Qualität.</p>
<span style="font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #1e7fc2;">Uni · Metallic · Perleffekt</span>
</div>
<div style="background: #F1F0EB; border-radius: 24px; padding: 34px 30px; display: flex; flex-direction: column; gap: 12px; box-shadow: 2px 4px 14px rgba(0,0,0,.25);">
<h3 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; font-size: 23px; color: #0D2A46;">Smart Repair</h3>
<p style="font-size: 15.5px; line-height: 1.55; color: #45535f; font-weight: 500; flex: 1;">Kleine Kratzer, Steinschläge und Parkdellen punktgenau beseitigt schnell, günstig und ohne sichtbaren Übergang.</p>
<span style="font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #1e7fc2;">Spot-Repair · Same Day</span>
</div>
<div style="background: #F1F0EB; border-radius: 24px; padding: 34px 30px; display: flex; flex-direction: column; gap: 12px; box-shadow: 2px 4px 14px rgba(0,0,0,.25);">
<h3 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; font-size: 23px; color: #0D2A46;">Oldtimer &amp; Klassiker</h3>
<p style="font-size: 15.5px; line-height: 1.55; color: #45535f; font-weight: 500; flex: 1;">Restaurierung mit Fingerspitzengefühl: originalgetreue Farbtöne und ein Finish, das dem Wert Ihres Fahrzeugs gerecht wird.</p>
<span style="font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #1e7fc2;">Restauration · Karosserie</span>
</div>
<div style="background: #F1F0EB; border-radius: 24px; padding: 34px 30px; display: flex; flex-direction: column; gap: 12px; box-shadow: 2px 4px 14px rgba(0,0,0,.25);">
<h3 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; font-size: 23px; color: #0D2A46;">Lackaufbereitung &amp; Politur</h3>
<p style="font-size: 15.5px; line-height: 1.55; color: #45535f; font-weight: 500; flex: 1;">Professionelles Polieren, Versiegeln und Aufbereiten für tiefen Glanz und langanhaltenden Schutz Ihres Lacks.</p>
<span style="font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #1e7fc2;">Politur · Versiegelung</span>
</div>
<div style="background: #F1F0EB; border-radius: 24px; padding: 34px 30px; display: flex; flex-direction: column; gap: 12px; box-shadow: 2px 4px 14px rgba(0,0,0,.25);">
<h3 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; font-size: 23px; color: #0D2A46;">Felgen &amp; Motorrad</h3>
<p style="font-size: 15.5px; line-height: 1.55; color: #45535f; font-weight: 500; flex: 1;">Felgenaufbereitung und Motorradlackierung im gleichen hohen Standard für den perfekten Auftritt bis ins Detail.</p>
<span style="font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #1e7fc2;">Felgen · Zweirad</span>
</div>
<div style="position: relative; border-radius: 24px; overflow: hidden; box-shadow: 2px 4px 14px rgba(0,0,0,.25);">
<video ref="{{ videoRef }}" src="assets/leistungen-video.mp4" autoplay muted loop playsinline style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;"></video>
<div style="position: absolute; inset: 0; background: linear-gradient(180deg, rgba(8,20,34,0) 45%, rgba(8,20,34,.78) 100%); pointer-events: none;"></div>
<div style="position: absolute; left: 30px; right: 30px; bottom: 26px; display: flex; flex-direction: column; gap: 8px;">
<h3 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; font-size: 23px; color: #f5f4f0;">Design- &amp; Sonderlackierung</h3>
<p style="font-size: 14.5px; line-height: 1.5; color: #d8e2ec; font-weight: 500;">Individuelle Farbwünsche, Sondereffekte und Akzente wir setzen Ihre Ideen sauber und hochwertig um.</p>
<span style="font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #9FD1F5;">Custom · Effektlack</span>
</div>
</div>
</div>
</div>
</section>
<!-- REFERENZEN SECTION -->
<section id="referenzen" data-screen-label="Referenzen" style="position: relative; overflow: hidden; background: #0D2A46; padding: clamp(80px, 12vh, 140px) clamp(20px, 4vw, 56px);">
<img src="https://lack-box.de/wp-content/uploads/2020/07/startseite-02-radkasten-2048-squo-1.jpg" alt="" style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: hue-rotate(85deg) saturate(1.2); pointer-events: none;">
<div style="position: absolute; inset: 0; background: rgba(18,60,120,0.38); mix-blend-mode: multiply; pointer-events: none;"></div>
<div style="position: absolute; inset: 0; background: linear-gradient(180deg, rgba(10,18,30,.8) 0%, rgba(10,18,30,.45) 45%, rgba(10,18,30,.8) 100%); pointer-events: none;"></div>
<div style="position: relative; max-width: 1320px; margin: 0 auto;">
<div style="max-width: 760px; margin-bottom: clamp(40px, 6vh, 64px);">
<span style="display: inline-block; font-size: 13px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; color: #9FD1F5; margin-bottom: 16px;">Referenzen</span>
<h2 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; line-height: 1.1; color: #f5f4f0; font-size: clamp(30px, 3.6vw, 52px); margin-bottom: 18px;">Echte Arbeiten aus unserer Werkstatt</h2>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.55; color: #c9d6e2; font-weight: 500;">Karosserieschaden? Ziehen Sie den Regler und sehen Sie den Unterschied vorher und nachher.</p>
</div>
<!-- Vorher/Nachher-Vergleich -->
<div ref="{{ cmpRef }}" onMouseDown="{{ cmpDown }}" onTouchStart="{{ cmpDown }}" style="position: relative; max-width: 980px; margin: 0 auto; aspect-ratio: 3/2; border-radius: 24px; overflow: hidden; box-shadow: 2px 6px 24px rgba(0,0,0,.45); user-select: none; touch-action: pan-y; cursor: ew-resize; background: #0a0e13;">
<!-- Vorher (untere Ebene) -->
<img src="assets/work/work5.jpg" alt="Kotflügel mit Karosserieschaden vor der Reparatur" draggable="false" style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;">
<!-- Nachher (obere Ebene, per clip-path beschnitten) -->
<div ref="{{ cmpAfterRef }}" style="position: absolute; inset: 0; overflow: hidden; clip-path: inset(0 50% 0 0);">
<img src="assets/work/work1.jpg" alt="Kotflügel nach Reparatur und Lackierung in Hochglanz" draggable="false" style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;">
</div>
<!-- Labels -->
<span style="position: absolute; top: 18px; left: 18px; padding: 6px 14px; border-radius: 20px; background: rgba(8,20,34,.75); color: #fff; font-size: 12.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; pointer-events: none;">Nachher</span>
<span style="position: absolute; top: 18px; right: 18px; padding: 6px 14px; border-radius: 20px; background: rgba(8,20,34,.75); color: #fff; font-size: 12.5px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; pointer-events: none;">Vorher</span>
<!-- Griff -->
<div ref="{{ cmpHandleRef }}" style="position: absolute; top: 0; bottom: 0; left: 50%; width: 3px; margin-left: -1.5px; background: rgba(255,255,255,.85); pointer-events: none;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 48px; height: 48px; border-radius: 50%; background: #fff; box-shadow: 0 4px 16px rgba(0,0,0,.4); display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#0D2A46" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 3 12 9 6"></polyline><polyline points="15 6 21 12 15 18"></polyline></svg>
</div>
</div>
</div>
<p style="margin-top: clamp(28px, 4vh, 44px); text-align: center; font-size: 15px; color: #c9d6e2; font-weight: 500;">Mehr laufende Projekte gibt es auf unserer <a href="https://www.facebook.com/derlackspezialist/" target="_blank" rel="noopener" style="color: #9FD1F5; font-weight: 700;">Facebook-Seite</a>.</p>
</div>
</section>
<!-- ÜBER UNS SECTION -->
<section id="ueber-uns" data-screen-label="Über uns" style="position: relative; overflow: hidden; background: #0D2A46; padding: clamp(80px, 12vh, 140px) clamp(20px, 4vw, 56px);">
<svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true" style="position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;">
<path d="M-100,120 C340,260 720,-20 1100,140 C1320,230 1450,160 1560,210" fill="none" stroke="#1d5c99" stroke-width="120" stroke-linecap="round" opacity="0.5"></path>
<path d="M-120,480 C280,340 640,620 1020,460 C1280,350 1400,420 1560,360" fill="none" stroke="#123a63" stroke-width="210" stroke-linecap="round" opacity="0.8"></path>
<path d="M-100,800 C320,660 560,940 960,780 C1220,670 1380,730 1560,610" fill="none" stroke="#164a7d" stroke-width="150" stroke-linecap="round" opacity="0.7"></path>
</svg>
<div style="position: absolute; inset: 0; background: linear-gradient(180deg, rgba(13,42,70,.45) 0%, rgba(13,42,70,.10) 40%, rgba(13,42,70,.45) 100%); pointer-events: none;"></div>
<div style="position: relative; max-width: 1320px; margin: 0 auto;">
<div style="max-width: 900px; margin-bottom: clamp(50px, 8vh, 90px);">
<span style="display: inline-block; font-size: 13px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; color: #9FD1F5; margin-bottom: 16px;">Über uns</span>
<h2 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; line-height: 1.12; color: #f5f4f0; font-size: clamp(32px, 4vw, 58px);">Wir kombinieren handwerkliches Wissen und viel Erfahrung mit modernster Technik.</h2>
</div>
<!-- Reihe 1: Vorstellung + Profilbild-Platzhalter -->
<div class="lw-about" style="display: grid; grid-template-columns: 1.1fr 0.9fr; gap: clamp(28px, 4vw, 64px); align-items: center; margin-bottom: clamp(50px, 8vh, 90px);">
<div style="display: flex; flex-direction: column; gap: 16px;">
<h3 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; font-size: clamp(24px, 2.2vw, 32px); line-height: 1.2; color: #f5f4f0;">Hallo, ich bin<br>Patric Hertz,</h3>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.6; color: #c9d6e2; font-weight: 500;">Lack- und Karosserie-Experte und Inhaber der Lackwerk GmbH in Kaiserslautern.</p>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.6; color: #c9d6e2; font-weight: 500;">Mit meinem Werkstatt-Team aus professionellen Fachkräften erfülle ich Ihre individuellen Wünsche zum bestmöglichen Preis — von der Smart-Repair bis zur kompletten Restauration.</p>
</div>
<div style="justify-self: center; width: min(340px, 100%);">
<div style="position: relative; width: 100%; aspect-ratio: 4/5; border-radius: 24px; overflow: hidden; box-shadow: 2px 6px 24px rgba(0,0,0,.4); background: rgba(255,255,255,.05); border: 1.5px dashed rgba(255,255,255,.3);">
<x-import component-from-global-scope="image-slot" from="./assets/image-slot.js" id="profil-patric" shape="rect" fit="cover" placeholder="Profilbild von Patric Hertz hier ablegen" hint-size="100%,100%" style="display:block;width:100%;height:100%;"></x-import>
</div>
</div>
</div>
<!-- Reihe 2: Kundenstimmen + Philosophie -->
<div class="lw-about" style="display: grid; grid-template-columns: 0.9fr 1.1fr; gap: clamp(28px, 4vw, 64px); align-items: center;">
<div style="position: relative; border-radius: 24px; padding: 28px; background: rgba(10,20,34,.55); border: 1px solid rgba(255,255,255,.12); backdrop-filter: blur(10px); box-shadow: 2px 6px 24px rgba(0,0,0,.35);">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
<span style="display: inline-flex; align-items: center; justify-content: center; width: 42px; height: 42px; border-radius: 50%; background: var(--accent, #2FA4F0); color: #0d0f12; font-weight: 800; font-size: 17px; flex-shrink: 0;">{{ reviewInitial }}</span>
<div style="display: flex; flex-direction: column; gap: 3px; min-width: 0;">
<span style="color: #f4f3ef; font-weight: 700; font-size: 15px;">{{ reviewAuthor }}</span>
<span style="color: #F2BD3F; font-size: 14px; letter-spacing: 2px; line-height: 1;">★★★★★</span>
</div>
</div>
<p ref="{{ reviewTextRef }}" style="color: #cdd6e0; font-size: 15px; line-height: 1.6; font-weight: 500; transition: opacity .4s ease;">{{ reviewText }}</p>
<span style="display: block; margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(255,255,255,.1); color: #8b9cae; font-size: 12.5px; font-weight: 600;">{{ reviewMeta }}</span>
</div>
<div style="display: flex; flex-direction: column; gap: 16px;">
<h3 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; font-size: clamp(24px, 2.2vw, 32px); line-height: 1.2; color: #f5f4f0;">Meine Philosophie</h3>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.6; color: #c9d6e2; font-weight: 500;">Für Sie meine Kunden soll meisterhafte Arbeit bezahlbar sein. Unser Service sieht aus wie aus der Vertragswerkstatt, hat die gleiche Qualität, ist aber deutlich günstiger.</p>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.6; color: #c9d6e2; font-weight: 500;">Es ist für mich oberstes Gesetz, dass Sie nur 1A-Qualität bekommen, statt billiger „Pinselarbeiten". Mit hastig erledigten, minderwertigen Flickarbeiten kann ich nicht dienen.</p>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.6; color: #c9d6e2; font-weight: 500;">Mein Credo: Glückliche Kunden kommen wieder. Mein Team und ich sind unheimlich stolz auf Ihre Zufriedenheit und begeisterten Bewertungen.</p>
</div>
</div>
</div>
</section>
<!-- KONTAKT SECTION -->
<section id="kontakt" data-screen-label="Kontakt &amp; Anfahrt" style="position: relative; overflow: hidden; background: #10151b; padding: clamp(80px, 12vh, 140px) clamp(20px, 4vw, 56px);">
<img src="https://lack-box.de/wp-content/uploads/2020/07/startseite-03-vorderlicht-rechts-squo.jpg" alt="" style="position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: hue-rotate(85deg) saturate(1.2); pointer-events: none;">
<div style="position: absolute; inset: 0; background: rgba(18,60,120,0.38); mix-blend-mode: multiply; pointer-events: none;"></div>
<div style="position: absolute; inset: 0; background: linear-gradient(180deg, rgba(10,18,30,.88) 0%, rgba(10,18,30,.55) 45%, rgba(10,18,30,.88) 100%); pointer-events: none;"></div>
<div style="position: relative; max-width: 1320px; margin: 0 auto;">
<div style="max-width: 760px; margin-bottom: clamp(40px, 6vh, 64px);">
<span style="display: inline-block; font-size: 13px; font-weight: 700; letter-spacing: .18em; text-transform: uppercase; color: #9FD1F5; margin-bottom: 16px;">Kontakt &amp; Anfahrt</span>
<h2 style="font-family: 'Source Serif 4', Georgia, serif; font-weight: 600; line-height: 1.1; color: #f5f4f0; font-size: clamp(30px, 3.6vw, 52px); margin-bottom: 18px;">Sprechen wir über Ihr Fahrzeug</h2>
<p style="font-size: clamp(15px, 1.3vw, 18px); line-height: 1.55; color: #c9d6e2; font-weight: 500;">Rufen Sie an, schreiben Sie uns oder kommen Sie direkt vorbei wir beraten Sie gern und unverbindlich.</p>
</div>
<div class="lw-contact" style="display: grid; grid-template-columns: 0.9fr 1.1fr; gap: clamp(24px, 3vw, 40px); align-items: stretch;">
<!-- Kontakt-Infos -->
<div style="display: flex; flex-direction: column; gap: 18px; border-radius: 24px; padding: clamp(26px, 3vw, 40px); background: rgba(10,20,34,.6); border: 1px solid rgba(255,255,255,.12); backdrop-filter: blur(10px); box-shadow: 2px 6px 24px rgba(0,0,0,.35);">
<div style="display: flex; flex-direction: column; gap: 4px;">
<span style="font-size: 12.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #9FD1F5;">Adresse</span>
<p style="font-size: 16px; line-height: 1.5; color: #e7ecf1; font-weight: 600;">Lackwerk GmbH<br>Gienanthstr. 24, 67663 Kaiserslautern</p>
</div>
<div style="display: flex; flex-direction: column; gap: 4px;">
<span style="font-size: 12.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #9FD1F5;">Telefon</span>
<p style="font-size: 16px; line-height: 1.5; color: #e7ecf1; font-weight: 600;"><a href="tel:+4963135789060" style="color: #e7ecf1;">0631 / 35 78 90-6</a><br><span style="font-weight: 500; color: #b8c4d0;">Fax: 0631 / 35 78 90-8</span></p>
</div>
<div style="display: flex; flex-direction: column; gap: 4px;">
<span style="font-size: 12.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #9FD1F5;">E-Mail</span>
<p style="font-size: 16px; line-height: 1.5; font-weight: 600;"><a href="mailto:info@lackwerk-kl.de" style="color: #e7ecf1;">info@lackwerk-kl.de</a></p>
</div>
<div style="display: flex; flex-direction: column; gap: 4px;">
<span style="font-size: 12.5px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #9FD1F5;">Öffnungszeiten</span>
<p style="font-size: 16px; line-height: 1.6; color: #e7ecf1; font-weight: 600;">Montag Freitag<br><span style="font-weight: 500; color: #b8c4d0;">09:00 13:00 · 14:00 17:00</span><br>Samstag &amp; Sonntag<br><span style="font-weight: 500; color: #b8c4d0;">Geschlossen</span></p>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px;">
<a href="tel:+4963135789060" style="display: inline-flex; align-items: center; gap: 8px; font-size: 14.5px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase; color: #0d0f12; background: var(--accent, #2FA4F0); padding: 13px 22px; border-radius: 44px; transition: transform .2s ease, box-shadow .2s ease;" style-hover="transform: translateY(-2px); box-shadow: 0 10px 26px rgba(47,164,240,.4);">Jetzt anrufen</a>
<a href="mailto:info@lackwerk-kl.de" style="display: inline-flex; align-items: center; font-size: 14.5px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase; color: #f4f3ef; padding: 13px 22px; border-radius: 44px; border: 1.5px solid rgba(255,255,255,.3); transition: border-color .2s ease, background .2s ease;" style-hover="border-color: var(--accent, #2FA4F0); background: rgba(255,255,255,.05);">E-Mail schreiben</a>
</div>
</div>
<!-- Karte -->
<div style="position: relative; border-radius: 24px; overflow: hidden; box-shadow: 2px 6px 24px rgba(0,0,0,.4); min-height: 420px; background: #0a0e13;">
<div ref="{{ mapRef }}" style="position: absolute; inset: 0;"></div>
</div>
</div>
</div>
</section>
<!-- FOOTER (sticky reveal) -->
<footer data-screen-label="Footer" style="position: relative; height: 70vh; clip-path: polygon(0% 0, 100% 0%, 100% 100%, 0 100%);">
<div style="position: relative; height: calc(100vh + 70vh); top: -100vh;">
<div style="height: 70vh; position: sticky; top: calc(100vh - 70vh);">
<div style="height: 100%; width: 100%; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; position: relative; background: linear-gradient(135deg, #0a1526 0%, #0D2A46 55%, #0a1526 100%); padding: clamp(32px, 6vh, 64px) clamp(20px, 4vw, 56px);">
<!-- Glow-Elemente -->
<div style="position: absolute; top: -120px; right: -120px; width: 380px; height: 380px; border-radius: 50%; background: rgba(47,164,240,.09); filter: blur(70px); pointer-events: none;"></div>
<div style="position: absolute; bottom: -140px; left: -100px; width: 380px; height: 380px; border-radius: 50%; background: rgba(29,92,153,.12); filter: blur(70px); pointer-events: none;"></div>
<!-- Link-Spalten -->
<div class="lw-footer-cols" style="position: relative; z-index: 1; display: grid; grid-template-columns: repeat(4, 1fr); gap: clamp(24px, 4vw, 80px); max-width: 1320px; width: 100%; margin: 0 auto;">
<div style="display: flex; flex-direction: column; gap: 9px;">
<h3 style="font-size: 12px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #7f97ac; border-bottom: 1px solid rgba(255,255,255,.12); padding-bottom: 7px; margin-bottom: 6px;">Navigation</h3>
<a href="#top" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Start</a>
<a href="#services" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Services</a>
<a href="#referenzen" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Referenzen</a>
<a href="#ueber-uns" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Über uns</a>
<a href="#kontakt" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Kontakt</a>
</div>
<div style="display: flex; flex-direction: column; gap: 9px;">
<h3 style="font-size: 12px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #7f97ac; border-bottom: 1px solid rgba(255,255,255,.12); padding-bottom: 7px; margin-bottom: 6px;">Leistungen</h3>
<a href="#services" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Lackierungen aller Art</a>
<a href="#services" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Smart Repair</a>
<a href="#services" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Oldtimer &amp; Klassiker</a>
<a href="#services" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Felgen &amp; Motorrad</a>
</div>
<div style="display: flex; flex-direction: column; gap: 9px;">
<h3 style="font-size: 12px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #7f97ac; border-bottom: 1px solid rgba(255,255,255,.12); padding-bottom: 7px; margin-bottom: 6px;">Kontakt</h3>
<span style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Gienanthstr. 24<br>67663 Kaiserslautern</span>
<a href="tel:+4963135789060" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">0631 / 35 78 90-6</a>
<a href="mailto:info@lackwerk-kl.de" style="font-size: 14px; color: #c9d6e2; font-weight: 500;">info@lackwerk-kl.de</a>
</div>
<div style="display: flex; flex-direction: column; gap: 9px;">
<h3 style="font-size: 12px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: #7f97ac; border-bottom: 1px solid rgba(255,255,255,.12); padding-bottom: 7px; margin-bottom: 6px;">Öffnungszeiten</h3>
<span style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Mo Fr<br>09:00 13:00 · 14:00 17:00</span>
<span style="font-size: 14px; color: #c9d6e2; font-weight: 500;">Sa &amp; So geschlossen</span>
</div>
</div>
<!-- Untere Zeile -->
<div class="lw-footer-bottom" style="position: relative; z-index: 1; display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; max-width: 1320px; width: 100%; margin: 0 auto;">
<div style="min-width: 0;">
<img src="assets/logo-lackwerk.png" alt="Lackwerk" style="width: min(46vw, 560px); height: auto; display: block; opacity: .95;">
<div style="display: flex; align-items: center; gap: 14px; margin-top: 16px;">
<span style="width: 44px; height: 2px; background: linear-gradient(90deg, #2FA4F0, transparent);"></span>
<span style="font-size: 13.5px; color: #8fa4b8; font-weight: 500;">Autolackiererei &amp; Karosserie · Kaiserslautern</span>
</div>
</div>
<div style="text-align: right; flex-shrink: 0;">
<p style="font-size: 13px; color: #8fa4b8; font-weight: 500; margin-bottom: 12px;">© 2026 Lackwerk GmbH · Alle Rechte vorbehalten</p>
<div style="display: flex; justify-content: flex-end; align-items: center; gap: 14px;">
<a href="https://www.facebook.com/derlackspezialist/" target="_blank" rel="noopener" aria-label="Facebook" style="width: 36px; height: 36px; border-radius: 50%; background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.14); display: inline-flex; align-items: center; justify-content: center; color: #c9d6e2; font-weight: 800; font-size: 15px; transition: background .2s ease, transform .2s ease;" style-hover="background: var(--accent, #2FA4F0); color: #0a1526; transform: scale(1.1);">f</a>
<a href="#impressum" style="font-size: 13px; color: #8fa4b8; font-weight: 500;">Impressum</a>
<a href="#datenschutz" style="font-size: 13px; color: #8fa4b8; font-weight: 500;">Datenschutz</a>
</div>
</div>
</div>
</div>
</div>
</div>
</footer>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1440,&quot;height&quot;:900},&quot;accentColor&quot;:{&quot;editor&quot;:&quot;color&quot;,&quot;default&quot;:&quot;#2FA4F0&quot;,&quot;tsType&quot;:&quot;string&quot;,&quot;section&quot;:&quot;Design&quot;,&quot;options&quot;:[&quot;#2FA4F0&quot;,&quot;#F0912F&quot;,&quot;#E23744&quot;,&quot;#16462C&quot;,&quot;#C9A227&quot;]},&quot;showReviewCard&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;,&quot;section&quot;:&quot;Inhalt&quot;},&quot;rotateWords&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:true,&quot;tsType&quot;:&quot;boolean&quot;,&quot;section&quot;:&quot;Inhalt&quot;}}">
class Component extends DCLogic {
state = {
loading: true,
reviews: [],
idx: 0,
rating: 4.8,
count: 112,
};
// demo fallback data (used when /api/reviews is unavailable, e.g. in preview)
DEMO = {
rating: 4.8,
user_ratings_total: 112,
reviews: [
{ author_name: "Michael Braun", rating: 5, text: "Kleiner Parkrempler an der Stoßstange — nach zwei Tagen sah der Wagen aus wie frisch vom Band. Perfekte Farbangleichung, fairer Preis." },
{ author_name: "Sandra Keller", rating: 5, text: "Top Beratung, ehrliche Einschätzung und absolut saubere Arbeit. Der Kratzer an der Fahrertür ist komplett verschwunden. Sehr zu empfehlen!" },
{ author_name: "Tomasz Nowak", rating: 5, text: "Delle im Kotflügel professionell instand gesetzt, Lack wie neu. Termin schnell bekommen, alles unkompliziert. Komme gerne wieder." },
],
};
PLACE_ID = "ChIJY8mtmUUNlkcRTRMePruieWA";
WORDS = ["Unfall", "Delle", "Beule", "Kratzer"];
componentDidMount() {
this.applyAccent();
this.sizeWordSlot();
this.onResize = () => this.sizeWordSlot();
window.addEventListener("resize", this.onResize);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => this.sizeWordSlot());
this.loadReviews();
if (this.props.rotateWords !== false) this.startWordRotator();
this.onScroll = () => this.updateNav();
window.addEventListener("scroll", this.onScroll, { passive: true });
this.updateNav();
this.initMap();
}
initMap() {
if (this._map || !this.mapEl) return;
if (!window.maplibregl) { setTimeout(() => this.initMap(), 250); return; }
const LNG = 7.7269046, LAT = 49.444377;
const map = new maplibregl.Map({
container: this.mapEl,
style: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
center: [LNG, LAT],
zoom: 14.5,
attributionControl: { compact: true },
cooperativeGestures: true,
});
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "bottom-right");
const el = document.createElement("div");
el.style.cssText = "width:20px;height:20px;border-radius:50%;background:#2FA4F0;border:3px solid #fff;box-shadow:0 0 0 6px rgba(47,164,240,.3),0 4px 12px rgba(0,0,0,.5);cursor:pointer;";
const popupHtml =
'<div style="font-family:Archivo,sans-serif;padding:4px 2px;min-width:190px;">' +
'<div style="font-weight:800;font-size:14px;color:#0D2A46;margin-bottom:3px;">Lackwerk GmbH</div>' +
'<div style="font-size:12.5px;color:#45535f;line-height:1.45;margin-bottom:8px;">Gienanthstr. 2\u20134<br>67663 Kaiserslautern</div>' +
'<a href="https://www.google.com/maps/dir/?api=1&destination=Gienanthstra\u00dfe+2-4,+67663+Kaiserslautern" target="_blank" rel="noopener" style="display:inline-block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#fff;background:#2FA4F0;padding:7px 13px;border-radius:20px;text-decoration:none;">Route planen</a>' +
"</div>";
const popup = new maplibregl.Popup({ offset: 20, closeButton: false }).setHTML(popupHtml);
new maplibregl.Marker({ element: el }).setLngLat([LNG, LAT]).setPopup(popup).addTo(map);
popup.setLngLat([LNG, LAT]).addTo(map);
this._map = map;
}
componentDidUpdate() { this.applyAccent(); }
componentWillUnmount() {
clearInterval(this._wordTimer);
clearInterval(this._revTimer);
if (this._map) { this._map.remove(); this._map = null; }
window.removeEventListener("scroll", this.onScroll);
window.removeEventListener("resize", this.onResize);
}
// Vorher/Nachher-Vergleich: Drag-Logik
cmpStart(e) {
e.preventDefault();
this.cmpMoveTo(e.touches ? e.touches[0].clientX : e.clientX);
this._cmpMove = (ev) => this.cmpMoveTo(ev.touches ? ev.touches[0].clientX : ev.clientX);
this._cmpUp = () => {
window.removeEventListener("mousemove", this._cmpMove);
window.removeEventListener("touchmove", this._cmpMove);
window.removeEventListener("mouseup", this._cmpUp);
window.removeEventListener("touchend", this._cmpUp);
};
window.addEventListener("mousemove", this._cmpMove);
window.addEventListener("touchmove", this._cmpMove, { passive: true });
window.addEventListener("mouseup", this._cmpUp);
window.addEventListener("touchend", this._cmpUp);
}
cmpMoveTo(clientX) {
if (!this.cmpEl || !this.cmpAfter || !this.cmpHandle) return;
const rect = this.cmpEl.getBoundingClientRect();
let p = ((clientX - rect.left) / rect.width) * 100;
p = Math.max(0, Math.min(100, p));
this.cmpAfter.style.clipPath = "inset(0 " + (100 - p) + "% 0 0)";
this.cmpHandle.style.left = p + "%";
}
// fix the rotating-word slot to the widest word so "Rein mit" never shifts
sizeWordSlot() {
const slot = this.wordSlot, word = this.word;
if (!slot || !word) return;
const probe = document.createElement("span");
probe.style.cssText = "position:absolute;visibility:hidden;white-space:nowrap;";
const cs = getComputedStyle(word);
probe.style.font = cs.font;
probe.style.fontStyle = cs.fontStyle;
probe.style.letterSpacing = cs.letterSpacing;
document.body.appendChild(probe);
let max = 0;
for (const w of this.WORDS) { probe.textContent = w; max = Math.max(max, probe.getBoundingClientRect().width); }
probe.remove();
slot.style.width = Math.ceil(max + 2) + "px";
}
applyAccent() {
if (this.root) this.root.style.setProperty("--accent", this.props.accentColor || "#F0912F");
}
updateNav() {
if (!this.nav) return;
const s = window.scrollY > 30;
this.nav.style.background = s ? "rgba(11,13,16,0.92)" : "transparent";
this.nav.style.backdropFilter = s ? "blur(14px)" : "none";
this.nav.style.borderBottomColor = s ? "rgba(255,255,255,0.10)" : "transparent";
this.nav.style.paddingTop = s ? "14px" : "22px";
this.nav.style.paddingBottom = s ? "14px" : "22px";
}
async loadReviews() {
let data = null;
try {
// In the production Next.js app this hits the server route handler
// /api/reviews (Place ID + GOOGLE_PLACES_API_KEY stay server-side).
const res = await fetch("/api/reviews", { cache: "no-store" });
if (res.ok) {
const j = await res.json();
if (j && Array.isArray(j.reviews) && j.reviews.length) data = j;
}
} catch (e) { /* fall through to demo */ }
if (!data) data = this.DEMO;
const reviews = (data.reviews || []).filter(r => r && r.text).slice(0, 5);
this.setState({
loading: false,
reviews,
rating: data.rating || this.DEMO.rating,
count: data.user_ratings_total || this.DEMO.user_ratings_total,
}, () => { if (reviews.length > 1) this.startReviewRotator(); });
}
startReviewRotator() {
this._revTimer = setInterval(() => {
const el = this.reviewText;
const next = (this.state.idx + 1) % this.state.reviews.length;
const swap = () => this.setState({ idx: next }, () => { if (this.reviewText) this.reviewText.style.opacity = 1; });
if (el) { el.style.opacity = 0; setTimeout(swap, 380); } else { swap(); }
}, 5200);
}
startWordRotator() {
let i = this.WORDS.indexOf("Kratzer");
this._wordTimer = setInterval(() => {
const el = this.word;
if (!el) return;
i = (i + 1) % this.WORDS.length;
const next = this.WORDS[i];
el.animate(
[{ transform: "translateY(0)", opacity: 1 }, { transform: "translateY(-110%)", opacity: 0 }],
{ duration: 300, easing: "cubic-bezier(.6,0,.4,1)", fill: "forwards" }
).onfinish = () => {
el.textContent = next;
el.animate(
[{ transform: "translateY(110%)", opacity: 0 }, { transform: "translateY(0)", opacity: 1 }],
{ duration: 340, easing: "cubic-bezier(.2,.7,.3,1)", fill: "forwards" }
);
};
}, 2400);
}
renderVals() {
const st = this.state;
const r = st.reviews[st.idx] || null;
const author = r ? r.author_name : "";
const initial = author ? author.trim().charAt(0).toUpperCase() : "";
const ratingLabel = String(st.rating).replace(".", ",");
return {
rootRef: (el) => { this.root = el; },
videoRef: (el) => { if (el) { el.muted = true; el.volume = 0; } },
mapRef: (el) => { this.mapEl = el; if (el) this.initMap(); },
cmpRef: (el) => { this.cmpEl = el; },
cmpAfterRef: (el) => { this.cmpAfter = el; },
cmpHandleRef: (el) => { this.cmpHandle = el; },
cmpDown: (e) => this.cmpStart(e),
navRef: (el) => { this.nav = el; },
wordRef: (el) => { this.word = el; },
wordSlotRef: (el) => { this.wordSlot = el; },
reviewTextRef: (el) => { this.reviewText = el; },
dot0Ref: () => {}, dot1Ref: () => {}, dot2Ref: () => {},
showReviewCard: this.props.showReviewCard !== false,
loading: st.loading,
hasReview: !st.loading && !!r,
reviewAuthor: author,
reviewInitial: initial,
reviewText: r ? ("“" + r.text + "”") : "",
reviewMeta: "Google Bewertung",
ratingLabel: ratingLabel,
countLabel: "(" + st.count + ")",
googleUrl: "https://www.google.com/maps/place/?q=place_id:" + this.PLACE_ID,
};
}
}
</script>
<script>
(async () => {
if (!document.title || !document.title.trim()) document.title = "Lackwerk Hero";
// Prepend so page-supplied @page / break rules (deck-stage's margin:0,
// author CSS) keep source-order precedence over the baseline.
const baseCss = document.createElement('style');
baseCss.textContent = "@media print {\n figure, table { break-inside: avoid; }\n *, *::before, *::after {\n print-color-adjust: exact; -webkit-print-color-adjust: exact;\n backdrop-filter: none !important; -webkit-backdrop-filter: none !important;\n animation-delay: -99s !important; animation-duration: .001s !important;\n animation-iteration-count: 1 !important; animation-fill-mode: both !important;\n animation-play-state: running !important; transition-duration: 0s !important;\n }\n}";
document.head.prepend(baseCss);
if (document.readyState === 'loading') {
await new Promise((r) => document.addEventListener('DOMContentLoaded', r, { once: true }));
}
let ds = document.querySelector('deck-stage');
// whenDefined never resolves when nothing registers the element, so only
// await it when a <deck-stage> tag exists (and race a timeout regardless).
if (ds && !customElements.get('deck-stage')) {
try {
await Promise.race([
customElements.whenDefined('deck-stage'),
new Promise((r) => setTimeout(r, 3000)),
]);
} catch (e) {}
ds = document.querySelector('deck-stage');
}
// dc-runtime assigns window.DCLogic at init and mounts into #dc-root on
// DOMContentLoaded; settle on that subtree so React has rendered before
// print(). Non-DC pages fall through to weighing body.
const isDc = !ds && 'DCLogic' in window;
const settleRoot = ds || (isDc ? document.getElementById('dc-root') : null) || document.body;
if (settleRoot) {
// Weigh only content children — skip script/style/template so inline
// Babel/CSS doesn't register as stable "content" before the framework
// has mounted anything.
const MEDIA = 'img,svg,canvas,video,picture,iframe,embed,object';
const weigh = () => {
let text = 0, media = 0;
for (const c of settleRoot.children) {
const t = c.tagName;
if (t === 'SCRIPT' || t === 'STYLE' || t === 'TEMPLATE') continue;
text += (c.textContent || '').trim().length;
media += c.querySelectorAll(MEDIA).length + (c.matches(MEDIA) ? 1 : 0);
}
return text + media * 100;
};
// "Non-zero and stable" alone is not sufficient to mean rendered: generic
// body can carry a static shell while async main content is pending, and
// #dc-root can carry the root DC's static text while child refs are still
// .sc-placeholder boxes. Gate generic on readyState 'complete', and never
// accept stability while a dc-runtime placeholder is present.
const isGeneric = settleRoot === document.body;
const deadline = Date.now() + 15000;
let last = -1, stable = 0;
while (Date.now() < deadline) {
const n = weigh();
const ready =
(!isGeneric || document.readyState === 'complete') &&
!settleRoot.querySelector('.sc-placeholder');
if (n > 0 && n === last && ready) { if (++stable >= 2) break; } else { stable = 0; }
last = n;
await new Promise((r) => setTimeout(r, 250));
}
}
// Project support.js may predate runtime default-seeding; push via bridge.
if (isDc && window.__dcSetProps && window.__dcRootName) {
try {
const dpEl = document.querySelector('script[data-dc-script]');
const meta = JSON.parse((dpEl && dpEl.getAttribute('data-props')) || '{}');
const bag = {};
for (const k in meta)
if (k[0] !== '$' && meta[k] && meta[k].default !== undefined)
bag[k] = meta[k].default;
if (Object.keys(bag).length) {
window.__dcSetProps(window.__dcRootName(), bag);
await new Promise((r) => setTimeout(r, 50));
}
} catch (e) {}
}
// Reveal content gated on scroll or visibility before printing. Lazy <img>,
// IntersectionObserver fade-ins, and collapsed <details> don't render while
// off-screen, so a long page would print with them blank or closed. Open
// every <details>, force native-lazy images eager, and scroll the document
// through its full height to trip lazy-load + IO; then wait (bounded) for the
// newly-triggered images. Inert for deck-stage (slides aren't scroll-gated).
try {
for (const d of document.querySelectorAll('details')) d.open = true;
for (const img of document.querySelectorAll('img[loading="lazy"]'))
img.loading = 'eager';
const scroller = document.scrollingElement || document.documentElement;
const full = Math.max(scroller.scrollHeight, document.body.scrollHeight);
const step = window.innerHeight || 800;
for (let y = 0; y <= full; y += step) {
window.scrollTo(0, y);
await new Promise((r) => requestAnimationFrame(r));
}
window.scrollTo(0, 0);
await Promise.race([
Promise.all(
[...document.images].map((img) =>
img.complete
? null
: new Promise((res) => {
img.addEventListener('load', res, { once: true });
img.addEventListener('error', res, { once: true });
})
)
),
new Promise((r) => setTimeout(r, 4000)),
]);
} catch (e) {}
try { await document.fonts.ready; } catch (e) {}
// Jump every animation to its end state so print() never captures a slide
// mid-entrance. Ordering matters: the 0s-transition style must be live
// BEFORE data-deck-active is set so attribute-keyed transitions don't
// start a (now-0s) run from the pre-active base.
const st = document.createElement('style');
st.textContent = '*,*::before,*::after{animation-delay:-99s !important;animation-duration:.001s !important;animation-iteration-count:1 !important;animation-fill-mode:both !important;animation-play-state:running !important;transition-duration:0s !important}';
document.head.appendChild(st);
for (const d of document.querySelectorAll('deck-stage')) {
for (const c of d.children) {
const t = c.tagName;
if (t === 'SCRIPT' || t === 'STYLE' || t === 'TEMPLATE') continue;
c.setAttribute('data-deck-active', '');
}
}
// Re-append deck-stage's @page last so no author @page can add a margin and
// spill each full-bleed slide onto two sheets. Backstop for old copies.
if (ds) {
let pg = document.getElementById('deck-stage-print-page');
if (!pg) {
pg = document.createElement('style');
pg.textContent = '@page{size:' + (+ds.getAttribute('width') || 1920) +
'px ' + (+ds.getAttribute('height') || 1080) + 'px;margin:0}';
}
document.body.appendChild(pg);
}
try {
const sweep = () => {
for (const a of document.getAnimations()) {
try { a.finish(); } catch (e) {
try { a.currentTime = (a.effect && a.effect.getComputedTiming().duration) || 0; a.pause(); } catch (e2) {}
}
}
};
sweep();
await new Promise((r) => requestAnimationFrame(r));
sweep();
await new Promise((r) => requestAnimationFrame(r));
} catch (e) {}
// A document that owns its print CSS (the doc-page starter injects the
// meta at runtime; hand-rolled print designs author it) gets no automatic
// page geometry and keeps its forced breaks: strip the injected default
// @page — write time cannot see a runtime-injected meta — skip the frame
// override below, and skip the fakepage-flattening further down. What
// still applies: the print baseline and break-hygiene defaults (no @page
// in either; low specificity, so the document's own print CSS wins) and
// the break-inside relaxation on >page elements (avoid on those cannot
// be honored whoever authored it).
var ownsPrint = !!document.querySelector('meta[name="omelette-owns-print" i]');
if (ownsPrint) {
var dst = document.querySelector('style[data-omelette-print-defaults]');
if (dst && dst.textContent) {
dst.textContent = dst.textContent.replace(/@page\b[^{}]*\{[^}]*\}/, '');
}
}
// Size the PDF page to the rendered design unless the file opted into
// standard paper via @page{size:letter|A4|…} or declared print ownership
// via the meta above. Decks own their own @page.
// Wrappers (body, …) lose padding+margin so the frame sits at (0,0); the
// frame itself loses only margin — its padding is INSIDE the measured
// rect and must stay, or content shifts and leaves a gap at bottom/right.
var __m = null;
try { __m = (() => {
var STAMP = true;
var docEl = document.documentElement;
var doc = { width: docEl.scrollWidth, height: docEl.scrollHeight };
// Name-presence check, case-insensitive — the same contract as the
// instant-PDF path's runtime check in deck-stage-export.ts. The name is
// a string literal here because that file interpolates this script into
// its print script, so importing a shared constant from it would be a
// cycle; measure.test.ts pins the literal so the two cannot drift apart
// silently.
var ownsPrint = !!document.querySelector('meta[name="omelette-owns-print" i]');
var ds = document.querySelector('deck-stage');
if (ds) {
var dw = Number(ds.getAttribute('width')) || 1920;
var dh = Number(ds.getAttribute('height')) || 1080;
var n = typeof ds.length === 'number' ? ds.length : ds.children.length;
var idx = typeof ds.index === 'number' ? ds.index : 0;
var frames = [];
for (var i = 0; i < n; i++) frames.push({ left: 0, top: 0, width: dw, height: dh });
return { kind: 'deck', source: 'declared', doc: doc, frames: frames,
authored: frames.map(function(){return{width:true,height:true}}),
deck: { width: dw, height: dh, slideCount: n, index: idx },
signals: { paperPage: false, pxPage: false, ownsPrint: ownsPrint } };
}
// Declared kind: a doc-page tag names the kind outright, so no signal
// heuristics run (like the deck branch above, signals report false —
// they describe inferred classifications only). The tag alone is not
// trusted: the element must have upgraded (shadowRoot present), so a
// leftover tag whose script was deleted falls through to the heuristics
// that handle plain markup.
var stageSx = window.scrollX || 0, stageSy = window.scrollY || 0;
var dp = document.querySelector('doc-page');
if (dp && dp.shadowRoot) {
// Crop to the sheet, not the host — the desk around it is screen-only
// chrome that must not land in PNG exports.
var sheet = dp.shadowRoot.querySelector('.sheet');
var sr = (sheet || dp).getBoundingClientRect();
return { kind: 'paper-doc', source: 'declared', doc: doc,
frames: [{ left: sr.left + stageSx, top: sr.top + stageSy,
width: sr.width, height: sr.height }],
authored: [{ width: false, height: false }],
signals: { paperPage: false, pxPage: false, ownsPrint: ownsPrint } };
}
var paperPage = false, pxPage = false;
var eachRule = function (fn) {
for (var s = 0; s < document.styleSheets.length; s++) {
var rs; try { rs = document.styleSheets[s].cssRules; } catch (e) { continue; }
for (var r = 0; r < rs.length; r++) {
fn(rs[r]);
if (rs[r].cssRules) for (var n = 0; n < rs[r].cssRules.length; n++) fn(rs[r].cssRules[n]);
}
}
};
eachRule(function (r) {
if (r.type !== 6) return;
var m = /\bsize\s*:\s*([^;}]+)/i.exec(r.cssText || '');
if (!m) return;
if (/\b(letter|a[3-5]|legal|ledger)\b/i.test(m[1])) paperPage = true;
if (/\d+(?:\.\d+)?px\b/i.test(m[1])) pxPage = true;
});
var signals = { paperPage: paperPage, pxPage: pxPage, ownsPrint: ownsPrint };
var pxVal = function (v) {
var m = /^(\d+(?:\.\d+)?)px$/.exec(v || ''); return m ? +m[1] : 0;
};
var authoredPx = function (el, prop) {
if (pxVal(el.style[prop])) return true;
var attr = el.getAttribute(prop);
if (attr && /^\d+$/.test(attr)) return true;
var hit = false;
eachRule(function (r) {
if (hit || r.type !== 1 || !r.style) return;
try { if (el.matches(r.selectorText) && pxVal(r.style[prop])) hit = true; } catch (e) {}
});
return hit;
};
var el = document.body, rootPxW = false;
for (;;) {
var kids = [];
for (var c = 0; c < el.children.length; c++) {
var t = el.children[c].tagName;
if (t !== 'SCRIPT' && t !== 'STYLE' && t !== 'TEMPLATE') kids.push(el.children[c]);
}
if (kids.length !== 1 || !(kids[0] instanceof HTMLElement)) break;
if (STAMP) el.setAttribute('data-om-frame-wrap', '');
el = kids[0];
rootPxW = authoredPx(el, 'width');
if (rootPxW) break;
}
if (!rootPxW) rootPxW = authoredPx(el, 'width');
// @page{size:letter} alone is a flowing doc; with a px-width root it's a
// fixed sheet pretending to be paper — fall through and size to render.
if (paperPage && !rootPxW) {
return { kind: 'paper-doc', source: 'inferred', doc: doc, signals: signals,
frames: [{ left: 0, top: 0, width: doc.width, height: doc.height }],
authored: [{ width: false, height: false }] };
}
var sx = window.scrollX || 0, sy = window.scrollY || 0;
var rectOf = function (e) {
var r = e.getBoundingClientRect();
var oy; try { oy = getComputedStyle(e).overflowY; } catch (err) {}
var h = oy === 'visible' ? Math.max(r.height, e.scrollHeight || 0) : r.height;
return { left: r.left + sx, top: r.top + sy, width: r.width, height: h };
};
// Multi-frame: off unless the page opts in via data-om-multi-frame on the
// stop element — the explicit opt-in is what guards against card grids
// and pricing tables that match the geometry but aren't separate
// canvases. Two ways to name the frames, explicit first:
// 1. Descendants marked data-om-frame-export (the social-frames starter
// marks each asset frame; shells/panels around them are presentation,
// so nesting is expected). Document order.
// 2. Fallback, no marked descendants: direct children with an authored px
// width AND height (the original geometry heuristic).
// Frames may be different sizes (a gallery mixes 1080×1920 stories with
// 728×90 banners); data-om-frame-label rides along for export filenames.
if (el.hasAttribute('data-om-multi-frame')) {
var visOk = function (e) {
var cs; try { cs = getComputedStyle(e); } catch (err) { return false; }
return cs.display !== 'none' && cs.visibility !== 'hidden' && cs.position !== 'fixed';
};
// A frame's crop is its VISUAL box — never the scrollHeight-inflated
// rect that rectOf computes for page roots. Shadow-DOM hover controls
// (image-slot's Replace bar parks 29px below its host) and similar
// escaping UI contribute scrollable overflow to the frame, and the
// inflated height both squashes the nominal-size resize and drags
// content below the frame into the crop. Zoom-independent; bit every
// slot-bearing frame.
var frameRectOf = function (e) {
var r = e.getBoundingClientRect();
return { left: r.left + sx, top: r.top + sy, width: r.width, height: r.height };
};
var pushCand = function (list, e) {
if (!visOk(e)) return;
var rc = frameRectOf(e);
if (rc.width < 2 || rc.height < 2) return;
var lbl = e.getAttribute('data-om-frame-label');
if (lbl) rc.label = lbl;
list.push(rc);
};
var cands = [];
var marked = el.querySelectorAll('[data-om-frame-export]');
if (marked.length) {
for (var mk = 0; mk < marked.length; mk++) pushCand(cands, marked[mk]);
} else {
for (var k = 0; k < el.children.length; k++) {
var ch = el.children[k];
if (!authoredPx(ch, 'width') || !authoredPx(ch, 'height')) continue;
pushCand(cands, ch);
}
}
if (cands.length >= 2) {
return { kind: 'multi-frame', source: 'inferred', doc: doc, signals: signals,
frames: cands, authored: cands.map(function(){return{width:true,height:true}}) };
}
}
if (STAMP) el.setAttribute('data-om-frame', '');
return { kind: 'single-canvas', source: 'inferred', doc: doc, signals: signals,
frames: [rectOf(el)],
authored: [{ width: rootPxW, height: authoredPx(el, 'height') }] };
})(); } catch (e) {}
var frame = null;
if (!ownsPrint && __m && __m.kind === 'single-canvas') {
var f = __m.frames && __m.frames[0];
if (f && f.width >= 200 && f.height >= 200) {
frame = f;
var fst = document.createElement('style');
fst.textContent = '@media print{@page{size:' + Math.ceil(f.width) + 'px ' +
Math.ceil(f.height) + 'px;margin:0}' +
'html,body,[data-om-frame-wrap]{padding:0!important;margin:0!important;background:none!important}' +
'[data-om-frame]{margin:0!important}}';
document.body.appendChild(fst);
}
}
// Relax break-inside on oversized elements. An element taller than a
// printable page can't honor break-inside: avoid — browsers either overflow
// it or push it whole to the next page, leaving a blank gap, then break it
// anyway at an arbitrary point. After the final layout (content revealed,
// animations jumped), find elements taller than a page that still compute
// break-inside: avoid and reset them to auto, so the browser breaks them at
// their children's boundaries instead (children keep their own avoid). PAGE_PX
// is a conservative portrait estimate at 96 CSS px/in — Letter/A4 usable
// height minus our 0.5in margins — so only clearly-oversized elements are
// touched and under-page elements still stay whole. Skipped for decks (slides
// are sized to the page) and for fixed-size canvases (frame set, or an author
// px-based @page) — runs as before for flowing docs on letter/A4.
try {
if (!ds && !frame && !(__m && __m.signals && __m.signals.pxPage)) {
const PAGE_PX = 1024;
// Our print defaults add a real @page margin only when the source has
// no @page of its own; computed style can't tell us (it reports screen
// media), so read the injected rule directly.
const defaults = document.querySelector('style[data-omelette-print-defaults]');
const pageInset = /@page\s*\{[^}]*margin/.test(defaults ? defaults.textContent || '' : '');
const SEL = 'section,article,figure,table,pre,blockquote,[class~="card" i],[class~="page" i],[class~="sheet" i]';
for (const el of document.body.querySelectorAll(SEL)) {
if (el instanceof HTMLElement && el.offsetHeight > PAGE_PX) {
el.setAttribute('data-om-oversized', '');
// Fake-paged docs simulate sheets with over-tall break-after:page
// boxes; honoring the break spills half-blank pages and ends a
// colored box mid-sheet. Flow them, and (only when we own the page
// inset) drop the redundant padding/background via the rule below.
const cs = getComputedStyle(el);
// An owns-print doc's forced breaks are intentional sheet
// boundaries, never fake-paged simulation — don't strip them.
// The break-inside relaxation above still applies: avoid on a
// >page element is impossible to honor whoever authored it.
if (
!ownsPrint &&
(cs.breakAfter === 'page' || cs.pageBreakAfter === 'always')
) {
el.setAttribute('data-om-fakepage', '');
}
}
}
const ost = document.createElement('style');
ost.textContent = '@media print{' +
'[data-om-oversized]{break-inside:auto !important;page-break-inside:auto !important}' +
'[data-om-fakepage]{break-after:auto !important;page-break-after:auto !important}' +
(pageInset ? '[data-om-fakepage]{padding:0 !important;background:none !important}' : '') +
'}';
document.head.appendChild(ost);
}
} catch (e) {}
// macOS Preview renders Skia-PDF Luminosity soft-masks as opaque grey boxes;
// swap the CSS that triggers them for Preview-safe forms.
try {
for (const el of document.body.querySelectorAll('*')) {
const cs = getComputedStyle(el);
if (cs.boxShadow !== 'none' && cs.filter === 'none') {
const outs = [], kept = [];
for (const s of cs.boxShadow.split(/,(?![^(]*[)])/)) {
const t = s.trim();
const m = t.match(/^(rgba?[(][^)]+[)]) (-?[0-9.]+px) (-?[0-9.]+px) ([0-9.]+px) -?[0-9.]+px$/);
if (m && parseFloat(m[4]) > 0) outs.push('drop-shadow(' + m[2] + ' ' + m[3] + ' ' + m[4] + ' ' + m[1] + ')');
else kept.push(t);
}
if (outs.length) {
el.style.setProperty('filter', outs.join(' '), 'important');
el.style.setProperty('box-shadow', kept.join(', ') || 'none', 'important');
}
}
const mask = cs.maskImage && cs.maskImage !== 'none' ? cs.maskImage : cs.webkitMaskImage;
if (mask && mask !== 'none' && cs.filter === 'none') el.style.setProperty('filter', 'opacity(1)', 'important');
}
} catch (e) {}
setTimeout(() => { try { window.print(); } catch (e) {} }, 200);
})();
</script>
</body>
</html>