refactor: update awards grid section and participation page styles for consistency

This commit is contained in:
syntaxbullet
2026-07-13 15:02:37 +02:00
parent b4be98fee0
commit b7c499dd35
4 changed files with 157 additions and 132 deletions

View File

@@ -243,7 +243,7 @@ export const participationFields: Field[] = [
name: 'awardsGrid', name: 'awardsGrid',
label: '06 · Awards grid', label: '06 · Awards grid',
type: 'group', type: 'group',
admin: sectionAdmin('Awards section rendered after the dates banner. On the participation page, Goldene Bavaria is featured above the remaining award cards.'), admin: sectionAdmin('Awards section rendered after the dates banner. On the participation page, the CMS heading and intro belong to the standalone Goldene Bavaria section; the remaining award cards render in a separate section below.'),
fields: [ fields: [
text('eyebrow', 'Eyebrow', participationContent.awardsGrid.eyebrow), text('eyebrow', 'Eyebrow', participationContent.awardsGrid.eyebrow),
textarea('heading', 'Heading', participationContent.awardsGrid.heading), textarea('heading', 'Heading', participationContent.awardsGrid.heading),

View File

@@ -895,7 +895,7 @@ export interface Page {
| null; | null;
}; };
/** /**
* Awards section rendered after the dates banner. On the participation page, Goldene Bavaria is featured above the remaining award cards. * Awards section rendered after the dates banner. On the participation page, the CMS heading and intro belong to the standalone Goldene Bavaria section; the remaining award cards render in a separate section below.
*/ */
awardsGrid?: { awardsGrid?: {
eyebrow?: string | null; eyebrow?: string | null;

View File

@@ -12,6 +12,7 @@ const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
const AWARD_ARTWORK_EDGE_NAVY = '#132B48';
type AwardCard = (typeof participationContent.awardsGrid.cards)[number]; type AwardCard = (typeof participationContent.awardsGrid.cards)[number];
type AwardCardCms = Partial<AwardCard> & { image?: unknown }; type AwardCardCms = Partial<AwardCard> & { image?: unknown };
@@ -80,8 +81,8 @@ function FeaturedAwardCard({ card }: { card: ResolvedAwardCard }) {
const isContainedImage = isContainedAwardImage(imageFilename); const isContainedImage = isContainedAwardImage(imageFilename);
return ( return (
<section aria-label={fallbackText(card.title, 'Goldene Bavaria')} style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(320px, 1fr)', borderBottom: '1px solid rgba(3,9,58,0.1)' }}> <article aria-label={fallbackText(card.title, 'Goldene Bavaria')} style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 1fr) minmax(320px, 1fr)', borderBottom: '1px solid rgba(3,9,58,0.1)' }}>
<div style={{ position: 'relative', height: isMobile ? 180 : 600, background: 'linear-gradient(145deg, #111D55 0%, #1E2B69 44%, #C9A227 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}> <div style={{ position: 'relative', height: isMobile ? 180 : 600, background: AWARD_ARTWORK_EDGE_NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
<Image unoptimized <Image unoptimized
src={mediaUrl(card.image, `/images/${imageFilename}`)} src={mediaUrl(card.image, `/images/${imageFilename}`)}
alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))} alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))}
@@ -90,10 +91,9 @@ function FeaturedAwardCard({ card }: { card: ResolvedAwardCard }) {
height: '100%', height: '100%',
objectFit: isContainedImage ? 'contain' : 'cover', objectFit: isContainedImage ? 'contain' : 'cover',
objectPosition: 'center', objectPosition: 'center',
padding: imageFilename.endsWith('.png') ? (isMobile ? 22 : 30) : 0, padding: 0,
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'block', display: 'block',
filter: imageFilename.endsWith('.png') ? 'drop-shadow(0 32px 46px rgba(3,9,58,0.34))' : undefined,
}} }}
/> />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.42), transparent 42%)', pointerEvents: 'none' }} /> <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,9,58,0.42), transparent 42%)', pointerEvents: 'none' }} />
@@ -160,43 +160,32 @@ function FeaturedAwardCard({ card }: { card: ResolvedAwardCard }) {
</a> </a>
</div> </div>
</div> </div>
</section> </article>
); );
} }
export default function AwardsGridSection({ content, featuredGoldeneBavaria = false }: { content?: AwardsGridContent; featuredGoldeneBavaria?: boolean }) { function SupportingAwardCards({
cards,
allCards,
followsFeaturedCard,
}: {
cards: ResolvedAwardCard[];
allCards: ResolvedAwardCard[];
followsFeaturedCard: boolean;
}) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const section = { ...participationContent.awardsGrid, ...(content || {}) };
const cards = mergeAwardCards(section.cards, participationContent.awardsGrid.cards); if (cards.length === 0) return null;
const { featuredCard, supportingCards } = splitFeaturedAwardCard(cards, featuredGoldeneBavaria);
return ( return (
<section id="auszeichnungen" style={{ background: '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}> <div style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : `repeat(${Math.min(Math.max(cards.length, 1), 3)}, minmax(0, 1fr))`, borderBottom: '1px solid rgba(3,9,58,0.08)' }}>
<MunichSkylineBg /> {cards.map((card, index) => {
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid rgba(3,9,58,0.1)' }}> const sourceIndex = allCards.indexOf(card);
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>
{fallbackText(section.eyebrow, participationContent.awardsGrid.eyebrow)}
</span>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 0.9fr) minmax(0, 1fr)', gap: isMobile ? 16 : 56, alignItems: 'end' }}>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.5vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0, overflowWrap: 'anywhere' }}>
<Lines text={fallbackText(section.heading, participationContent.awardsGrid.heading)} />
</h2>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.55)', lineHeight: 1.8, margin: 0 }}>
{fallbackText(section.description, participationContent.awardsGrid.description)}
</p>
</div>
</div>
{featuredCard ? <FeaturedAwardCard card={featuredCard} /> : null}
{supportingCards.length > 0 ? (
<div style={{ position: 'relative', zIndex: 1, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : `repeat(${Math.min(Math.max(supportingCards.length, 1), 3)}, minmax(0, 1fr))`, borderBottom: '1px solid rgba(3,9,58,0.08)' }}>
{supportingCards.map((card, index) => {
const sourceIndex = cards.indexOf(card);
const imageFilename = getAwardImageFilename(card, sourceIndex >= 0 ? sourceIndex : index); const imageFilename = getAwardImageFilename(card, sourceIndex >= 0 ? sourceIndex : index);
const isContainedImage = isContainedAwardImage(imageFilename); const isContainedImage = isContainedAwardImage(imageFilename);
const isLast = index === supportingCards.length - 1; const usesGoldeneBavariaArtwork = imageFilename.includes('goldene-bavaria');
const isDarkCard = !featuredCard && index === 1; const isLast = index === cards.length - 1;
const isDarkCard = !followsFeaturedCard && index === 1;
return ( return (
<article <article
@@ -204,13 +193,13 @@ export default function AwardsGridSection({ content, featuredGoldeneBavaria = fa
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
minHeight: isMobile ? 'auto' : featuredCard ? 540 : 620, minHeight: isMobile ? 'auto' : followsFeaturedCard ? 540 : 620,
borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none', borderRight: !isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none', borderBottom: isMobile && !isLast ? '1px solid rgba(3,9,58,0.1)' : 'none',
background: isDarkCard ? NAVY : 'rgba(255,255,255,0.72)', background: isDarkCard ? NAVY : followsFeaturedCard ? '#fff' : 'rgba(255,255,255,0.72)',
}} }}
> >
<div style={{ height: isMobile ? 220 : 260, background: imageFilename.includes('roland-berger') ? '#DDE7E2' : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}> <div style={{ height: isMobile ? 220 : 260, background: usesGoldeneBavariaArtwork ? AWARD_ARTWORK_EDGE_NAVY : imageFilename.includes('roland-berger') ? '#DDE7E2' : NAVY, display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
<Image unoptimized <Image unoptimized
src={mediaUrl(card.image, `/images/${imageFilename}`)} src={mediaUrl(card.image, `/images/${imageFilename}`)}
alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))} alt={mediaAlt(card.image, fallbackText(card.imageAlt, fallbackText(card.title, 'Auszeichnung')))}
@@ -219,14 +208,14 @@ export default function AwardsGridSection({ content, featuredGoldeneBavaria = fa
height: '100%', height: '100%',
objectFit: isContainedImage ? 'contain' : 'cover', objectFit: isContainedImage ? 'contain' : 'cover',
objectPosition: isDarkCard ? 'center top' : 'center', objectPosition: isDarkCard ? 'center top' : 'center',
padding: imageFilename.endsWith('.png') ? 22 : 0, padding: usesGoldeneBavariaArtwork ? 0 : imageFilename.endsWith('.png') ? 22 : 0,
boxSizing: 'border-box', boxSizing: 'border-box',
display: 'block', display: 'block',
}} }}
/> />
</div> </div>
<div style={{ padding: isMobile ? '28px 24px 32px' : featuredCard ? '38px 44px 42px' : '36px 36px 40px', display: 'flex', flexDirection: 'column', flex: 1 }}> <div style={{ padding: isMobile ? '28px 24px 32px' : followsFeaturedCard ? '38px 44px 42px' : '36px 36px 40px', display: 'flex', flexDirection: 'column', flex: 1 }}>
<span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.26em', textTransform: 'uppercase', color: isDarkCard ? 'rgba(239,191,4,0.8)' : '#4A8FC9', marginBottom: 12 }}> <span style={{ fontFamily: FF, fontSize: 9, fontWeight: 700, letterSpacing: '0.26em', textTransform: 'uppercase', color: isDarkCard ? 'rgba(239,191,4,0.8)' : '#4A8FC9', marginBottom: 12 }}>
{fallbackText(card.label, '')} {fallbackText(card.label, '')}
</span> </span>
@@ -291,7 +280,47 @@ export default function AwardsGridSection({ content, featuredGoldeneBavaria = fa
); );
})} })}
</div> </div>
) : null} );
</section> }
export default function AwardsGridSection({ content, featuredGoldeneBavaria = false }: { content?: AwardsGridContent; featuredGoldeneBavaria?: boolean }) {
const isMobile = useIsMobile();
const section = { ...participationContent.awardsGrid, ...(content || {}) };
const cards = mergeAwardCards(section.cards, participationContent.awardsGrid.cards);
const { featuredCard, supportingCards } = splitFeaturedAwardCard(cards, featuredGoldeneBavaria);
return (
<>
<section id="auszeichnungen" style={{ background: featuredCard ? '#fff' : '#F7F7F5', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
{!featuredCard ? <MunichSkylineBg /> : null}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid rgba(3,9,58,0.1)' }}>
<span style={{ fontFamily: FF, fontSize: 10, color: '#4A8FC9', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 16 }}>
{fallbackText(section.eyebrow, participationContent.awardsGrid.eyebrow)}
</span>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'minmax(0, 0.9fr) minmax(0, 1fr)', gap: isMobile ? 16 : 56, alignItems: 'end' }}>
<h2 style={{ fontFamily: FF, fontSize: 'clamp(2rem, 3.5vw, 3rem)', fontWeight: 900, color: '#101828', textTransform: 'uppercase', letterSpacing: '-0.025em', lineHeight: 1.03, margin: 0, overflowWrap: 'anywhere' }}>
<Lines text={fallbackText(section.heading, participationContent.awardsGrid.heading)} />
</h2>
<p style={{ fontFamily: FB, fontSize: 18, color: 'rgba(16,24,40,0.55)', lineHeight: 1.8, margin: 0 }}>
{fallbackText(section.description, participationContent.awardsGrid.description)}
</p>
</div>
</div>
{featuredCard ? (
<div style={{ position: 'relative', zIndex: 1, background: '#fff', paddingBottom: isMobile ? 32 : 56 }}>
<FeaturedAwardCard card={featuredCard} />
</div>
) : (
<SupportingAwardCards cards={supportingCards} allCards={cards} followsFeaturedCard={false} />
)}
</section>
{featuredCard && supportingCards.length > 0 ? (
<section aria-label="Weitere Auszeichnungen" style={{ background: '#fff', position: 'relative', overflow: 'hidden', padding: isMobile ? '40px 0 56px' : '64px 0 80px', borderTop: '1px solid rgba(17,29,85,0.08)' }}>
<SupportingAwardCards cards={supportingCards} allCards={cards} followsFeaturedCard />
</section>
) : null}
</>
); );
} }

