feat: implement newsletter subscription feature with new forms and database structure

- Added new newsletter subscription functionality to the participation page.
- Created a new `NewsletterForm` component to handle user subscriptions.
- Introduced `NewsletterSubscriptions` collection to manage subscriptions in the database.
- Developed migration script to set up necessary database tables and fields for the newsletter feature.
- Refactored participation content to remove deprecated newsletter phase data and integrate new structure.
- Updated the `Participation` page to utilize the new newsletter form and handle different application phases.
This commit is contained in:
syntaxbullet
2026-07-13 13:37:03 +02:00
parent 64049baef0
commit bd2e99ca91
20 changed files with 1033 additions and 566 deletions

View File

@@ -5,18 +5,19 @@ import { Link } from '@/spa/router';
import { PartnerTicker } from '@/spa/components/ui/partner-ticker';
import { WINNERS } from '@/spa/data/winners';
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm';
import TestimonialsSection from '@/spa/components/TestimonialsSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import Image from '@/spa/components/ui/UnoptimizedImage'
import type { SpaApplicationPhaseData, SpaApplicationPhaseItem } from '@/spa/applicationPhase'
import { resolveCmsRelationship, useApplicationPhase, useCmsCollection, useCmsRoute } from '@/spa/cmsRoute'
import { resolveCmsRelationship, useApplicationPhase, useCmsCollection, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute'
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'
import { homeContent } from '@/spa/homeContent'
import { netzwerkContent } from '@/spa/netzwerkContent'
import { usePreistraegerPlaceholderImage } from '@/spa/preistraegerPlaceholder'
import { PartnerLogoMark, type PartnerLogoData } from '@/spa/components/ui/partner-logo'
import { newsletterFormCopyForPhase, newsletterPhaseForApplicationPhase } from '@/spa/newsletterForm'
const FF = '"IBM Plex Sans", sans-serif';
@@ -57,8 +58,6 @@ type HomeSectionCms = CmsRecord & {
imageAlt?: string | null;
imageLabel?: string | null;
items?: unknown;
newsletterPhaseCompleted?: NewsletterInterestCms | null;
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
note?: string | null;
quote?: string | null;
stats?: unknown;
@@ -118,39 +117,17 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
const HIDE_MEMBERSHIP_HOME_HERO_CTA = true;
const APPLICATION_PHASE_OPEN = '0';
const STATUS_SECTION_BACKGROUND = '#111D55';
const LEGACY_STATUS_SECTION_BACKGROUNDS = new Set(['#020A1E', '#060D1C', '#06090F']);
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
'1': homeContent.application.newsletterPhaseEvaluation,
'2': homeContent.application.newsletterPhaseCompleted,
};
const statusSectionBackground = (value?: string | null) => {
const background = value?.trim();
const mergeNewsletterInterestCopy = (
value: NewsletterInterestCms | null | undefined,
fallback: NewsletterInterestCopy,
): NewsletterInterestCopy => ({
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
heading: fallbackText(value?.heading, fallback.heading),
body: fallbackText(value?.body, fallback.body),
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
formBody: fallbackText(value?.formBody, fallback.formBody),
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
privacy: fallbackText(value?.privacy, fallback.privacy),
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
successBody: fallbackText(value?.successBody, fallback.successBody),
footnote: fallbackText(value?.footnote, fallback.footnote),
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
});
if (!background || LEGACY_STATUS_SECTION_BACKGROUNDS.has(background.toUpperCase())) {
return STATUS_SECTION_BACKGROUND;
}
const getNewsletterInterestCopy = (phase: string | undefined, application: HomeSectionCms) => {
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
const cmsCopy = phase === '1' ? application.newsletterPhaseEvaluation : application.newsletterPhaseCompleted;
return mergeNewsletterInterestCopy(cmsCopy, fallback);
return background;
};
const isMembershipCta = (cta?: { label?: string; url?: string; to?: string } | null) =>
@@ -199,11 +176,13 @@ function HomeWinnerCard({
fallbackTitle,
fallbackImage,
hoverLabel,
horizontalScroll = false,
}: {
winner: WinnerCardData;
fallbackTitle: string;
fallbackImage: string;
hoverLabel: string;
horizontalScroll?: boolean;
}) {
const [hovered, setHovered] = useState(false);
const isMobile = useIsMobile();
@@ -220,7 +199,7 @@ function HomeWinnerCard({
return (
<Link
to={href}
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile ? 'start' : undefined }}
style={{ textDecoration: 'none', display: 'block', position: 'relative', height: isMobile ? 220 : 280, overflow: 'hidden', scrollSnapAlign: isMobile || horizontalScroll ? 'start' : undefined }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
@@ -265,9 +244,11 @@ const Home: React.FC = () => {
const form = home.form || {};
const videoModal = home.videoModal || {};
const applicationPhase = useApplicationPhase();
const newsletterForm = useNewsletterForm();
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, application);
const newsletterCopy = newsletterFormCopyForPhase(newsletterForm, activeApplicationPhase);
const newsletterPhase = newsletterPhaseForApplicationPhase(activeApplicationPhase);
const cmsPreistraeger = useCmsCollection('preistraeger');
const selectedWinners = fallbackArray(winnersSection.featured, []);
const heroSecondaryCta = {
@@ -281,6 +262,8 @@ const Home: React.FC = () => {
const homeWinners = selectedWinnerDocs.length
? selectedWinnerDocs
: (cmsPreistraeger.length ? cmsPreistraeger : WINNERS).filter((w) => Number((w as { year?: string | number }).year) === 2025).slice(0, 8);
const scrollDesktopWinners = !isMobile && homeWinners.length > 8;
const scrollWinnerGrid = isMobile || scrollDesktopWinners;
const quickCriteria = fallbackArray(quickCheck.criteria, homeContent.quickCheck.criteria);
const introStats = fallbackArray(intro.stats, homeContent.intro.stats);
const benefitItems = fallbackArray(benefits.items, homeContent.benefits.items);
@@ -313,53 +296,58 @@ const Home: React.FC = () => {
</div>
<div className="container mx-auto relative z-10" style={{ padding: isMobile ? '0 24px' : '0 24px' }}>
<div style={{ maxWidth: isMobile ? '100%' : '48rem' }}>
<div style={{ maxWidth: isMobile ? '100%' : '62rem' }}>
<div className="inline-flex items-center gap-3 bg-accent/20 border border-accent/30 rounded-full px-4 py-1.5 mb-8 backdrop-blur-sm animate-fade-in" style={{ marginBottom: isMobile ? 20 : undefined }}>
<span className="flex h-2 w-2 rounded-full bg-accent"></span>
<span className="text-accent text-[10px] uppercase tracking-[0.2em] font-bold">{fallbackText(hero.badge, homeContent.hero.badge)}</span>
</div>
<h1 className="font-display font-bold text-white mb-6 leading-[0.9] tracking-tight" style={{ fontSize: isMobile ? 'clamp(1.8rem, 10vw, 3rem)' : 'clamp(3.5rem, 8vw, 6rem)' }}>
{fallbackText(hero.headingLine1, homeContent.hero.headingLine1)} <br />
<span className="text-accent">{fallbackText(hero.headingAccent, homeContent.hero.headingAccent)}</span>
<h1
className="font-display font-bold text-white mb-6 leading-[0.9] tracking-tight"
style={{ fontSize: isMobile ? 'clamp(1.75rem, 8vw, 3rem)' : 'clamp(3.25rem, 6.5vw, 6rem)' }}
>
<span style={{ display: 'block', whiteSpace: 'nowrap' }}>
{fallbackText(hero.headingLine1, homeContent.hero.headingLine1)}
</span>
<span className="text-accent" style={{ display: 'block', whiteSpace: 'nowrap' }}>
{fallbackText(hero.headingAccent, homeContent.hero.headingAccent)}
</span>
</h1>
<p className="text-white/70 mb-12 font-light leading-relaxed" style={{ fontSize: isMobile ? '1.0625rem' : '1.375rem', maxWidth: isMobile ? '100%' : '36rem', marginBottom: isMobile ? 28 : undefined }}>
{fallbackText(hero.description, homeContent.hero.description)}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', flexWrap: isMobile ? 'nowrap' : 'wrap', gap: 16, alignItems: isMobile ? 'stretch' : 'center' }}>
<Link
to={fallbackText(hero.primaryCta?.url, homeContent.hero.primaryCta.url)}
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'#101828', background:'#EFBF04', padding:'16px 36px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent: isMobile ? 'center' : 'flex-start', gap:10, transition:'background 0.15s, box-shadow 0.2s', width: isMobile ? '100%' : 'auto' }}
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'#101828', background:'#EFBF04', padding:'16px 24px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent:'center', gap:10, transition:'background 0.15s, box-shadow 0.2s', width: isMobile ? '100%' : 'auto' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.background = '#FFD130'; (e.currentTarget as HTMLElement).style.boxShadow = '0 0 18px rgba(239,191,4,0.65), 0 0 40px rgba(239,191,4,0.3)'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = '#EFBF04'; (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
>
{fallbackText(hero.primaryCta?.label, homeContent.hero.primaryCta.label)} <ChevronRight size={16} />
</Link>
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 16, alignItems: isMobile ? 'stretch' : 'center' }}>
{showHeroSecondaryCta && (
<Link
to={heroSecondaryCta.url}
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'rgba(255,255,255,0.85)', border:'1.5px solid rgba(255,255,255,0.35)', padding:'14px 32px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent: isMobile ? 'center' : 'flex-start', gap:10, transition:'border-color 0.15s, color 0.15s', background:'transparent', width: isMobile ? '100%' : 'auto' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#EFBF04'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.35)'; (e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.85)'; }}
>
{heroSecondaryCta.label}
</Link>
)}
<button
type="button"
onClick={() => setShowVideo(true)}
className="flex items-center gap-4 text-white hover:text-accent transition-colors group"
style={{ justifyContent: isMobile ? 'center' : 'flex-start' }}
<button
type="button"
onClick={() => setShowVideo(true)}
className="flex items-center gap-4 text-white hover:text-accent transition-colors group"
style={{ justifyContent: isMobile ? 'center' : 'flex-start' }}
>
<div className="w-12 h-12 rounded-full border border-white/30 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/10 transition-all">
<Play size={20} fill="currentColor" />
</div>
<span className="uppercase text-xs tracking-widest font-bold">{fallbackText(hero.videoLabel, homeContent.hero.videoLabel)}</span>
</button>
{showHeroSecondaryCta && (
<Link
to={heroSecondaryCta.url}
style={{ fontFamily:'"IBM Plex Sans", sans-serif', fontSize: 16, fontWeight:700, textTransform:'uppercase', letterSpacing:'0.12em', color:'rgba(255,255,255,0.85)', border:'1.5px solid rgba(255,255,255,0.35)', padding:'14px 32px', textDecoration:'none', display:'inline-flex', alignItems:'center', justifyContent:'center', gap:10, transition:'border-color 0.15s, color 0.15s', background:'transparent', width: isMobile ? '100%' : 'auto' }}
onMouseEnter={e => { (e.currentTarget as HTMLElement).style.borderColor = '#EFBF04'; (e.currentTarget as HTMLElement).style.color = '#EFBF04'; }}
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.35)'; (e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.85)'; }}
>
<div className="w-12 h-12 rounded-full border border-white/30 flex items-center justify-center group-hover:border-accent group-hover:bg-accent/10 transition-all">
<Play size={20} fill="currentColor" />
</div>
<span className="uppercase text-xs tracking-widest font-bold">{fallbackText(hero.videoLabel, homeContent.hero.videoLabel)}</span>
</button>
</div>
{heroSecondaryCta.label}
</Link>
)}
</div>
</div>
</div>
@@ -402,12 +390,13 @@ const Home: React.FC = () => {
<div style={{
display: 'grid',
gridTemplateColumns: isMobile ? undefined : 'repeat(4, 1fr)',
gridAutoFlow: isMobile ? 'column' : undefined,
gridTemplateRows: isMobile ? 'repeat(2, 1fr)' : undefined,
gridAutoColumns: isMobile ? '66vw' : undefined,
overflowX: isMobile ? 'auto' : undefined,
scrollSnapType: isMobile ? 'x mandatory' : undefined,
gridTemplateColumns: scrollWinnerGrid ? undefined : 'repeat(4, 1fr)',
gridAutoFlow: scrollWinnerGrid ? 'column' : undefined,
gridTemplateRows: scrollWinnerGrid ? `repeat(2, ${isMobile ? '1fr' : '280px'})` : undefined,
gridAutoColumns: isMobile ? '66vw' : scrollDesktopWinners ? '25vw' : undefined,
overflowX: scrollWinnerGrid ? 'auto' : undefined,
scrollSnapType: scrollWinnerGrid ? 'x mandatory' : undefined,
overscrollBehaviorX: scrollWinnerGrid ? 'contain' : undefined,
gap: isMobile ? 8 : 0,
padding: isMobile ? '0 16px 8px' : 0,
WebkitOverflowScrolling: 'touch',
@@ -420,6 +409,7 @@ const Home: React.FC = () => {
fallbackTitle={fallbackText(winnersSection.cardFallbackTitle, homeContent.winners.cardFallbackTitle)}
fallbackImage={winnerFallbackImage}
hoverLabel={fallbackText(winnersSection.hoverLabel, homeContent.winners.hoverLabel)}
horizontalScroll={scrollDesktopWinners}
/>
))}
</div>
@@ -716,7 +706,7 @@ const Home: React.FC = () => {
<div style={{ padding: isMobile ? '32px 24px' : '24px 48px 32px 40px', flex: isMobile ? 'none' : 1, height: isMobile && showApplicationForm ? '82svh' : undefined, display: 'flex', flexDirection: 'column', minHeight: 0, position: 'relative', zIndex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{showApplicationForm ? fallbackText(application.formEyebrow, homeContent.application.formEyebrow) : newsletterCopy.formEyebrow}</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{showApplicationForm ? fallbackText(application.formTitle, homeContent.application.formTitle) : newsletterCopy.formTitle}</h3>
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} />}
{showApplicationForm ? <BewerbungsForm theme="gold" content={form} /> : <NewsletterInterestForm copy={newsletterCopy} phase={newsletterPhase} source="homepage" />}
</div>
</div>
@@ -784,7 +774,7 @@ function StatusSlider({ data }: { data?: SpaApplicationPhaseData }) {
return {
...phase,
accent: phase.accent || '#EFBF04',
bg: phase.bg || '#020A1E',
bg: statusSectionBackground(phase.bg),
cta,
membershipCta,
}

View File

@@ -2,15 +2,16 @@ import React, { useRef } from 'react';
import { MapPin, Users, TrendingUp, Building2, ArrowRight, CheckCircle2, UserCheck, Send, Star } from 'lucide-react';
import { Link } from '@/spa/router';
import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import { NewsletterInterestForm, type NewsletterBenefit, type NewsletterInterestCms, type NewsletterInterestCopy } from '@/spa/components/forms/NewsletterInterestForm';
import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm';
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection';
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useApplicationPhase, useCmsRoute } from '@/spa/cmsRoute';
import { useApplicationPhase, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute';
import { participationContent } from '@/spa/participationContent';
import Image from '@/spa/components/ui/UnoptimizedImage'
import { newsletterFormCopyForPhase, newsletterPhaseForApplicationPhase } from '@/spa/newsletterForm';
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
@@ -26,8 +27,6 @@ type ParticipationCms = Partial<typeof participationContent> & {
type ParticipationApplicationFormCms = Partial<typeof participationContent.applicationForm> & {
image?: unknown;
newsletterPhaseCompleted?: NewsletterInterestCms | null;
newsletterPhaseEvaluation?: NewsletterInterestCms | null;
};
const ICONS = {
@@ -48,40 +47,6 @@ const fallbackArray = <T,>(value: unknown, fallback: readonly T[]) =>
const APPLICATION_PHASE_OPEN = '0';
const newsletterInterestCopy: Record<string, NewsletterInterestCopy> = {
'1': participationContent.applicationForm.newsletterPhaseEvaluation,
'2': participationContent.applicationForm.newsletterPhaseCompleted,
};
const mergeNewsletterInterestCopy = (
value: NewsletterInterestCms | null | undefined,
fallback: NewsletterInterestCopy,
): NewsletterInterestCopy => ({
eyebrow: fallbackText(value?.eyebrow, fallback.eyebrow),
heading: fallbackText(value?.heading, fallback.heading),
body: fallbackText(value?.body, fallback.body),
benefits: fallbackArray<NewsletterBenefit>(value?.benefits, fallback.benefits),
formEyebrow: fallbackText(value?.formEyebrow, fallback.formEyebrow),
formTitle: fallbackText(value?.formTitle, fallback.formTitle),
formBody: fallbackText(value?.formBody, fallback.formBody),
emailLabel: fallbackText(value?.emailLabel, fallback.emailLabel),
emailPlaceholder: fallbackText(value?.emailPlaceholder, fallback.emailPlaceholder),
submitLabel: fallbackText(value?.submitLabel, fallback.submitLabel),
privacy: fallbackText(value?.privacy, fallback.privacy),
validationRequired: fallbackText(value?.validationRequired, fallback.validationRequired),
validationInvalid: fallbackText(value?.validationInvalid, fallback.validationInvalid),
successHeading: fallbackText(value?.successHeading, fallback.successHeading),
successBody: fallbackText(value?.successBody, fallback.successBody),
footnote: fallbackText(value?.footnote, fallback.footnote),
footnoteStrong: fallbackText(value?.footnoteStrong, fallback.footnoteStrong),
});
const getNewsletterInterestCopy = (phase: string | undefined, applicationForm: ParticipationApplicationFormCms) => {
const fallback = newsletterInterestCopy[phase || ''] || newsletterInterestCopy['2'];
const cmsCopy = phase === '1' ? applicationForm.newsletterPhaseEvaluation : applicationForm.newsletterPhaseCompleted;
return mergeNewsletterInterestCopy(cmsCopy, fallback);
};
const isFutureAwardApplicationWay = (value: { label?: unknown; title?: unknown }) => {
const label = typeof value.label === 'string' ? value.label.toLowerCase() : '';
const title = typeof value.title === 'string' ? value.title.toLowerCase() : '';
@@ -98,6 +63,7 @@ const Participation: React.FC = () => {
const qualSectionRef = useRef<HTMLElement>(null);
const cms = (useCmsRoute()?.doc?.participation || {}) as ParticipationCms;
const applicationPhase = useApplicationPhase();
const newsletterForm = useNewsletterForm();
const hero = { ...participationContent.hero, ...(cms.hero || {}) };
const process = { ...participationContent.process, ...(cms.process || {}) };
const eligibility = { ...participationContent.eligibility, ...(cms.eligibility || {}) };
@@ -108,7 +74,8 @@ const Participation: React.FC = () => {
const form = { ...participationContent.form, ...(cms.form || {}) };
const activeApplicationPhase = applicationPhase?.activePhase || APPLICATION_PHASE_OPEN;
const showApplicationForm = activeApplicationPhase === APPLICATION_PHASE_OPEN;
const newsletterCopy = getNewsletterInterestCopy(activeApplicationPhase, applicationForm);
const newsletterCopy = newsletterFormCopyForPhase(newsletterForm, activeApplicationPhase);
const newsletterPhase = newsletterPhaseForApplicationPhase(activeApplicationPhase);
const eligibilityCriteria = fallbackArray(eligibility.criteria, participationContent.eligibility.criteria);
const eligibilityNotes = fallbackArray(eligibility.notes, participationContent.eligibility.notes);
const mobileDates = fallbackArray(datesBanner.mobileItems, participationContent.datesBanner.mobileItems);
@@ -324,7 +291,7 @@ const Participation: React.FC = () => {
<>
<span style={{ fontFamily: FF, fontSize: 10, color: 'rgba(17,29,85,0.5)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 8 }}>{newsletterCopy.formEyebrow}</span>
<h3 style={{ fontFamily: FF, fontSize: 'clamp(1.2rem, 1.8vw, 1.6rem)', fontWeight: 900, color: '#111D55', textTransform: 'uppercase', letterSpacing: '-0.02em', lineHeight: 1.1, marginBottom: 24 }}>{newsletterCopy.formTitle}</h3>
<NewsletterInterestForm copy={newsletterCopy} />
<NewsletterInterestForm copy={newsletterCopy} phase={newsletterPhase} source="participation" />
</>
)}
</div>