Files
bmp-website-2026/src/spa/components/layout/Layout.tsx
syntaxbullet b57e08940d Refactor typography styles across multiple pages to use CSS classes for consistency and maintainability
- Updated Participation.tsx to replace inline styles with class names for headings and paragraphs.
- Refactored Preistraeger.tsx to utilize CSS classes for typography instead of inline styles.
- Modified PreistraegerDetail.tsx to implement class-based styling for headings and text elements.
- Adjusted Press.tsx to standardize typography using CSS classes, enhancing readability and maintainability.
2026-07-13 21:46:56 +02:00

869 lines
42 KiB
TypeScript
Raw 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.
'use client'
import Image from '@/spa/components/ui/UnoptimizedImage'
import React, { useState, useEffect, useRef } from 'react';
import { Link, useLocation } from '@/spa/router';
import { ArrowRight } from 'lucide-react';
import LoadingScreen from '@/spa/components/ui/LoadingScreen';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { useApplicationPhase } from '@/spa/cmsRoute';
import {
defaultFooterData,
defaultHeaderData,
type SpaFooterData,
type SpaHeaderData,
type SpaIconName,
withoutLoginCtas,
} from '@/spa/cmsNavigation';
interface LayoutProps { children: React.ReactNode; header?: SpaHeaderData; footer?: SpaFooterData; }
/* ── SVG icons ─────────────────────────────────────────── */
const IgIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="5"/><circle cx="12" cy="12" r="4"/><circle cx="17.5" cy="6.5" r=".8" fill="currentColor" stroke="none"/></svg>;
const LiIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg>;
const FbIcon = () => <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z"/></svg>;
const ICONS: Record<SpaIconName, () => React.ReactElement> = {
instagram: IgIcon,
linkedin: LiIcon,
facebook: FbIcon,
};
/* ── Tokens ────────────────────────────────────────────── */
const ANTHRA = '#1A2030'; // nav + footer shared bg
const GOLD = '#EFBF04';
const MUTED = '#D0D5DD';
const SEP = 'rgba(239,191,4,0.2)';
const LINE = 'rgba(239,191,4,0.35)';
const FF_DISPLAY = '"IBM Plex Sans", sans-serif';
const FF_BODY = '"Inter", sans-serif';
const HIDE_MEMBERSHIP_FOOTER_ENTRY = true;
const APPLICATION_PHASE_OPEN = '0';
const isMembershipFooterLink = (link?: { label?: string; path?: string }) =>
link?.path === '/mitglied-werden' || link?.label?.trim().toLowerCase() === 'mitglied werden';
/* ── SocialLink helper ─────────────────────────────────── */
const SocialLink: React.FC<{ href:string; icon:SpaIconName; label:string }> = ({ href, icon, label }) => {
const Icon = ICONS[icon] || LiIcon;
return (
<a href={href} target="_blank" rel="noopener noreferrer" aria-label={label}
style={{ color:MUTED, display:'flex', textDecoration:'none', transition:'color .2s' }}
onMouseEnter={e=>(e.currentTarget.style.color=GOLD)}
onMouseLeave={e=>(e.currentTarget.style.color=MUTED)}
><Icon /></a>
);
};
/* ── Component ──────────────────────────────────────────── */
const Layout: React.FC<LayoutProps> = ({ children, header = defaultHeaderData, footer = defaultFooterData }) => {
const NAV = header.navItems;
const SECONDARY = header.secondaryLinks;
const linkedInHeaderSocial = header.socialLinks.filter((item) => item.icon === 'linkedin');
const linkedInFooterSocial = footer.socialLinks.filter((item) => item.icon === 'linkedin');
const HEADER_SOCIAL = linkedInHeaderSocial.length
? linkedInHeaderSocial
: defaultHeaderData.socialLinks.filter((item) => item.icon === 'linkedin');
const FOOTER_SOCIAL = linkedInFooterSocial.length
? linkedInFooterSocial
: defaultFooterData.socialLinks.filter((item) => item.icon === 'linkedin');
const logo = header.logo?.url || defaultHeaderData.logo?.url || '';
const footerLogo = footer.logo?.url || defaultFooterData.logo?.url || '';
const headerCtas = withoutLoginCtas(header.ctas);
const footerCtas = withoutLoginCtas(footer.ctas);
const fallbackHeaderCtas = withoutLoginCtas(defaultHeaderData.ctas);
const fallbackFooterCtas = withoutLoginCtas(defaultFooterData.ctas);
const applicationPhase = useApplicationPhase();
const showFloatingApplicationCta = applicationPhase?.activePhase === APPLICATION_PHASE_OPEN;
const footerMembershipCta = footerCtas[1] || fallbackFooterCtas[1];
const showFooterMembershipCta =
!HIDE_MEMBERSHIP_FOOTER_ENTRY || !isMembershipFooterLink(footerMembershipCta);
const [open, setOpen] = useState(false); // desktop overlay
const [activeKey, setActive] = useState<string | null>(null); // desktop hover subnav
const [mobileOpen, setMobileOpen] = useState(false); // mobile sidebar overlay
const [expandKey, setExpand] = useState<string | null>(null); // mobile accordion
const [mounted, setMounted] = useState(false);
const [showLoader, setShowLoader] = useState(true);
const [formVisible, setFormVisible] = useState(false);
const location = useLocation();
const firstLinkRef = useRef<HTMLAnchorElement>(null);
const isLegalPage = ['/impressum', '/datenschutz'].includes(location.pathname);
const isMobile = useIsMobile();
// Close everything on route change
useEffect(() => {
setTimeout(() => {
setOpen(false);
setMobileOpen(false);
setExpand(null);
}, 0);
if (location.hash) {
setTimeout(() => {
const el = document.querySelector(location.hash);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 120);
} else {
window.scrollTo(0, 0);
}
}, [location]);
// Reset mobile accordion when its overlay closes
useEffect(() => {
if (!mobileOpen) setTimeout(() => setExpand(null), 220);
}, [mobileOpen]);
// Body scroll lock (either overlay)
useEffect(() => {
const anyOpen = open || mobileOpen;
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
if (anyOpen) {
document.body.style.overflow = 'hidden';
document.body.style.paddingRight = open ? `${scrollbarWidth}px` : '';
} else {
document.body.style.overflow = '';
document.body.style.paddingRight = '';
setTimeout(() => setActive(null), 220);
}
return () => {
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}, [open, mobileOpen]);
// Focus first desktop nav link on open
useEffect(() => {
if (open) setTimeout(() => firstLinkRef.current?.focus(), 300);
}, [open]);
// Escape closes either overlay
useEffect(() => {
const h = (e:KeyboardEvent) => {
if (e.key === 'Escape') { if (open) setOpen(false); if (mobileOpen) setMobileOpen(false); }
};
window.addEventListener('keydown', h);
return () => window.removeEventListener('keydown', h);
}, [open, mobileOpen]);
useEffect(() => {
const t = setTimeout(() => setMounted(true), 60);
return () => clearTimeout(t);
}, []);
// Hide floating CTA when the form section (#bewerben) is in view – homepage only
useEffect(() => {
setTimeout(() => setFormVisible(false), 0);
if (location.pathname !== '/') return;
const el = document.getElementById('bewerben');
if (!el) return;
const obs = new IntersectionObserver(
([entry]) => setFormVisible(entry.isIntersecting),
{ threshold: 0.05 }
);
obs.observe(el);
return () => obs.disconnect();
}, [location.pathname]);
return (
<div
style={{ paddingTop: 60, paddingLeft: 0 }}
className="min-h-screen flex flex-col bg-background text-foreground font-body"
>
{showLoader && (
<LoadingScreen
logoAlt={header.logo?.alt || header.overlayLogoAlt}
logoSrc={logo}
onDone={() => setShowLoader(false)}
/>
)}
{/* ── GLOBAL BAYERN CSS ────────────────────────── */}
<style>{`
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #060C14; }
::-webkit-scrollbar-thumb { background: #051A87; }
::-webkit-scrollbar-thumb:hover { background: #03126B; }
* { scrollbar-width: thin; scrollbar-color: #051A87 #060C14; }
::selection { background: #051A87; color: #fff; }
`}</style>
{/* ═══════════════════════════════════════════════════════════ */}
{/* ══ DESKTOP CHROME (≥768px) – original top bar + overlay ══ */}
{/* ═══════════════════════════════════════════════════════════ */}
{!isMobile && (
<>
{/* ── TOP BAR ──────────────────────────────────── */}
<header style={{
position:'fixed', top:0, left:0, right:0, height:60,
background:ANTHRA, borderBottom:`1px solid ${LINE}`,
display:'flex', alignItems:'center',
zIndex:400, userSelect:'none',
transform: mounted ? 'translateY(0)' : 'translateY(-100%)',
transition:'transform 0.45s cubic-bezier(0.22,1,0.36,1)',
}}>
{/* LEFT – logo */}
<div style={{ flex:1, display:'flex', alignItems:'center', padding:'0 24px', height:'100%', borderRight:`1px solid ${LINE}` }}>
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
<Image unoptimized
src={logo}
alt={header.logo?.alt || header.overlayLogoAlt}
style={{ height:38, width:'auto', display:'block', objectFit:'contain' }}
/>
</Link>
</div>
{/* CENTER – hamburger */}
<div style={{ flex:1, display:'flex', alignItems:'center', justifyContent:'center', height:'100%', borderRight:`1px solid ${LINE}`, gap:16 }}>
<span style={{ fontSize:8, color:'rgba(5,26,135,0.5)', letterSpacing:2, userSelect:'none' }}>◆</span>
<button
onClick={() => setOpen(v => !v)}
aria-expanded={open}
aria-label={header.openLabel}
style={{ display:'flex', alignItems:'center', gap:14, background:'none', border:'none', cursor:'pointer', padding:'8px 16px' }}
>
<div style={{ width:20, height:15, position:'relative' }}>
<span style={{ position:'absolute', top:0, left:0, width:'100%', height:1.5, background:MUTED, transformOrigin:'center', transition:'transform .3s ease, opacity .3s ease', transform: open ? 'translateY(6.5px) rotate(45deg)' : 'none' }} />
<span style={{ position:'absolute', top:'50%', left:0, width:'100%', height:1.5, background:MUTED, marginTop:'-0.75px', transition:'opacity .3s', opacity: open ? 0 : 1 }} />
<span style={{ position:'absolute', bottom:0, left:0, width:'100%', height:1.5, background:MUTED, transformOrigin:'center', transition:'transform .3s ease', transform: open ? 'translateY(-6.5px) rotate(-45deg)' : 'none' }} />
</div>
<span style={{ fontSize:9, fontWeight:600, textTransform:'uppercase', letterSpacing:'.22em', color:MUTED, lineHeight:1, fontFamily:FF_DISPLAY }}>{header.menuLabel}</span>
</button>
<span style={{ fontSize:8, color:'rgba(5,26,135,0.5)', letterSpacing:2, userSelect:'none' }}>◆</span>
</div>
{/* RIGHT – social */}
<div style={{ flex:1, display:'flex', alignItems:'center', justifyContent:'flex-end', padding:'0 28px', height:'100%' }}>
<div style={{ display:'flex', gap:14, alignItems:'center' }}>
{HEADER_SOCIAL.map(s => <SocialLink key={s.label} {...s} />)}
</div>
</div>
</header>
{/* ── DESKTOP OVERLAY ──────────────────────────── */}
<div role="dialog" aria-modal="true" aria-label={header.dialogLabel}
style={{
position:'fixed', top:60, left:0, right:0, bottom:0,
background:ANTHRA, zIndex:300,
display:'flex', flexDirection:'column',
opacity: open ? 1 : 0,
pointerEvents: open ? 'all' : 'none',
transition:`opacity ${open ? '.28s' : '.2s'} ease`,
}}
>
<div style={{ display:'flex', flexDirection:'column', height:'100%', overflow:'hidden' }}>
{/* TOP – tagline + logo */}
<div style={{ flex:'1 1 50%', display:'flex', alignItems:'flex-end', padding:'0 80px 48px', position:'relative', minHeight:0, zIndex:1 }}>
<div>
<div style={{ fontSize:12, color:'rgba(255,255,255,.4)', letterSpacing:'.12em', textTransform:'uppercase', marginBottom:12, fontFamily:FF_DISPLAY }}>
{header.overlayKicker}
</div>
<div style={{ fontSize:'clamp(42px,6.5vw,88px)', fontWeight:900, color:'#fff', textTransform:'uppercase', lineHeight:.95, letterSpacing:'-.01em', fontFamily:FF_DISPLAY }}>
{header.overlayHeadline.split('\n').map((line, index) => <React.Fragment key={line + index}>{index > 0 && <br />}{line}</React.Fragment>)}
</div>
</div>
<div style={{ position:'absolute', top:48, right:80 }}>
<Image unoptimized src={logo} alt={header.overlayLogoAlt} style={{ height:40, width:'auto', display:'block', opacity:0.9 }} />
</div>
</div>
{/* BOTTOM – 3 columns */}
<div style={{
flex:'0 0 auto',
display:'grid', gridTemplateColumns:'1fr 1.4fr 1fr',
borderTop:`1px solid ${SEP}`,
}}>
{/* LEFT – main nav */}
<div style={{ borderRight:`1px solid ${SEP}`, padding:'40px 0 40px 60px' }}>
<ul style={{ listStyle:'none' }}>
{NAV.map((item, idx) => (
<li key={item.key} style={{ borderTop:`1px solid ${SEP}` }}>
<Link
ref={idx===0 ? firstLinkRef : undefined}
to={item.path}
onClick={() => setOpen(false)}
onMouseEnter={() => setActive(item.key)}
style={{
display:'flex', alignItems:'center', justifyContent:'space-between',
padding:'14px 40px 14px 0',
textDecoration:'none',
color: activeKey===item.key ? GOLD : '#fff',
fontSize:'clamp(15px,1.4vw,20px)',
fontFamily:FF_DISPLAY,
fontWeight:600, textTransform:'uppercase', letterSpacing:'.03em',
transition:'color .15s, transform .15s, opacity .3s ease-out',
transform: open
? `translateX(${activeKey===item.key ? 6 : 0}px)`
: 'translateX(-16px)',
opacity: open ? 1 : 0,
transitionDelay: open ? `${.04 + idx*.06}s` : '0s',
}}
>
{item.label}
<span style={{ color:GOLD, fontSize:'1.15em', marginLeft:10, flexShrink:0 }}>›</span>
</Link>
</li>
))}
<li style={{ borderTop:`1px solid ${SEP}` }} />
</ul>
</div>
{/* MIDDLE – subnav */}
<div style={{ borderRight:`1px solid ${SEP}`, padding:'40px 48px', position:'relative', minHeight:440 }}>
{NAV.map(item => (
<div key={item.key} style={{
position: 'absolute',
top: 40,
left: 48,
right: 48,
opacity: activeKey===item.key ? 1 : 0,
transform: activeKey===item.key ? 'translateY(0)' : 'translateY(8px)',
transition:'opacity .2s ease, transform .2s ease',
pointerEvents: activeKey===item.key ? 'all' : 'none',
}}>
<div style={{ fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'.22em', color:'rgba(255,255,255,.4)', marginBottom:16, fontFamily:FF_DISPLAY }}>
{header.overviewLabel}
</div>
<div style={{ fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'.1em', color:'#fff', marginBottom:20, fontFamily:FF_DISPLAY }}>
{item.subLabel}
</div>
<ul style={{ listStyle:'none' }}>
{item.sub?.map(sub => (
<li key={`${sub.label}-${sub.path}`} style={{ borderTop:'1px solid rgba(239,191,4,.12)' }}>
<Link to={sub.path} onClick={() => setOpen(false)}
style={{ display:'block', padding:'9px 0', textDecoration:'none', color:MUTED, fontSize: 16, fontFamily:FF_BODY, transition:'color .15s, padding-left .15s' }}
onMouseEnter={e=>{ e.currentTarget.style.color='#fff'; e.currentTarget.style.paddingLeft='8px'; }}
onMouseLeave={e=>{ e.currentTarget.style.color=MUTED; e.currentTarget.style.paddingLeft='0'; }}
>{sub.label}</Link>
</li>
))}
<li style={{ borderTop:'1px solid rgba(239,191,4,.12)' }} />
</ul>
</div>
))}
</div>
{/* RIGHT – utilities */}
<div style={{ padding:'40px 80px 40px 48px', display:'flex', flexDirection:'column', gap:24 }}>
<div style={{ display:'flex', alignItems:'center', gap:20, flexWrap:'wrap' }}>
<div style={{ display:'flex', gap:14 }}>
{HEADER_SOCIAL.map(s => <SocialLink key={s.label} {...s} />)}
</div>
</div>
<div style={{ display:'flex', flexDirection:'column', gap:6 }}>
{SECONDARY.map(item => (
<Link key={item.path} to={item.path} onClick={() => setOpen(false)}
style={{ fontSize: 16, color:MUTED, textDecoration:'none', fontFamily:FF_BODY, transition:'color .15s', width:'fit-content' }}
onMouseEnter={e=>(e.currentTarget.style.color=GOLD)}
onMouseLeave={e=>(e.currentTarget.style.color=MUTED)}
>{item.label}</Link>
))}
</div>
<div style={{ display:'flex', flexDirection:'column', gap:10 }}>
<Link to={headerCtas[0]?.path || fallbackHeaderCtas[0].path} onClick={() => setOpen(false)}
style={{ display:'inline-block', background:'#EFBF04', color:'#101828', fontSize: 15, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', padding:'11px 24px', textDecoration:'none', transition:'background .2s, box-shadow .2s', borderRadius:0, fontFamily:FF_DISPLAY }}
onMouseEnter={e=>{ e.currentTarget.style.background='#FFD130'; e.currentTarget.style.boxShadow='0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
onMouseLeave={e=>{ e.currentTarget.style.background='#EFBF04'; e.currentTarget.style.boxShadow='none'; }}
>{headerCtas[0]?.label || fallbackHeaderCtas[0].label}</Link>
</div>
</div>
</div>
</div>
</div>
</>
)}
{/* ═══════════════════════════════════════════════════════════ */}
{/* ══ MOBILE CHROME (<768px) – top bar + fullscreen overlay ══ */}
{/* ═══════════════════════════════════════════════════════════ */}
{isMobile && (
<>
{/* ── TOP BAR ──────────────────────────────────── */}
<header style={{
position:'fixed', top:0, left:0, right:0, height:60,
background:ANTHRA, borderBottom:`1px solid ${LINE}`,
display:'flex', alignItems:'center', justifyContent:'space-between',
padding:'0 8px 0 20px',
zIndex:400, userSelect:'none',
transform: mounted ? 'translateY(0)' : 'translateY(-100%)',
transition:'transform 0.45s cubic-bezier(0.22,1,0.36,1)',
}}>
{/* Logo */}
<Link to="/" style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
<Image unoptimized src={logo} alt={header.logo?.alt || header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
</Link>
{/* Hamburger */}
<button
onClick={() => setMobileOpen(true)}
aria-expanded={mobileOpen}
aria-label={header.openLabel}
style={{ display:'flex', alignItems:'center', justifyContent:'center', width:48, height:48, background:'none', border:'none', cursor:'pointer', padding:0, flexShrink:0 }}
>
<div style={{ width:24, position:'relative', display:'flex', flexDirection:'column', gap:5 }}>
<span style={{ display:'block', height:2, background:MUTED, borderRadius:2 }} />
<span style={{ display:'block', height:2, background:MUTED, borderRadius:2 }} />
<span style={{ display:'block', height:2, width:'70%', background:GOLD, borderRadius:2 }} />
</div>
</button>
</header>
{/* ── FULLSCREEN OVERLAY ───────────────────────── */}
<div role="dialog" aria-modal="true" aria-label={header.dialogLabel}
style={{
position: 'fixed', inset: 0,
background: ANTHRA,
zIndex: 500,
opacity: mobileOpen ? 1 : 0,
pointerEvents: mobileOpen ? 'all' : 'none',
transition: `opacity ${mobileOpen ? '.28s' : '.2s'} ease`,
display: 'flex', flexDirection: 'column',
overflow: 'hidden',
}}
>
{/* Brand watermark */}
<div aria-hidden style={{
position:'absolute', bottom:24, left:24, right:24,
fontFamily:FF_DISPLAY, fontWeight:900,
fontSize:'clamp(40px,17vw,80px)', lineHeight:.9,
textTransform:'uppercase', letterSpacing:'-.02em',
color:'rgba(255,255,255,.04)',
pointerEvents:'none', userSelect:'none', zIndex:0,
}}>{header.mobileWatermark.split('\n').map((line, index) => <React.Fragment key={line + index}>{index > 0 && <br />}{line}</React.Fragment>)}</div>
{/* Header row: logo + close */}
<div style={{ position:'relative', zIndex:1, display:'flex', alignItems:'center', justifyContent:'space-between', padding:'0 8px 0 20px', height:60, borderBottom:`1px solid ${SEP}`, flexShrink:0 }}>
<Link to="/" onClick={() => setMobileOpen(false)} style={{ display:'flex', alignItems:'center', textDecoration:'none' }}>
<Image unoptimized src={logo} alt={header.overlayLogoAlt} style={{ height:32, width:'auto', display:'block', objectFit:'contain' }} />
</Link>
<button onClick={() => setMobileOpen(false)} aria-label={header.closeLabel}
style={{ display:'flex', alignItems:'center', justifyContent:'center', width:48, height:48, background:'none', border:'none', cursor:'pointer', padding:0, flexShrink:0 }}>
<div style={{ width:26, height:26, position:'relative', display:'flex', alignItems:'center', justifyContent:'center' }}>
<span style={{ position:'absolute', width:22, height:2, background:MUTED, borderRadius:2, transform:'rotate(45deg)' }} />
<span style={{ position:'absolute', width:22, height:2, background:MUTED, borderRadius:2, transform:'rotate(-45deg)' }} />
</div>
</button>
</div>
{/* Scrollable body */}
<div style={{ position:'relative', zIndex:1, flex:1, overflowY:'auto', WebkitOverflowScrolling:'touch' }}>
{/* Accordion nav */}
<nav>
<ul style={{ listStyle:'none', margin:0, padding:0 }}>
{NAV.map((item, idx) => {
const expanded = expandKey === item.key;
return (
<li key={item.key} style={{ borderBottom:`1px solid ${SEP}` }}>
<button
onClick={() => setExpand(k => k === item.key ? null : item.key)}
aria-expanded={expanded}
style={{
display:'flex', alignItems:'center', justifyContent:'space-between', width:'100%',
padding:'18px 24px',
background: expanded ? 'rgba(239,191,4,0.04)' : 'none',
border:'none', cursor:'pointer',
color: expanded ? GOLD : '#fff',
textAlign:'left', minHeight:60,
transition:'color .18s, background .18s',
}}
>
<span style={{ display:'flex', alignItems:'baseline', gap:12 }}>
<span style={{ fontFamily:FF_DISPLAY, fontSize:11, fontWeight:700, color: expanded ? GOLD : 'rgba(255,255,255,.28)', letterSpacing:'.1em', transition:'color .18s' }}>
0{idx + 1}
</span>
<span style={{ fontFamily:FF_DISPLAY, fontSize:19, fontWeight:600, textTransform:'uppercase', letterSpacing:'.02em' }}>
{item.label}
</span>
</span>
<span style={{ color:GOLD, fontSize:22, lineHeight:1, transition:'transform .26s cubic-bezier(0.22,1,0.36,1)', display:'inline-block', transform: expanded ? 'rotate(90deg)' : 'none' }}>›</span>
</button>
{/* Subnav */}
<div style={{ overflow:'hidden', maxHeight: expanded ? 520 : 0, transition:'max-height .3s cubic-bezier(0.22,1,0.36,1)' }}>
{item.sub && (
<ul style={{ listStyle:'none', margin:0, padding:'0 24px 18px 48px' }}>
{item.sub.map(sub => (
<li key={sub.label + sub.path}>
<Link to={sub.path} onClick={() => setMobileOpen(false)}
style={{ display:'flex', alignItems:'center', minHeight:46, padding:'4px 0', textDecoration:'none', color:'rgba(255,255,255,.62)', fontSize:15, fontFamily:FF_BODY }}
>{sub.label}</Link>
</li>
))}
</ul>
)}
</div>
</li>
);
})}
</ul>
</nav>
{/* CTAs */}
<div style={{ padding:'24px 24px 8px', display:'flex', flexDirection:'column', gap:12 }}>
<Link to={headerCtas[0]?.path || fallbackHeaderCtas[0].path} onClick={() => setMobileOpen(false)}
style={{ display:'flex', alignItems:'center', justifyContent:'center', minHeight:52, background:GOLD, color:'#101828', fontSize:14, fontWeight:700, textTransform:'uppercase', letterSpacing:'.12em', textDecoration:'none', fontFamily:FF_DISPLAY }}
>{headerCtas[0]?.label || fallbackHeaderCtas[0].label}</Link>
</div>
{/* Footer zone: social + secondary */}
<div style={{ marginTop:8, padding:'20px 24px 28px', borderTop:`1px solid ${SEP}`, display:'flex', flexDirection:'column', gap:18 }}>
{/* Social row */}
<div style={{ display:'flex', gap:18 }}>
{HEADER_SOCIAL.map(s => <SocialLink key={s.label} {...s} />)}
</div>
{/* Secondary links */}
<div style={{ display:'flex', flexWrap:'wrap', gap:'0 20px' }}>
{SECONDARY.map(item => (
<Link key={item.path} to={item.path} onClick={() => setMobileOpen(false)}
style={{ display:'inline-flex', alignItems:'center', minHeight:40, fontSize:13, color:'rgba(255,255,255,.5)', textDecoration:'none', fontFamily:FF_BODY }}
>{item.label}</Link>
))}
</div>
</div>
</div>
</div>
</>
)}
{/* ── FLOATING CTA ─────────────────────────────── */}
{location.pathname === '/' && showFloatingApplicationCta && !formVisible && !isMobile && (
<Link
to="/teilnahme"
style={{
position: 'fixed', bottom: 32, right: 32, zIndex: 300,
display: 'flex', alignItems: 'center', gap: 10,
background: '#111D55', color: '#fff',
fontFamily: FF_DISPLAY, fontSize: 14, fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.14em',
padding: '14px 22px', textDecoration: 'none',
boxShadow: '0 4px 24px rgba(3,9,58,0.5)',
transform: mounted ? 'translateY(0)' : 'translateY(80px)',
opacity: mounted ? 1 : 0,
transition: 'transform 0.5s cubic-bezier(0.22,1,0.36,1) 0.6s, opacity 0.4s ease 0.6s, background 0.15s, color 0.15s',
}}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#101828'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#111D55'; (e.currentTarget as HTMLElement).style.color = '#fff'; }}
>
{headerCtas[0]?.label || fallbackHeaderCtas[0].label} <ArrowRight size={13} />
</Link>
)}
{/* ── MAIN ─────────────────────────────────────── */}
<main className="flex-1" style={!isLegalPage ? { borderTop: '2px solid #111D55' } : {}}>{children}</main>
{/* ── FOOTER ───────────────────────────────────── */}
<footer style={{
background: '#1A2030',
borderTop: `4px solid ${GOLD}`,
position: 'relative',
overflow: 'hidden',
}}>
{/* Radial glow top-left */}
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'radial-gradient(ellipse 600px 300px at 0% 0%, rgba(239,191,4,0.04) 0%, transparent 60%)',
pointerEvents: 'none',
}} />
{/* ── EDITORIAL HEADLINE BAND ── */}
<div style={{
position: 'relative',
padding: isMobile ? '28px 24px 16px' : '36px 64px 0',
}}>
<h2 className="type-section-title text-role-inverse" style={{
textTransform: 'uppercase',
margin: 0,
whiteSpace: isMobile ? 'normal' : 'nowrap',
}}>
{footer.headlinePrefix} <span style={{ color: GOLD }}>{footer.headlineAccent}</span> {footer.headlineSuffix}
</h2>
{/* Gold rule beneath headline */}
<div style={{
marginTop: 14,
display: 'flex',
alignItems: 'center',
gap: 16,
}}>
<div style={{ width: 36, height: 1, background: GOLD }} />
<span style={{
fontFamily: FF_DISPLAY,
fontSize: 9,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.35em',
color: 'rgba(239,191,4,0.55)',
}}>{footer.sinceLabel}</span>
<div style={{ flex: 1, height: 1, background: 'rgba(239,191,4,0.18)' }} />
</div>
</div>
{/* ── MAIN BODY: asymmetric layout ── */}
<div style={{
position: 'relative',
padding: isMobile ? '24px 24px' : '24px 64px 20px',
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 40 : 0,
alignItems: 'flex-start',
}}>
{/* Decorative year – desktop only */}
{!isMobile && (
<div style={{
position: 'absolute',
left: 52,
top: -28,
fontFamily: FF_DISPLAY,
fontWeight: 900,
fontSize: 160,
lineHeight: 1,
color: 'transparent',
WebkitTextStroke: '1px rgba(239,191,4,0.1)',
userSelect: 'none',
pointerEvents: 'none',
zIndex: 0,
}}>{footer.year}</div>
)}
{/* ── LEFT COLUMN: copy + CTAs ── */}
<div style={{
position: 'relative',
zIndex: 1,
flex: isMobile ? 'none' : '0 0 420px',
maxWidth: isMobile ? '100%' : 420,
}}>
{/* Logo */}
<Image unoptimized
src={footerLogo}
alt={footer.logo?.alt || footer.logoAlt}
style={{ width: 140, height: 'auto', marginBottom: 10, opacity: 0.9 }}
/>
<p className="type-small text-role-inverse-secondary" style={{
margin: '0 0 16px 0',
maxWidth: 300,
}}>
{footer.description}
</p>
{/* CTA stack */}
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: 8,
}}>
<Link
to={footerCtas[0]?.path || fallbackFooterCtas[0].path}
style={{
fontFamily: FF_DISPLAY,
fontSize: 16,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.12em',
color: '#101828',
background: GOLD,
padding: '10px 22px',
textDecoration: 'none',
display: 'inline-block',
borderRadius: 2,
transition: 'background 0.15s, box-shadow 0.2s',
}}
onMouseEnter={e => {
e.currentTarget.style.background = '#FFD130';
e.currentTarget.style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)';
}}
onMouseLeave={e => {
e.currentTarget.style.background = GOLD;
e.currentTarget.style.boxShadow = 'none';
}}
>{footerCtas[0]?.label || fallbackFooterCtas[0].label}</Link>
{showFooterMembershipCta && (
<Link
to={footerCtas[1]?.path || fallbackFooterCtas[1].path}
style={{
fontFamily: FF_DISPLAY,
fontSize: 16,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.12em',
color: MUTED,
border: '1.5px solid rgba(239,191,4,0.35)',
padding: '8px 22px',
textDecoration: 'none',
display: 'inline-block',
borderRadius: 2,
transition: 'border-color 0.15s, color 0.15s',
}}
onMouseEnter={e => {
e.currentTarget.style.borderColor = GOLD;
e.currentTarget.style.color = GOLD;
}}
onMouseLeave={e => {
e.currentTarget.style.borderColor = 'rgba(239,191,4,0.35)';
e.currentTarget.style.color = MUTED;
}}
>{footerCtas[1]?.label || fallbackFooterCtas[1].label}</Link>
)}
<Link
to={footerCtas[2]?.path || fallbackFooterCtas[2].path}
style={{
fontFamily: FF_DISPLAY,
fontSize: 16,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.12em',
color: 'rgba(255,255,255,0.4)',
textDecoration: 'none',
paddingTop: 4,
transition: 'color 0.15s',
}}
onMouseEnter={e => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(255,255,255,0.4)'; }}
>{footerCtas[2]?.label || fallbackFooterCtas[2].label}</Link>
</div>
</div>
{/* ── RIGHT COLUMN: nav links ── */}
<div style={{
position: 'relative',
zIndex: 1,
flex: 1,
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
justifyContent: isMobile ? 'flex-start' : 'flex-end',
alignItems: 'flex-start',
gap: isMobile ? 32 : 0,
paddingLeft: isMobile ? 0 : 64,
}}>
{/* Nav pair container with gold vertical separator */}
<div style={{
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
gap: isMobile ? 32 : 0,
alignItems: 'flex-start',
}}>
{footer.linkColumns.map((column, index) => (
<React.Fragment key={column.label + index}>
<div style={{ minWidth: isMobile ? 'auto' : index === footer.linkColumns.length - 1 ? 120 : 160 }}>
<p className="type-eyebrow text-role-inverse-muted" style={{
margin: '0 0 10px 0',
}}>{column.label}</p>
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 8 }}>
{column.links.map(item => (
<li key={item.path + item.label}>
<Link
to={item.path}
style={{
fontFamily: FF_BODY,
fontSize: 16,
color: 'rgba(255,255,255,0.55)',
textDecoration: 'none',
transition: 'color 0.2s',
display: 'inline-block',
}}
onMouseEnter={e => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={e => { e.currentTarget.style.color = 'rgba(255,255,255,0.55)'; }}
>{item.label}</Link>
</li>
))}
</ul>
</div>
{!isMobile && index < footer.linkColumns.length - 1 && (
<div style={{
width: 1,
alignSelf: 'stretch',
background: 'rgba(239,191,4,0.22)',
margin: '0 40px',
flexShrink: 0,
}} />
)}
</React.Fragment>
))}
</div>
</div>
</div>
{/* ── PARTNER STRIP ── */}
<div style={{
borderTop: '1px solid rgba(239,191,4,0.15)',
borderBottom: '1px solid rgba(239,191,4,0.15)',
padding: isMobile ? '20px 24px' : '20px 64px',
display: 'flex',
alignItems: 'center',
gap: isMobile ? 16 : 28,
flexWrap: 'wrap',
}}>
<span style={{
fontFamily: FF_DISPLAY,
fontSize: 8,
fontWeight: 700,
textTransform: 'uppercase',
letterSpacing: '0.32em',
color: 'rgba(255,255,255,0.22)',
flexShrink: 0,
}}>{footer.partnerLabel}</span>
<div style={{ width: 1, height: 20, background: 'rgba(239,191,4,0.2)', flexShrink: 0 }} />
{footer.partnerLogos.map((partner, index) => (
partner.image?.url ? (
<Image unoptimized
key={`${partner.alt}-${index}`}
src={partner.image.url}
alt={partner.image.alt || partner.alt}
style={{ height: 32, width: 'auto', opacity: 0.5, filter: 'brightness(2)', display: 'block' }}
/>
) : null
))}
</div>
{/* ── BOTTOM BAR ── */}
<div style={{
padding: isMobile ? '20px 24px' : '20px 64px',
display: 'flex',
flexDirection: isMobile ? 'column' : 'row',
alignItems: isMobile ? 'flex-start' : 'center',
justifyContent: 'space-between',
gap: isMobile ? 16 : 0,
}}>
<p className="type-caption text-role-inverse-muted" style={{
textTransform: 'uppercase',
letterSpacing: '0.2em',
margin: 0,
}}>
{footer.copyright}
</p>
<div style={{ display: 'flex', gap: 20, alignItems: 'center' }}>
{FOOTER_SOCIAL.map(s => <SocialLink key={s.label} {...s} />)}
</div>
</div>
</footer>
</div>
);
};
export default Layout;