// mapemedia — shared primitives, reused across all pages. // Adapted from the design-system website UI kit. const SectionEyebrow = ({ children }) => {children}; // Render a title string that may contain .. as React nodes, // so the

can be direct-edited in the preview without crashing. const renderSectionTitle = (title) => String(title).split(/(.*?<\/em>)/g).map((p, i) => { const m = p.match(/^([\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 }) => ( ); // 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 (
); }; // Portrait iPhone mockup wrapping a looping video. const IPhoneMock = ({ src, width = 230, dark = true }) => (
{/* dynamic island */}
); // 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)' }}> {afterLabel}
{beforeLabel}
{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 ? : } {!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 });