([\s\S]*?)<\/em>$/);
return m ? {m[1]} : {p} ;
});
const SectionHead = ({ eyebrow, title, intro, center }) =>
{eyebrow}
{renderSectionTitle(title)}
{intro ?
{intro}
: null}
;
const Button = ({ variant = "primary", children, href, ...rest }) => {
const base = {
fontFamily: 'var(--font-sans)', fontWeight: 500, fontSize: 11,
letterSpacing: '.14em', textTransform: 'uppercase', lineHeight: 1,
borderRadius: 0, cursor: 'pointer',
transition: 'background .2s, color .2s, border-color .2s',
textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 8
};
const variants = {
primary: { background: 'var(--primary)', color: '#fff', border: 0, padding: '14px 32px' },
ghost: { background: 'transparent', color: 'rgba(255,255,255,.7)', border: '.5px solid rgba(255,255,255,.2)', padding: '13px 28px' },
ghostDark: { background: 'transparent', color: 'var(--body)', border: '.5px solid var(--hairline-color)', padding: '13px 28px' },
ghostCopper: { background: 'transparent', color: 'var(--primary)', border: '.5px solid rgba(184,115,51,.35)', padding: '9px 20px' }
};
const style = { ...base, ...variants[variant] };
const Tag = href ? 'a' : 'button';
const hover = (e) => {
if (variant === 'primary') e.currentTarget.style.background = 'var(--primary-active)';
if (variant === 'ghost') {e.currentTarget.style.borderColor = 'rgba(255,255,255,.5)';e.currentTarget.style.color = '#fff';}
if (variant === 'ghostDark') {e.currentTarget.style.borderColor = 'var(--primary)';e.currentTarget.style.color = 'var(--primary)';}
if (variant === 'ghostCopper') {e.currentTarget.style.background = 'var(--primary)';e.currentTarget.style.color = '#fff';}
};
const out = (e) => {Object.assign(e.currentTarget.style, style);};
return (
{children}
);
};
// Inline CTA link with animated arrow.
const ServiceCTA = ({ children, href = "#", dark = false }) =>
{const a = e.currentTarget.querySelector('.arr');if (a) a.style.transform = 'translateX(4px)';}}
onMouseLeave={(e) => {const a = e.currentTarget.querySelector('.arr');if (a) a.style.transform = 'translateX(0)';}}>
{children}
→
;
// Decorative copper rings (hero + cta).
const CopperRings = ({ size = 600, opacity = 0.12, style = {} }) =>
;
// IntersectionObserver reveal wrapper.
const Reveal = ({ children, delay = 0, as: Tag = 'div', ...rest }) => {
const ref = React.useRef(null);
React.useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
setTimeout(() => e.target.classList.add('visible'), delay);
io.unobserve(e.target);
}
});
}, { threshold: 0.08 });
io.observe(el);
return () => io.disconnect();
}, [delay]);
return {children} ;
};
// Marquee track — duplicates items inline so translateX(-50%) loops seamlessly.
const MarqueeTrack = ({ items, gap = 80, duration = 28, style = {} }) =>
{[...items, ...items].map((label, i) => {label} )}
;
// "KI-generiert" provenance badge — placed on every generated image/video.
const AILabel = ({ position = 'tl', label = 'KI-generiert', style = {} }) => {
const corners = {
tl: { top: 12, left: 12 }, tr: { top: 12, right: 12 },
bl: { bottom: 12, left: 12 }, br: { bottom: 12, right: 12 },
};
return (
{label}
);
};
// Speaker glyph (muted / unmuted) — thin-line, matches the no-emoji rule.
const SpeakerIcon = ({ muted, size = 14 }) => (
{muted
?
: }
);
// Looping video that starts MUTED with a click-to-unmute speaker control (bottom-left).
// Pass `silent` to drop the audio entirely: permanently muted, no toggle.
const LoopVideo = ({ src, ratio = '4 / 3', dark = false, style = {}, fit = 'cover', radius = 0, silent = false }) => {
const vref = React.useRef(null);
const [muted, setMuted] = React.useState(true);
const toggle = (e) => {
e.stopPropagation(); e.preventDefault();
const v = vref.current; if (!v) return;
v.muted = !v.muted;
if (!v.muted) { v.volume = 1; const p = v.play(); if (p && p.catch) p.catch(() => {}); }
setMuted(v.muted);
};
return (
{ vref.current = el; if (el && silent) { el.muted = true; el.defaultMuted = true; el.volume = 0; } }}
onCanPlay={(e) => { if (silent) e.currentTarget.muted = true; const p = e.currentTarget.play(); if (p && p.catch) p.catch(() => {}); }}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: fit, display: 'block' }} />
{silent ? null :
{muted ? 'Ton an' : 'Ton aus'}
}
);
};
// Portrait iPhone mockup wrapping a looping video.
const IPhoneMock = ({ src, width = 230, dark = true }) => (
);
// Reusable before/after comparison slider (drag handle).
const BAWidget = ({ beforeSrc, afterSrc, beforeLabel = 'Vorher', afterLabel = 'Nachher', dark = false, ratio = '4 / 3' }) => {
const [pos, setPos] = React.useState(50);
const ref = React.useRef(null);
const dragging = React.useRef(false);
const setFromClientX = (clientX) => {
const el = ref.current;if (!el) return;
const r = el.getBoundingClientRect();
setPos(Math.max(0, Math.min(100, (clientX - r.left) / r.width * 100)));
};
React.useEffect(() => {
const move = (e) => {if (dragging.current) setFromClientX(e.touches ? e.touches[0].clientX : e.clientX);};
const up = () => {dragging.current = false;};
window.addEventListener('mousemove', move);window.addEventListener('mouseup', up);
window.addEventListener('touchmove', move, { passive: false });window.addEventListener('touchend', up);
return () => {window.removeEventListener('mousemove', move);window.removeEventListener('mouseup', up);window.removeEventListener('touchmove', move);window.removeEventListener('touchend', up);};
}, []);
const border = dark ? 'rgba(255,255,255,.1)' : 'var(--hairline-color)';
const lbl = (side) => ({
position: 'absolute', top: 14, [side]: 14, zIndex: 2,
fontFamily: 'var(--font-sans)', fontSize: 9, fontWeight: 500, letterSpacing: '.2em', textTransform: 'uppercase',
color: side === 'right' ? 'var(--primary)' : '#fff', background: 'rgba(26,26,26,.7)', padding: '6px 12px'
});
const imgStyle = { position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' };
return (
{dragging.current = true;setFromClientX(e.clientX);}}
onTouchStart={(e) => {dragging.current = true;setFromClientX(e.touches[0].clientX);}}
style={{ position: 'relative', width: '100%', aspectRatio: ratio, overflow: 'hidden', border: `.5px solid ${border}`, cursor: 'ew-resize', userSelect: 'none', background: 'var(--surface-warm-grey)' }}>
{beforeLabel}
{afterLabel}
);
};
// Lightweight YouTube facade: poster thumbnail + play button, swaps to the
// embed (autoplay) on click. More reliable than a cold iframe in previews.
const YTLite = ({ id, title = 'Video', dark = false, ratio = '16 / 9' }) => {
const [play, setPlay] = React.useState(false);
const border = dark ? 'rgba(255,255,255,.1)' : 'var(--hairline-color)';
return (
{play ?
VIDEO :
setPlay(true)} aria-label={`${title} abspielen`}
style={{ all: 'unset', cursor: 'pointer', position: 'absolute', inset: 0, display: 'block' }}>
{e.currentTarget.src = `https://img.youtube.com/vi/${id}/hqdefault.jpg`;}}
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
▶ {title}
}
{!play &&
e.stopPropagation()}
style={{ position: 'absolute', top: 14, right: 14, zIndex: 3, fontFamily: 'var(--font-sans)', fontSize: 9, fontWeight: 500, letterSpacing: '.16em', textTransform: 'uppercase', color: '#fff', background: 'rgba(26,26,26,.66)', padding: '7px 12px', textDecoration: 'none' }}>
Auf YouTube ↗
}
);
};
// Looping YouTube embed: autoplays muted on repeat, no chrome. For ambient
// showreel loops where the video IS the visual, not a click target.
const YTLoop = ({ id, title = 'Video', dark = false, ratio = '16 / 9', label }) => {
const border = dark ? 'rgba(255,255,255,.1)' : 'var(--hairline-color)';
const src = `https://www.youtube.com/embed/${id}?autoplay=1&mute=1&loop=1&playlist=${id}&controls=0&rel=0&modestbranding=1&playsinline=1&disablekb=1&iv_load_policy=3`;
return (
{label &&
{label}
}
);
};
// Node-based comparison slider: arbitrary content on each side.
// `before` fills from the left up to the handle; `after` is the base layer.
const BACompareNode = ({ before, after, beforeLabel = 'Vorher', afterLabel = 'Nachher', dark = false, ratio = '16 / 9', initial = 62 }) => {
const [pos, setPos] = React.useState(initial);
const ref = React.useRef(null);
const dragging = React.useRef(false);
const setFromClientX = (clientX) => {
const el = ref.current;if (!el) return;
const r = el.getBoundingClientRect();
setPos(Math.max(0, Math.min(100, (clientX - r.left) / r.width * 100)));
};
React.useEffect(() => {
const move = (e) => {if (dragging.current) {if (e.cancelable && e.touches) e.preventDefault();setFromClientX(e.touches ? e.touches[0].clientX : e.clientX);}};
const up = () => {dragging.current = false;};
window.addEventListener('mousemove', move);window.addEventListener('mouseup', up);
window.addEventListener('touchmove', move, { passive: false });window.addEventListener('touchend', up);
return () => {window.removeEventListener('mousemove', move);window.removeEventListener('mouseup', up);window.removeEventListener('touchmove', move);window.removeEventListener('touchend', up);};
}, []);
const border = dark ? 'rgba(255,255,255,.1)' : 'var(--hairline-color)';
const lbl = (side, accent) => ({
position: 'absolute', top: 14, [side]: 14, zIndex: 3,
fontFamily: 'var(--font-sans)', fontSize: 9, fontWeight: 500, letterSpacing: '.2em', textTransform: 'uppercase',
color: accent ? 'var(--primary)' : '#fff', background: 'rgba(26,26,26,.7)', padding: '6px 12px', pointerEvents: 'none'
});
return (
{dragging.current = true;setFromClientX(e.clientX);}}
onTouchStart={(e) => {dragging.current = true;setFromClientX(e.touches[0].clientX);}}
style={{ position: 'relative', width: '100%', aspectRatio: ratio, overflow: 'hidden', border: `.5px solid ${border}`, cursor: 'ew-resize', userSelect: 'none', background: '#000' }}>
{/* after = base (revealed on the left as the handle moves left) */}
{after}
{/* before = overlay clipped from left edge to handle */}
{before}
{beforeLabel}
{afterLabel}
);
};
// ── Global tweak bridge ───────────────────────────────────────────────
// Mirrors accent + headline-font tweaks to localStorage so the look carries
// across all four pages in preview. Apply on every page at load.
const MM_TWEAK_KEY = 'mm_global_tweaks';
function readGlobalTweaks() {
try {return JSON.parse(localStorage.getItem(MM_TWEAK_KEY) || '{}');}
catch (e) {return {};}
}
function applyGlobalTweaks(t) {
const root = document.documentElement;
if (t.accent) {
root.style.setProperty('--primary', t.accent);
// derive a darker active shade
root.style.setProperty('--primary-active', shade(t.accent, -0.16));
}
if (t.headlineFont) {
root.style.setProperty('--font-display', t.headlineFont);
}
}
function persistGlobalTweaks(patch) {
const next = { ...readGlobalTweaks(), ...patch };
try {localStorage.setItem(MM_TWEAK_KEY, JSON.stringify(next));} catch (e) {}
applyGlobalTweaks(next);
}
// crude hex shade helper
function shade(hex, amt) {
const m = hex.replace('#', '');
if (m.length !== 6) return hex;
let r = parseInt(m.slice(0, 2), 16),g = parseInt(m.slice(2, 4), 16),b = parseInt(m.slice(4, 6), 16);
r = Math.max(0, Math.min(255, Math.round(r + r * amt)));
g = Math.max(0, Math.min(255, Math.round(g + g * amt)));
b = Math.max(0, Math.min(255, Math.round(b + b * amt)));
return '#' + [r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('');
}
// Apply immediately on script load (before React renders) to avoid flash.
applyGlobalTweaks(readGlobalTweaks());
Object.assign(window, {
SectionEyebrow, SectionHead, Button, ServiceCTA, CopperRings, Reveal, MarqueeTrack, BAWidget, YTLite, YTLoop, BACompareNode, AILabel, SpeakerIcon, LoopVideo, IPhoneMock,
MM_TWEAK_KEY, readGlobalTweaks, applyGlobalTweaks, persistGlobalTweaks, shade
});