Files
bmp-website-2026/src/spa/pages/Netzwerk.tsx

920 lines
58 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.
import React, { useState, useEffect, useRef } from 'react';
import { Link } from '@/spa/router';
import { X, ArrowRight, Globe, Linkedin, Building2, MapPin, ExternalLink, Play } from 'lucide-react';
import SponsoringForm from '@/spa/components/forms/SponsoringForm';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { homeContent } from '@/spa/homeContent';
import { selectJuryDocs, sortJuryMembersQuoteFirst } from '@/spa/juryMembers';
import { netzwerkContent } from '@/spa/netzwerkContent';
import Image from '@/spa/components/ui/UnoptimizedImage'
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo';
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
const NAVY = '#111D55';
const GOLD = '#EFBF04';
type NetzwerkCms = Omit<Partial<typeof netzwerkContent>, 'hero' | 'jury' | 'sponsoring'> & {
hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown }
jury?: Partial<Omit<typeof netzwerkContent.jury, 'members'>> & { members?: unknown[] | null }
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
}
type HomeVideoCms = {
hero?: {
videoLabel?: string | null
}
videoModal?: {
video?: unknown
title?: string | null
description?: string | null
closeLabel?: string | null
}
}
type PatronPerson = (typeof netzwerkContent.patronage.people)[number] & { imageUpload?: unknown }
type JuryMember = (typeof netzwerkContent.jury.members)[number] & {
active?: boolean | null
imageAlt?: string | null
imageUpload?: unknown
organization?: string | null
quote?: string | null
sortOrder?: number | null
}
type Partner = (typeof netzwerkContent.partners.items)[number] &
PartnerLogoData & {
active?: boolean | null
heroImg?: string
linkedin?: string
showOnHomepage?: boolean | null
sortOrder?: number | null
stableId?: string | null
tier: 1 | 2 | 3
}
type PartnerTier = (typeof netzwerkContent.partners.tiers)[number]
type ExpertiseRow = (typeof netzwerkContent.expertise.rows)[number]
type StatItem = (typeof netzwerkContent.juryIntro.stats)[number]
type BenefitItem = (typeof netzwerkContent.sponsoring.benefits)[number]
type PartnerModalCopy = typeof netzwerkContent.partners.modal
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
const fallbackArray = <T,>(value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
const managedImage = (upload: unknown, fallback: unknown) => mediaUrl(upload, fallbackText(fallback, ''))
const deprecatedPartnerDescription = 'Partner in drei Kategorien: Hauptsponsoren, Medienpartner und weitere Sponsoren. Klicken für Details.'
const visiblePartnerDescription = (value: unknown) => {
const text = fallbackText(value, netzwerkContent.partners.description)
return text.trim() === deprecatedPartnerDescription ? netzwerkContent.partners.description : text
}
const juryImageFallback = '/images/preistraeger-placeholder.svg'
const sortedPartners = <T extends PartnerLogoData>(partners: T[]) =>
partners
.map((partner, index) => ({ partner, index }))
.sort((a, b) => Number(a.partner.sortOrder ?? a.index) - Number(b.partner.sortOrder ?? b.index))
.map(({ partner }) => partner)
const normalizeJuryMember = (member: CmsRouteDoc): JuryMember => {
const organization = fallbackText(member.organization, '')
const rawRole = fallbackText(member.role, '')
const role = rawRole && organization && rawRole !== organization ? `${rawRole} / ${organization}` : fallbackText(rawRole, organization)
const image = member.image ?? member.imageUpload
return {
name: fallbackText(member.name, ''),
role,
bio: fallbackText(member.bio, ''),
image: mediaUrl(image, juryImageFallback),
imageAlt: fallbackText(member.imageAlt, fallbackText(member.name, 'Jury-Mitglied')),
imageUpload: image,
organization,
quote: fallbackText(member.quote, ''),
sortOrder: typeof member.sortOrder === 'number' ? member.sortOrder : undefined,
active: member.active !== false,
}
}
const hasJuryText = (value: unknown) => typeof value === 'string' && value.trim().length > 0
function Lines({ text, highlight }: { text: string; highlight?: string }) {
return (
<>
{text.split('\n').map((line, i) => {
const highlighted = highlight && line.includes(highlight)
return (
<React.Fragment key={`${line}-${i}`}>
{i > 0 && <br />}
{highlighted ? (
<>
{line.replace(highlight, '')}<span style={{ color: GOLD }}>{highlight}</span>
</>
) : line}
</React.Fragment>
)
})}
</>
)
}
// ── PersonCard (Schirmherrschaft) ─────────────────────────────────────────────
interface PersonCardProps {
imgSrc: string;
imgAlt: string;
name: string;
titleLine1: string;
titleLine2: string;
institution: string;
borderRight?: boolean;
}
function PersonCard({ imgSrc, imgAlt, name, titleLine1, titleLine2, institution, borderRight }: PersonCardProps) {
const [hovered, setHovered] = useState(false);
const isMobileCard = useIsMobile();
return (
<div
style={{ borderRight: borderRight && !isMobileCard ? '1px solid rgba(255,255,255,0.08)' : 'none', display: 'flex', flexDirection: 'column', ...(isMobileCard ? { minWidth: '85vw', maxWidth: '85vw', flexShrink: 0, scrollSnapAlign: 'start', borderRadius: 16, overflow: 'hidden' } : {}) }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<div style={{ position: 'relative', height: isMobileCard ? 250 : 320, overflow: 'hidden', width: '100%' }}>
<Image unoptimized
src={imgSrc}
alt={imgAlt}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', filter: hovered ? 'grayscale(0%)' : 'grayscale(100%)', transition: 'filter 0.55s ease, transform 0.55s ease', transform: hovered ? 'scale(1.04)' : 'scale(1)' }}
/>
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.8) 0%, transparent 60%)', pointerEvents: 'none' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, background: GOLD }} />
</div>
<div style={{ padding: isMobileCard ? '28px 24px' : '36px 40px', background: NAVY, flex: 1 }}>
<div style={{ fontFamily: FF, fontSize: 20, fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 10 }}>{name}</div>
<div style={{ fontFamily: FF, fontSize: 17, fontWeight: 400, color: `${GOLD}CC`, lineHeight: 1.6, marginBottom: 2 }}>{titleLine1}</div>
<div style={{ fontFamily: FF, fontSize: 17, fontWeight: 400, color: 'rgba(255,255,255,0.5)', lineHeight: 1.6 }}>{titleLine2}</div>
<div style={{ fontFamily: FF, fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.2em', color: 'rgba(255,255,255,0.3)', marginTop: 12 }}>{institution}</div>
</div>
</div>
);
}
// ── JuryCard ──────────────────────────────────────────────────────────────────
const JuryCard: React.FC<{ member: JuryMember }> = ({ member }) => {
const [hovered, setHovered] = useState(false);
const isMobileCard = useIsMobile();
const bio = hasJuryText(member.bio) ? member.bio : '';
const quote = hasJuryText(member.quote) ? member.quote : '';
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ display: 'flex', flexDirection: 'column', height: '100%', cursor: 'default', background: hovered ? 'rgba(255,255,255,0.04)' : NAVY, transition: 'background 0.2s', ...(isMobileCard ? { minWidth: '72vw', maxWidth: '72vw', flexShrink: 0, scrollSnapAlign: 'start', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 16, overflow: 'hidden' } : { minWidth: 0, border: '1px solid rgba(255,255,255,0.07)' }) }}
>
<div style={{ position: 'relative', overflow: 'hidden', height: isMobileCard ? 142 : 170, flexShrink: 0, background: 'rgba(255,255,255,0.03)' }}>
<Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={fallbackText(member.imageAlt, member.name)} style={{ width: '100%', height: '100%', objectFit: 'contain', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '40%', background: 'linear-gradient(to top, rgba(3,9,58,0.65), transparent)' }} />
</div>
<div style={{ padding: isMobileCard ? '18px 18px 22px' : '20px 20px 24px', flex: 1, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
<div style={{ fontFamily: FF, fontSize: isMobileCard ? 16 : 17, fontWeight: 700, color: '#fff', lineHeight: 1.15, marginBottom: 5, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>{member.name}</div>
<div style={{ fontFamily: FF, fontSize: isMobileCard ? 9 : 10, color: GOLD, textTransform: 'uppercase', letterSpacing: isMobileCard ? '0.1em' : '0.14em', fontWeight: 700, lineHeight: 1.35, marginBottom: 10, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>{member.role}</div>
<div style={{ width: hovered ? '100%' : 20, height: 1, background: GOLD, opacity: 0.4, transition: 'width 0.4s ease', marginBottom: 10 }} />
{bio ? (
<div style={{ fontFamily: FB, fontSize: isMobileCard ? 14 : 15, color: 'rgba(255,255,255,0.55)', lineHeight: 1.55, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>{bio}</div>
) : null}
{quote ? (
<div style={{ borderLeft: `2px solid ${GOLD}`, color: 'rgba(255,255,255,0.72)', fontFamily: FB, fontSize: isMobileCard ? 13 : 14, fontStyle: 'italic', lineHeight: 1.5, marginTop: 14, paddingLeft: 10, minWidth: 0, overflowWrap: 'anywhere', wordBreak: 'break-word' }}>
{quote}
</div>
) : null}
</div>
</div>
);
};
// ── JuryChairCard (Vorsitz prominent) ───────────────────────────────────────
const JuryChairCard: React.FC<{ member: JuryMember; badge: string }> = ({ member, badge }) => {
const [hovered, setHovered] = useState(false);
const isMobileCard = useIsMobile();
const bio = hasJuryText(member.bio) ? member.bio : '';
const quote = hasJuryText(member.quote) ? member.quote : '';
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
display: 'grid',
gridTemplateColumns: isMobileCard ? '1fr' : '380px 1fr',
gap: 0,
background: 'rgba(255,255,255,0.03)',
border: `1px solid ${GOLD}40`,
borderRadius: 20,
overflow: 'hidden',
}}
>
<div style={{ position: 'relative', overflow: 'hidden', aspectRatio: isMobileCard ? '16/10' : '3/4', minHeight: isMobileCard ? 240 : 'auto', background: 'rgba(255,255,255,0.03)' }}>
<Image unoptimized src={managedImage(member.imageUpload, member.image)} alt={fallbackText(member.imageAlt, member.name)} style={{ width: '100%', height: '100%', objectFit: 'contain', objectPosition: 'center top', filter: hovered ? 'none' : 'grayscale(100%)', transition: 'filter 0.5s', display: 'block' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: '45%', background: 'linear-gradient(to top, rgba(3,9,58,0.7), transparent)' }} />
</div>
<div style={{ padding: isMobileCard ? '32px 28px 36px' : '56px 56px', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
<span style={{ alignSelf: 'flex-start', fontFamily: FF, fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.18em', color: NAVY, background: GOLD, padding: '7px 16px', borderRadius: 999, marginBottom: 24 }}>
{badge}
</span>
<div style={{ fontFamily: FF, fontSize: isMobileCard ? 26 : 34, fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.05, marginBottom: 10 }}>{member.name}</div>
<div style={{ fontFamily: FF, fontSize: 13, color: GOLD, textTransform: 'uppercase', letterSpacing: '0.18em', fontWeight: 700, marginBottom: 24 }}>{member.role}</div>
<div style={{ width: 48, height: 2, background: GOLD, opacity: 0.5, marginBottom: 24 }} />
{bio ? (
<div style={{ fontFamily: FB, fontSize: isMobileCard ? 18 : 21, color: 'rgba(255,255,255,0.6)', lineHeight: 1.7, maxWidth: 620 }}>{bio}</div>
) : null}
{quote ? (
<div style={{ borderLeft: `3px solid ${GOLD}`, color: 'rgba(255,255,255,0.78)', fontFamily: FB, fontSize: isMobileCard ? 17 : 19, fontStyle: 'italic', lineHeight: 1.65, marginTop: 24, maxWidth: 620, paddingLeft: 18 }}>
{quote}
</div>
) : null}
</div>
</div>
);
};
// ── PartnerCell ───────────────────────────────────────────────────────────────
// ── SponsorCell (tiered) ──────────────────────────────────────────────────────
const SponsorCell: React.FC<{ partner: Partner; variant: 'lg' | 'md' | 'sm'; idx: number; cols: number; detailLabel: string; onClick: () => void }> = ({ partner, variant, idx, cols, detailLabel, onClick }) => {
const [hovered, setHovered] = useState(false);
const isMobileCell = useIsMobile();
const lg = variant === 'lg';
const sm = variant === 'sm';
const lastCol = (idx % cols) === cols - 1;
const logoHeight = isMobileCell ? (lg ? 76 : sm ? 54 : 62) : lg ? 92 : sm ? 62 : 76;
return (
<div
onClick={onClick}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
padding: isMobileCell ? '26px 20px' : lg ? '44px 38px 40px' : sm ? '30px 28px' : '36px 32px',
borderRight: !lastCol && !isMobileCell ? '1px solid rgba(3,9,58,0.08)' : 'none',
borderBottom: '1px solid rgba(3,9,58,0.08)',
display: 'flex', flexDirection: 'column', gap: lg ? 18 : 16,
cursor: 'pointer',
background: hovered ? '#E8E5E4' : 'transparent',
transition: 'background 0.15s',
position: 'relative',
}}
>
<div style={{ height: logoHeight, display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', minWidth: 0 }}>
<PartnerLogoMark partner={partner} />
</div>
<div style={{ width: hovered ? 56 : 24, height: 1, background: GOLD, transition: 'width 0.3s ease' }} />
<div style={{ fontFamily: FF, fontSize: lg ? 22 : 18, fontWeight: 700, color: '#101828', letterSpacing: '-0.01em', lineHeight: 1.15 }}>{partner.name}</div>
{!sm && <div style={{ fontFamily: FB, fontSize: lg ? 17 : 16, color: 'rgba(16,24,40,0.45)', lineHeight: 1.6, flexGrow: 1 }}>{partner.desc}</div>}
<div style={{ fontFamily: FF, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.12em', color: GOLD, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 4 }}>
{detailLabel} <ArrowRight size={10} />
</div>
</div>
);
};
// ── PartnerModal ─────────────────────────────────────────────────────────────
function PartnerModal({ partner, copy, onClose }: { partner: Partner; copy: PartnerModalCopy; onClose: () => void }) {
const accentColor = GOLD;
useEffect(() => {
document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}, []);
return (
<div
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
style={{
position: 'fixed', inset: 0, zIndex: 500,
background: 'rgba(0,0,0,0.4)',
backdropFilter: 'blur(10px)',
WebkitBackdropFilter: 'blur(10px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 24, overflowY: 'auto',
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: '#fff', width: '100%', maxWidth: 720,
boxShadow: '0 32px 80px rgba(0,0,0,0.35)',
overflow: 'hidden', flexShrink: 0,
maxHeight: '92vh', overflowY: 'auto',
}}
>
{/* ── Hero ── */}
<div style={{ position: 'relative', height: 220, flexShrink: 0, overflow: 'visible' }}>
{/* Background */}
{partner.heroImg
? <Image unoptimized src={partner.heroImg} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
: <div style={{ position: 'absolute', inset: 0, background: `linear-gradient(135deg, ${accentColor}33 0%, ${NAVY} 70%)` }} />
}
{/* Gradient to white */}
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0) 35%, rgba(255,255,255,0.6) 75%, #fff 100%)' }} />
{/* Social + Close top right */}
<div style={{ position: 'absolute', top: 16, right: 20, display: 'flex', gap: 8, alignItems: 'center' }}>
{partner.linkedin && (
<a href={partner.linkedin} target="_blank" rel="noopener noreferrer" style={{
width: 36, height: 36, borderRadius: '50%',
background: 'rgba(255,255,255,0.92)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: NAVY, textDecoration: 'none',
boxShadow: '0 2px 8px rgba(0,0,0,0.18)',
}}>
<Linkedin size={14} />
</a>
)}
{partner.website && (
<a href={`https://${partner.website}`} target="_blank" rel="noopener noreferrer" style={{
width: 36, height: 36, borderRadius: '50%',
background: 'rgba(255,255,255,0.92)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: NAVY, textDecoration: 'none',
boxShadow: '0 2px 8px rgba(0,0,0,0.18)',
}}>
<Globe size={14} />
</a>
)}
<button onClick={onClose} style={{
width: 36, height: 36, borderRadius: '50%',
background: 'rgba(255,255,255,0.92)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
border: 'none', cursor: 'pointer',
boxShadow: '0 2px 8px rgba(0,0,0,0.18)', color: NAVY,
}}>
<X size={15} />
</button>
</div>
{/* Logo box bottom left */}
<div style={{
position: 'absolute', bottom: -38, left: 28,
width: 'min(260px, calc(100% - 56px))', minWidth: 112, height: 112, background: '#fff',
borderRadius: 12, boxShadow: '0 4px 24px rgba(0,0,0,0.14)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
boxSizing: 'border-box',
flexShrink: 0,
overflow: 'hidden',
padding: 14, zIndex: 2,
}}>
<div style={{ width: '100%', height: '100%', minWidth: 0, overflow: 'hidden' }}>
<PartnerLogoMark partner={partner} />
</div>
</div>
</div>
{/* ── Content ── */}
<div style={{ padding: '72px 32px 32px' }}>
<h2 className="type-subtitle text-role-heading" style={{
margin: '0 0 10px',
}}>
{partner.name}
</h2>
<p className="type-body text-role-secondary" style={{ margin: '0 0 24px' }}>
{partner.desc}
</p>
{/* 3 info cards */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10, marginBottom: 28 }}>
{[
{ Icon: Building2, label: fallbackText(copy.industryLabel, netzwerkContent.partners.modal.industryLabel), value: partner.industry },
{ Icon: MapPin, label: fallbackText(copy.locationLabel, netzwerkContent.partners.modal.locationLabel), value: partner.location },
{ Icon: Globe, label: fallbackText(copy.webLabel, netzwerkContent.partners.modal.webLabel), value: partner.website },
].map(({ Icon, label, value }) => (
<div key={label} style={{ background: 'hsl(220,40%,97%)', padding: '14px 16px', borderRadius: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }}>
<Icon size={11} color="rgba(16,24,40,0.35)" />
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.14em', color: 'rgba(16,24,40,0.35)' }}>{label}</span>
</div>
<div style={{ fontFamily: FB, fontSize: 17, fontWeight: 600, color: '#101828', lineHeight: 1.4, wordBreak: 'break-word' }}>{value}</div>
</div>
))}
</div>
{/* Separator */}
<div style={{ height: 1, background: 'rgba(16,24,40,0.08)', marginBottom: 28 }} />
{/* Über section */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
<div style={{ width: 4, height: 20, background: accentColor, borderRadius: 2, flexShrink: 0 }} />
<span style={{ fontFamily: FF, fontSize: 17, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: '#101828' }}>
{fallbackText(copy.aboutLabel, netzwerkContent.partners.modal.aboutLabel)}
</span>
</div>
<p className="type-body text-role-secondary" style={{ marginBottom: 36 }}>
{partner.fullDesc}
</p>
{/* CTAs */}
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
{partner.website && (
<a
href={`https://${partner.website}`}
target="_blank" rel="noopener noreferrer"
style={{
padding: '13px 28px', background: GOLD, color: '#101828',
fontFamily: FF, fontSize: 11, fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.1em',
textDecoration: 'none',
display: 'inline-flex', alignItems: 'center', gap: 8,
transition: 'background 0.15s, box-shadow 0.2s',
}}
onMouseEnter={e => { const el = e.currentTarget as HTMLElement; el.style.background = '#FFD130'; el.style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.background = GOLD; el.style.boxShadow = 'none'; }}
>
{fallbackText(copy.websiteLabel, netzwerkContent.partners.modal.websiteLabel)} <ExternalLink size={12} />
</a>
)}
<button
onClick={onClose}
style={{
padding: '13px 28px', background: 'transparent',
color: 'rgba(16,24,40,0.45)', border: '1px solid rgba(16,24,40,0.12)',
fontFamily: FF, fontSize: 11, fontWeight: 700,
textTransform: 'uppercase', letterSpacing: '0.1em',
cursor: 'pointer', transition: 'all 0.15s',
}}
onMouseEnter={e => { const el = e.currentTarget as HTMLElement; el.style.background = NAVY; el.style.color = '#fff'; el.style.borderColor = NAVY; }}
onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.background = 'transparent'; el.style.color = 'rgba(16,24,40,0.45)'; el.style.borderColor = 'rgba(16,24,40,0.12)'; }}
>
{fallbackText(copy.closeLabel, netzwerkContent.partners.modal.closeLabel)}
</button>
</div>
</div>
{/* ── Footer strip ── */}
<div style={{
background: 'hsl(220,20%,96%)', padding: '14px 32px',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
borderTop: '1px solid rgba(16,24,40,0.06)',
}}>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.12em', color: 'rgba(16,24,40,0.3)' }}>
{fallbackText(copy.footerTitle, netzwerkContent.partners.modal.footerTitle)}
</span>
</div>
</div>
</div>
);
}
// ── Netzwerk ──────────────────────────────────────────────────────────────────
const Netzwerk: React.FC = () => {
const isMobile = useIsMobile();
const [selectedPartner, setSelectedPartner] = useState<Partner | null>(null);
const [showGreetingVideo, setShowGreetingVideo] = useState(false);
const greetingVideoRef = useRef<HTMLVideoElement>(null);
const cms = (useCmsRoute()?.doc?.netzwerk || {}) as NetzwerkCms;
const homePage = useCmsCollection('pages').find((page) => page.spaPath === '/' || page.slug === 'startseite' || page.slug === 'home');
const cmsPartners = useCmsCollection('partners') as unknown as Partner[];
const cmsJuryMembers = useCmsCollection('jury-mitglieder');
const home = (homePage?.home || {}) as HomeVideoCms;
const homeVideoModal = home.videoModal || {};
const hero = { ...netzwerkContent.hero, ...(cms.hero || {}) };
const patronage = { ...netzwerkContent.patronage, ...(cms.patronage || {}) };
const juryIntro = { ...netzwerkContent.juryIntro, ...(cms.juryIntro || {}) };
const jury = { ...netzwerkContent.jury, ...(cms.jury || {}) };
const expertise = { ...netzwerkContent.expertise, ...(cms.expertise || {}) };
const partnerSection = { ...netzwerkContent.partners, ...(cms.partners || {}) };
const partnerModal = { ...netzwerkContent.partners.modal, ...((cms.partners as typeof netzwerkContent.partners | undefined)?.modal || {}) };
const sponsoring = { ...netzwerkContent.sponsoring, ...(cms.sponsoring || {}) };
const patronPeople = fallbackArray<PatronPerson>(patronage.people, netzwerkContent.patronage.people);
const cmsJurySelection = cms.jury?.members;
const hasCmsJurySelection = cmsJurySelection !== undefined;
const sourcedJuryDocs = selectJuryDocs(cmsJurySelection, cmsJuryMembers);
const normalizedJuryMembers = sourcedJuryDocs
.map(normalizeJuryMember)
.filter((member) => member.active !== false && member.name);
const jurySourceMembers: JuryMember[] = normalizedJuryMembers.length || hasCmsJurySelection
? normalizedJuryMembers
: netzwerkContent.jury.members;
const juryMembers = sortJuryMembersQuoteFirst(jurySourceMembers);
const expertiseRows = fallbackArray<ExpertiseRow>(expertise.rows, netzwerkContent.expertise.rows);
const partners = sortedPartners(cmsPartners)
.filter((partner) => partner.active !== false)
.map((partner) => ({
...partner,
id: partner.stableId || partner.id,
}));
const partnerTiers = fallbackArray<PartnerTier>(partnerSection.tiers, netzwerkContent.partners.tiers);
const mobileStats = fallbackArray<StatItem>(juryIntro.stats, netzwerkContent.juryIntro.stats);
const desktopStats = fallbackArray<StatItem>(juryIntro.desktopStats, netzwerkContent.juryIntro.desktopStats);
const sponsorBenefits = fallbackArray<BenefitItem>(sponsoring.benefits, netzwerkContent.sponsoring.benefits);
const greetingVideoSrc = mediaUrl(homeVideoModal.video, '');
const greetingVideoTitle = fallbackText(homeVideoModal.title, homeContent.videoModal.title);
const greetingVideoLabel = fallbackText(home.hero?.videoLabel, homeContent.hero.videoLabel);
const greetingVideoCloseLabel = fallbackText(homeVideoModal.closeLabel, homeContent.videoModal.closeLabel);
useEffect(() => {
if (!showGreetingVideo || !greetingVideoSrc) return;
greetingVideoRef.current?.play().catch(() => {
// Browser autoplay rules can still require the native control click.
});
}, [showGreetingVideo, greetingVideoSrc]);
return (
<div className="animate-fade-in">
{/* ── 1. HERO ──────────────────────────────────────────────────────────── */}
<section style={{ position: 'relative', minHeight: '72vh', display: 'flex', alignItems: 'flex-end', overflow: 'hidden', background: '#060C14' }}>
<Image unoptimized src={mediaUrl(hero.image, `/images/${netzwerkContent.hero.imageFilename}`)} alt={mediaAlt(hero.image, fallbackText(hero.imageAlt, netzwerkContent.hero.imageAlt))} style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 30%' }} />
<div style={{ position: 'absolute', inset: 0, background: isMobile ? 'linear-gradient(to top, rgba(2,9,48,0.95) 0%, rgba(2,9,48,0.72) 38%, rgba(2,9,48,0.22) 72%, transparent 100%)' : 'linear-gradient(to right, #020930 0%, rgba(2,9,48,0.90) 38%, rgba(2,9,48,0.18) 65%, transparent 100%)' }} />
<div style={{ position: 'absolute', top: 0, left: 80, width: 2, height: '100%', background: `linear-gradient(to bottom, transparent, ${GOLD}, transparent)`, opacity: 0.4 }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'linear-gradient(to right, #EFBF04, rgba(239,191,4,0.3), transparent)', zIndex: 2 }} />
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '0 24px 48px' : '0 80px 88px', maxWidth: 860 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 28 }}>
<div style={{ width: 40, height: 2, background: GOLD }} />
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: GOLD }}>{fallbackText(hero.eyebrow, netzwerkContent.hero.eyebrow)}</span>
</div>
<h1 className="type-display text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 28px' }}>
<Lines text={fallbackText(hero.heading, netzwerkContent.hero.heading)} highlight={fallbackText(hero.highlight, netzwerkContent.hero.highlight)} />
</h1>
<p className="type-body text-role-inverse-secondary" style={{ maxWidth: 560, margin: 0 }}>
{fallbackText(hero.description, netzwerkContent.hero.description)}
</p>
</div>
</section>
{/* ── 2. SCHIRMHERRSCHAFT ──────────────────────────────────────────────── */}
<section id="schirmherrschaft" style={{ background: NAVY }}>
<div style={{ padding: isMobile ? '48px 24px 32px' : '80px 80px 56px' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: GOLD, display: 'block', marginBottom: 16 }}>{fallbackText(patronage.eyebrow, netzwerkContent.patronage.eyebrow)}</span>
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 20px' }}>{fallbackText(patronage.heading, netzwerkContent.patronage.heading)}</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 14 }} />
<p className="type-body text-role-inverse-secondary" style={{ margin: 0 }}>{fallbackText(patronage.description, netzwerkContent.patronage.description)}</p>
</div>
<div style={{ display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : '1fr 1fr', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 14 : 0, padding: isMobile ? '0 24px 28px' : 0, WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}>
{patronPeople.map((person, index) => (
<PersonCard
key={`${person.name}-${index}`}
imgSrc={managedImage(person.imageUpload, fallbackText(person.image, netzwerkContent.patronage.people[index]?.image || ''))}
imgAlt={fallbackText(person.imageAlt, person.name)}
name={fallbackText(person.name, netzwerkContent.patronage.people[index]?.name || '')}
titleLine1={fallbackText(person.titleLine1, netzwerkContent.patronage.people[index]?.titleLine1 || '')}
titleLine2={fallbackText(person.titleLine2, netzwerkContent.patronage.people[index]?.titleLine2 || '')}
institution={fallbackText(person.institution, netzwerkContent.patronage.people[index]?.institution || '')}
borderRight={index === 0}
/>
))}
</div>
{/* Grußwort Ilse Aigner */}
<div style={{ background: 'rgba(255,255,255,0.03)', borderTop: '1px solid rgba(255,255,255,0.07)' }}>
<div style={{ padding: isMobile ? '32px 24px' : '64px 80px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '40% 60%', gap: isMobile ? '24px 0' : '0 64px', alignItems: 'flex-start' }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.3em', textTransform: 'uppercase', color: GOLD, display: 'block', marginBottom: 8 }}>{fallbackText(patronage.greetingEyebrow, netzwerkContent.patronage.greetingEyebrow)}</span>
<div style={{ fontFamily: FF, fontSize: 11, fontWeight: 400, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'rgba(255,255,255,0.3)', marginBottom: 16 }}>{fallbackText(patronage.greetingRole, netzwerkContent.patronage.greetingRole)}</div>
<div style={{ fontFamily: FF, fontSize: 18, fontWeight: 900, color: '#fff', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.15, marginBottom: 10 }}>{fallbackText(patronage.greetingName, netzwerkContent.patronage.greetingName)}</div>
<div style={{ fontFamily: FF, fontSize: 18, color: 'rgba(255,255,255,0.5)', lineHeight: 1.6, marginBottom: 4 }}>{fallbackText(patronage.greetingTitleLine1, netzwerkContent.patronage.greetingTitleLine1)}</div>
<div style={{ fontFamily: FF, fontSize: 17, color: 'rgba(255,255,255,0.4)', lineHeight: 1.6 }}>{fallbackText(patronage.greetingTitleLine2, netzwerkContent.patronage.greetingTitleLine2)}</div>
</div>
<div>
<blockquote className="type-body text-role-inverse-secondary" style={{ fontStyle: 'italic', margin: 0, paddingLeft: 24, borderLeft: `3px solid ${GOLD}` }}>
{fallbackText(patronage.greetingQuote, netzwerkContent.patronage.greetingQuote)}
</blockquote>
<div style={{ fontFamily: FF, fontSize: 11, color: 'rgba(255,255,255,0.35)', marginTop: 20, paddingLeft: 24 }}>
{fallbackText(patronage.greetingAttribution, netzwerkContent.patronage.greetingAttribution)}
</div>
{greetingVideoSrc && (
<div style={{ paddingLeft: 24, marginTop: 28 }}>
<button
type="button"
onClick={() => setShowGreetingVideo(true)}
aria-label={greetingVideoLabel}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 10,
border: `1px solid ${GOLD}73`,
background: 'rgba(239,191,4,0.1)',
color: '#fff',
cursor: 'pointer',
fontFamily: FF,
fontSize: 11,
fontWeight: 700,
letterSpacing: '0.12em',
lineHeight: 1,
padding: '13px 18px',
textTransform: 'uppercase',
transition: 'background 0.15s, border-color 0.15s, color 0.15s',
}}
onMouseEnter={e => {
const el = e.currentTarget as HTMLElement;
el.style.background = GOLD;
el.style.borderColor = GOLD;
el.style.color = NAVY;
}}
onMouseLeave={e => {
const el = e.currentTarget as HTMLElement;
el.style.background = 'rgba(239,191,4,0.1)';
el.style.borderColor = `${GOLD}73`;
el.style.color = '#fff';
}}
>
<Play size={15} fill="currentColor" />
{greetingVideoLabel}
</button>
</div>
)}
</div>
</div>
</div>
<div style={{ height: 2, background: `linear-gradient(to right, ${GOLD}, rgba(239,191,4,0.2), transparent)` }} />
</section>
{/* ── 3. JURY-INTRO (editorial 2-col split) ────────────────────────────── */}
<section style={{ display: isMobile ? 'block' : 'grid', gridTemplateColumns: isMobile ? undefined : '40% 60%', minHeight: isMobile ? 'auto' : 400, position: 'relative', overflow: 'hidden', isolation: 'isolate', background: isMobile ? NAVY : undefined }}>
{!isMobile && <MunichSkylineBg />}
{isMobile && <MunichSkylineBg inset opacity={0.07} />}
{isMobile ? (
/* Mobile: one cohesive navy module copy + compact stat strip */
<div style={{ padding: '48px 24px', position: 'relative', zIndex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, color: GOLD, display: 'block', marginBottom: 18 }}>{fallbackText(juryIntro.eyebrow, netzwerkContent.juryIntro.eyebrow)}</span>
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 22px' }}>
<Lines text={fallbackText(juryIntro.heading, netzwerkContent.juryIntro.heading)} />
</h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 22 }} />
<p className="type-small text-role-inverse-secondary" style={{ margin: 0 }}>
{fallbackText(juryIntro.description, netzwerkContent.juryIntro.description)}
</p>
{/* Stat strip */}
<div style={{ display: 'flex', marginTop: 32, paddingTop: 26, borderTop: '1px solid rgba(255,255,255,0.14)' }}>
{mobileStats.map((s, i) => (
<div key={i} style={{ flex: 1, minWidth: 0, paddingLeft: i > 0 ? 14 : 0, borderLeft: i > 0 ? '1px solid rgba(255,255,255,0.14)' : 'none' }}>
<div style={{ fontFamily: FF, fontSize: 'clamp(2.2rem, 9vw, 3rem)', fontWeight: 900, color: GOLD, letterSpacing: '-0.03em', lineHeight: 1 }}>{fallbackText(s.value, netzwerkContent.juryIntro.stats[i]?.value || '')}</div>
<div style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: 'rgba(255,255,255,0.5)', marginTop: 8, lineHeight: 1.3 }}>{fallbackText(s.label, netzwerkContent.juryIntro.stats[i]?.label || '')}</div>
</div>
))}
</div>
</div>
) : (
<>
{/* Left White, stats */}
<div style={{ background: '#fff', padding: '72px 64px', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative', zIndex: 1 }}>
<div>
<div style={{ fontFamily: FF, fontSize: 'clamp(5rem, 8.5vw, 7.5rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.04em', lineHeight: 1 }}>{fallbackText(desktopStats[0]?.value, netzwerkContent.juryIntro.desktopStats[0].value)}</div>
<div style={{ fontFamily: FF, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.15em', color: 'rgba(16,24,40,0.4)', marginTop: 4 }}>{fallbackText(desktopStats[0]?.label, netzwerkContent.juryIntro.desktopStats[0].label)}</div>
</div>
<div style={{ width: '100%', height: 1, background: 'rgba(3,9,58,0.1)', margin: '32px 0' }} />
<div>
<div style={{ fontFamily: FF, fontSize: 'clamp(2.4rem, 4vw, 3.4rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.03em', lineHeight: 1 }}>{fallbackText(desktopStats[1]?.value, netzwerkContent.juryIntro.desktopStats[1].value)}</div>
<div style={{ fontFamily: FF, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.15em', color: 'rgba(16,24,40,0.4)', marginTop: 8 }}>{fallbackText(desktopStats[1]?.label, netzwerkContent.juryIntro.desktopStats[1].label)}</div>
</div>
<div style={{ width: '100%', height: 1, background: 'rgba(3,9,58,0.1)', margin: '32px 0' }} />
<div>
<div style={{ fontFamily: FF, fontSize: 'clamp(5rem, 8.5vw, 7.5rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.04em', lineHeight: 1 }}>{fallbackText(desktopStats[2]?.value, netzwerkContent.juryIntro.desktopStats[2].value)}</div>
<div style={{ fontFamily: FF, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.15em', color: 'rgba(16,24,40,0.4)', marginTop: 4 }}>{fallbackText(desktopStats[2]?.label, netzwerkContent.juryIntro.desktopStats[2].label)}</div>
</div>
</div>
{/* Right Navy, editorial copy */}
<div style={{ background: NAVY, padding: '72px 64px', display: 'flex', flexDirection: 'column', justifyContent: 'center', position: 'relative', zIndex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, color: GOLD, marginBottom: 20 }}>{fallbackText(juryIntro.eyebrow, netzwerkContent.juryIntro.eyebrow)}</span>
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 24px' }}>
<Lines text={fallbackText(juryIntro.heading, netzwerkContent.juryIntro.heading)} />
</h2>
<div style={{ width: 40, height: 2, background: GOLD }} />
<p className="type-body text-role-inverse-secondary" style={{ marginTop: 24 }}>
{fallbackText(juryIntro.description, netzwerkContent.juryIntro.description)}
</p>
</div>
</>
)}
</section>
{/* ── 4. JURY-MITGLIEDER ───────────────────────────────────────────────── */}
<section id="jury" style={{ background: NAVY, overflow: 'hidden', position: 'relative' }}>
<div style={{ padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 16 : 48, alignItems: 'flex-end', borderBottom: '1px solid rgba(255,255,255,0.07)' }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, color: GOLD, display: 'block', marginBottom: 16 }}>{fallbackText(jury.eyebrow, netzwerkContent.jury.eyebrow)}</span>
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: 0 }}><Lines text={fallbackText(jury.heading, netzwerkContent.jury.heading)} /></h2>
</div>
<p className="type-body text-role-inverse-secondary" style={{ margin: 0 }}>
{fallbackText(jury.description, netzwerkContent.jury.description)}
</p>
</div>
{/* ── Prominenter Vorsitz-Bereich ─────────────────────────────────── */}
{/* TODO: echten Jury-Vorsitzenden bestätigen (Name & Foto sind Platzhalter) */}
{juryMembers.length ? (
<div style={{ padding: isMobile ? '28px 24px 8px' : '64px 80px 16px' }}>
<JuryChairCard member={juryMembers[0]} badge={fallbackText(jury.chairBadge, netzwerkContent.jury.chairBadge)} />
</div>
) : null}
{/* ── Übrige Jury-Mitglieder ──────────────────────────────────────── */}
<div style={{ display: isMobile ? 'flex' : 'grid', gridTemplateColumns: isMobile ? undefined : 'repeat(auto-fill, minmax(260px, 1fr))', overflowX: isMobile ? 'auto' : undefined, scrollSnapType: isMobile ? 'x mandatory' : undefined, gap: isMobile ? 12 : 0, padding: isMobile ? '24px 24px 36px' : '0 0 56px', WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none', borderTop: isMobile ? 'none' : '1px solid rgba(255,255,255,0.07)' }}>
{juryMembers.slice(1).map((member, idx) => (
<JuryCard key={idx} member={member} />
))}
</div>
</section>
{/* ── 5. HINTERGRUND & EXPERTISE ───────────────────────────────────────── */}
<section style={{ background: '#101828', overflow: 'hidden', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', borderTop: '1px solid rgba(255,255,255,0.07)' }}>
<div style={{ padding: isMobile ? '48px 24px' : 80, display: 'flex', flexDirection: 'column', justifyContent: 'center', background: '#101828' }}>
<span style={{ fontFamily: FF, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, color: GOLD, display: 'block', marginBottom: 16 }}>{fallbackText(expertise.eyebrow, netzwerkContent.expertise.eyebrow)}</span>
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 24px' }}><Lines text={fallbackText(expertise.heading, netzwerkContent.expertise.heading)} /></h2>
<div style={{ width: 40, height: 2, background: GOLD, marginBottom: 40 }} />
<div>
{expertiseRows.map((row) => (
<div key={row.num} style={{ display: 'grid', gridTemplateColumns: '40px 1fr', gap: '0 20px', padding: '20px 0', borderBottom: '1px solid rgba(255,255,255,0.06)', alignItems: 'center' }}>
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(239,191,4,0.5)' }}>{row.num}</div>
<div>
<div style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 3 }}>{row.label}</div>
<div style={{ fontFamily: FB, fontSize: 18, color: 'rgba(255,255,255,0.4)', lineHeight: 1.6 }}>{row.body}</div>
</div>
</div>
))}
</div>
</div>
<div style={{ padding: isMobile ? '48px 24px' : '80px 64px', display: 'flex', flexDirection: 'column', justifyContent: 'center', borderLeft: isMobile ? 'none' : '1px solid rgba(255,255,255,0.07)', borderTop: isMobile ? '1px solid rgba(255,255,255,0.07)' : 'none', overflow: 'hidden', background: '#101828' }}>
<span style={{ fontFamily: 'Georgia, serif', fontSize: 'clamp(60px, 8vw, 120px)', color: 'rgba(239,191,4,0.1)', lineHeight: 0.8, display: 'block', marginBottom: -20 }}>&ldquo;</span>
<p className="type-lead text-role-inverse-secondary" style={{ margin: '0 0 40px' }}>
{fallbackText(expertise.quote, netzwerkContent.expertise.quote)}
</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: 40, height: 2, background: GOLD, flexShrink: 0 }} />
<span style={{ fontFamily: FF, fontSize: 17, color: 'rgba(255,255,255,0.4)' }}>{fallbackText(expertise.quoteAttribution, netzwerkContent.expertise.quoteAttribution)}</span>
</div>
</div>
</section>
{/* ── 6. PARTNER & SPONSOREN ───────────────────────────────────────────── */}
<section id="partner" style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
<MunichSkylineBg />
<div style={{ padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 12 : 40, alignItems: 'end', borderBottom: '1px solid rgba(3,9,58,0.1)' }}>
<div>
<span style={{ fontFamily: FF, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, color: 'var(--text-eyebrow-light)', display: 'block', marginBottom: 16 }}>{fallbackText(partnerSection.eyebrow, netzwerkContent.partners.eyebrow)}</span>
<h2 className="type-section-title text-role-heading" style={{ textTransform: 'uppercase', margin: 0 }}><Lines text={fallbackText(partnerSection.heading, netzwerkContent.partners.heading)} /></h2>
</div>
<p className="type-body text-role-secondary" style={{ margin: 0 }}>
{visiblePartnerDescription(partnerSection.description)}
</p>
</div>
{/* Tiered grid continuous, ranks marked by label bars */}
<div style={{ position: 'relative', zIndex: 1 }}>
{partnerTiers.map(t => {
const tier = Number(t.tier) as 1 | 2 | 3;
const variant = tier === 1 ? 'lg' as const : tier === 2 ? 'md' as const : 'sm' as const;
const cols = isMobile ? (tier === 1 ? 1 : 2) : (tier === 3 ? 4 : 2);
const items = partners.filter(p => p.tier === tier);
return (
<div key={t.tier} style={{ display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)` }}>
{items.map((p, i) => (
<SponsorCell key={p.id} partner={p} variant={variant} idx={i} cols={cols} detailLabel={fallbackText(partnerSection.detailLabel, netzwerkContent.partners.detailLabel)} onClick={() => setSelectedPartner(p)} />
))}
</div>
);
})}
</div>
</section>
{/* ── 7. SPONSORING-FORM ───────────────────────────────────────────────── */}
<section id="sponsoring" style={{
background: '#24366A', overflow: 'hidden', position: 'relative',
height: isMobile ? 'auto' : 'min(680px, calc(100vh - 100px))',
display: 'flex', flexDirection: 'column',
}}>
<div style={{ flex: 1, position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '4fr 1px 8fr', minHeight: 0, overflow: 'hidden' }}>
{/* Left pitch */}
<div style={{ padding: isMobile ? '32px 24px 24px' : '36px 36px 32px 52px', display: 'flex', flexDirection: 'column', justifyContent: 'center', overflow: 'hidden' }}>
<span style={{ fontFamily: FF, fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, color: GOLD, display: 'block', marginBottom: 8 }}>{fallbackText(sponsoring.eyebrow, netzwerkContent.sponsoring.eyebrow)}</span>
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: '0 0 12px' }}><Lines text={fallbackText(sponsoring.heading, netzwerkContent.sponsoring.heading)} /></h2>
<div style={{ width: 36, height: 2, background: GOLD, margin: '0 0 14px', flexShrink: 0 }} />
<p className="type-small text-role-inverse-secondary" style={{ marginBottom: 16 }}>
{fallbackText(sponsoring.description, netzwerkContent.sponsoring.description)}
</p>
{sponsorBenefits.map((item) => (
<div key={item.num} style={{ display: 'grid', gridTemplateColumns: '32px 1fr', gap: '0 12px', padding: '10px 0', borderBottom: '1px solid rgba(255,255,255,0.06)', alignItems: 'center' }}>
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(239,191,4,0.5)' }}>{item.num}</span>
<div>
<div style={{ fontFamily: FF, fontSize: 'clamp(14px, 1.05vw, 16px)', fontWeight: 700, color: '#fff', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: 2 }}>{item.label}</div>
<div style={{ fontFamily: FB, fontSize: 'clamp(13px, 1vw, 15px)', color: 'rgba(255,255,255,0.65)', lineHeight: 1.4 }}>{item.sub}</div>
</div>
</div>
))}
</div>
{/* Center divider */}
{!isMobile && <div style={{ background: 'rgba(255,255,255,0.07)' }} />}
{/* Right gold form panel */}
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0, position: 'relative', background: 'linear-gradient(160deg,#DDB84A 0%,#C9A227 52%,#A87800 100%)' }}>
<svg style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none', zIndex: 0 }} aria-hidden="true">
<defs>
<pattern id="sponsoringCross" width="18" height="18" patternUnits="userSpaceOnUse">
<path d="M0,0 L18,18 M18,0 L0,18" stroke="rgba(17,29,85,0.055)" strokeWidth="0.65" />
</pattern>
<pattern id="sponsoringDiamond" width="36" height="36" patternUnits="userSpaceOnUse">
<polygon points="18,2 34,18 18,34 2,18" fill="none" stroke="rgba(17,29,85,0.045)" strokeWidth="0.6" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#sponsoringCross)" />
<rect width="100%" height="100%" fill="url(#sponsoringDiamond)" />
</svg>
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse at center, transparent 55%, rgba(100,70,0,0.25) 100%)', pointerEvents: 'none', zIndex: 0 }} />
{!isMobile && (
<div style={{ height: 220, position: 'relative', overflow: 'hidden', flexShrink: 0, zIndex: 1 }}>
<Image unoptimized src={mediaUrl(sponsoring.image, `/images/${netzwerkContent.sponsoring.imageFilename}`)} alt={mediaAlt(sponsoring.image, fallbackText(sponsoring.imageAlt, netzwerkContent.sponsoring.imageAlt))} style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 40%', filter: 'sepia(0.18) brightness(0.92)' }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, rgba(168,120,0,0.1) 0%, rgba(168,120,0,0.25) 50%, rgba(168,120,0,0.92) 88%, #A87800 100%)' }} />
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2, background: 'rgba(17,29,85,0.35)' }} />
<div style={{ position: 'absolute', bottom: 14, left: 40, display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ width: 5, height: 5, borderRadius: '50%', background: 'rgba(17,29,85,0.7)' }} />
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, color: 'rgba(17,29,85,0.75)', textTransform: 'uppercase', letterSpacing: '0.2em' }}>{fallbackText(sponsoring.imageLabel, netzwerkContent.sponsoring.imageLabel)}</span>
</div>
</div>
)}
<div id="sponsoring-form" style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1, scrollMarginTop: 80 }}>
<SponsoringForm theme="gold" content={cms.sponsoringForm} />
</div>
</div>
</div>
<div style={{ position: 'relative', zIndex: 1, height: 2, background: `linear-gradient(to right, ${GOLD}, rgba(239,191,4,0.3), transparent)`, flexShrink: 0 }} />
</section>
{/* ── 8. PARTNER MODAL ─────────────────────────────────────────────────── */}
{selectedPartner && (
<PartnerModal partner={selectedPartner} copy={partnerModal} onClose={() => setSelectedPartner(null)} />
)}
{/* ── 9. GRUSSWORT VIDEO MODAL ────────────────────────────────────────── */}
{showGreetingVideo && greetingVideoSrc && (
<div
onClick={(e) => { if (e.target === e.currentTarget) setShowGreetingVideo(false); }}
style={{
position: 'fixed',
inset: 0,
zIndex: 520,
background: 'rgba(0,0,0,0.95)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: isMobile ? 16 : 24,
}}
>
<button
type="button"
onClick={() => setShowGreetingVideo(false)}
aria-label={greetingVideoCloseLabel}
style={{
position: 'absolute',
top: isMobile ? 18 : 32,
right: isMobile ? 18 : 32,
width: isMobile ? 42 : 52,
height: isMobile ? 42 : 52,
border: 'none',
borderRadius: '50%',
background: 'rgba(255,255,255,0.1)',
color: 'rgba(255,255,255,0.75)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'background 0.15s, color 0.15s',
}}
onMouseEnter={e => {
const el = e.currentTarget as HTMLElement;
el.style.background = 'rgba(255,255,255,0.18)';
el.style.color = '#fff';
}}
onMouseLeave={e => {
const el = e.currentTarget as HTMLElement;
el.style.background = 'rgba(255,255,255,0.1)';
el.style.color = 'rgba(255,255,255,0.75)';
}}
>
<X size={isMobile ? 24 : 30} />
</button>
<div style={{ width: '100%', maxWidth: 1120, aspectRatio: '16/9', background: '#000' }}>
<video
ref={greetingVideoRef}
src={greetingVideoSrc}
style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }}
controls
autoPlay
playsInline
aria-label={greetingVideoTitle}
/>
</div>
</div>
)}
</div>
);
};
export default Netzwerk;