920 lines
58 KiB
TypeScript
920 lines
58 KiB
TypeScript
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 }}>“</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;
|