View File

@@ -5,7 +5,6 @@ import BewerbungsForm from '@/spa/components/forms/BewerbungsForm';
import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm'; import { NewsletterInterestForm } from '@/spa/components/forms/NewsletterInterestForm';
import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection'; import AwardsGridSection, { type AwardsGridContent } from '@/spa/components/AwardsGridSection';
import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection'; import WegZurAuszeichnungSection from '@/spa/components/WegZurAuszeichnungSection';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField'; import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import { useApplicationPhase, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute'; import { useApplicationPhase, useCmsRoute, useNewsletterForm } from '@/spa/cmsRoute';
@@ -17,7 +16,6 @@ const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
const NAVY = '#111D55'; const NAVY = '#111D55';
const GOLD = '#EFBF04'; const GOLD = '#EFBF04';
const CREAM = '#E4E2E3';
type ParticipationCms = Partial<typeof participationContent> & { type ParticipationCms = Partial<typeof participationContent> & {
hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown }; hero?: Partial<typeof participationContent.hero> & { backgroundImage?: unknown };
@@ -115,8 +113,7 @@ const Participation: React.FC = () => {
<WegZurAuszeichnungSection content={process} /> <WegZurAuszeichnungSection content={process} />
{/* ── QUALIFICATIONS ───────────────────────────────────────────────────── */} {/* ── QUALIFICATIONS ───────────────────────────────────────────────────── */}
<section id="voraussetzungen" ref={qualSectionRef} style={{ background: CREAM, overflow: 'hidden', position: 'relative', isolation: 'isolate' }}> <section id="voraussetzungen" ref={qualSectionRef} style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
<MunichSkylineBg />
{/* Header */} {/* Header */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 24 : 48, alignItems: 'flex-end', borderBottom: `1px solid #D0D5DD` }}> <div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 24 : 48, alignItems: 'flex-end', borderBottom: `1px solid #D0D5DD` }}>
@@ -144,7 +141,7 @@ const Participation: React.FC = () => {
</div> </div>
{/* 4 criteria as editorial rows */} {/* 4 criteria as editorial rows */}
<div style={{ position: 'relative', zIndex: 1, borderBottom: `1px solid #D0D5DD` }}> <div style={{ position: 'relative', zIndex: 1, background: '#fff', borderBottom: `1px solid #D0D5DD` }}>
{eligibilityCriteria.map((item, i, arr) => { {eligibilityCriteria.map((item, i, arr) => {
const Icon = ICONS[item.icon as keyof typeof ICONS] || MapPin; const Icon = ICONS[item.icon as keyof typeof ICONS] || MapPin;
return ( return (
@@ -360,8 +357,7 @@ function BewerbungswegeSection({ content }: { content?: ApplicationWaysContent }
(way) => !isFutureAwardApplicationWay(way), (way) => !isFutureAwardApplicationWay(way),
); );
return ( return (
<section style={{ background: CREAM, position: 'relative', overflow: 'hidden', isolation: 'isolate' }}> <section style={{ background: '#fff', position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
<MunichSkylineBg />
{/* Header */} {/* Header */}
<div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid #D0D5DD' }}> <div style={{ position: 'relative', zIndex: 1, padding: isMobile ? '48px 24px 32px' : '80px 80px 56px', borderBottom: '1px solid #D0D5DD' }}>