Rebuild the site to the Claude Design drafts

Carries the 11 page drafts plus the shared SiteNav and SiteFooter onto the
project's own design system: tier-2 tokens, the published type roles, logical
properties throughout, and both locales.

Surfaces
- The page stands on #0B0B0A with #1D1D1B bands lifted off it, as the drafts
  do; the body previously painted the band colour, so every band was a step
  too light.
- A card is drawn one step off whatever it stands on (#1D1D1B on the ground,
  #141413 inside a band). Components paint --surface-card and a band re-points
  it for its subtree, so a card can never come out the same colour as its band.

Shell
- Nav: translucent ground with a blur, chevrons on group triggers, an accent
  rule under the current section, the drafts' compact dropdown card, and an
  EN | AR toggle in place of the globe menu.
- Footer: the drafts' four-column grid, one-line brand text, Facebook as a
  bordered button, the EU lockup, the implemented-by row and the funding
  disclaimer.

Pages
- Home: hero with counting figures, launch band, three areas of change,
  newsroom, FAQ accordion and the consortium band.
- The nine interior pages rebuilt and then audited section by section against
  their drafts.
- Applications: the three-step application form now posts real submissions to
  the Strapi `application` type, validated server-side and working without
  JavaScript.

Motion
- Every animation in the drafts, as classes in _page-kit/motion.css: staggered
  entrances, scroll-driven reveals, the drifting washes (each page carries its
  own geometry), the sheen, the counting figures and the accordion. The
  drafts' coloured hover glow is deliberately not shipped.

CMS
- New `faq` collection type behind the home page's accordion.
- `role` added to the consortium member component so an editor can supply the
  chip and the deliverable separately.

Content
- No invented figure, date, name or commitment ships: the five-working-day
  reply, the placeholder email and Facebook handle, the sample news, events,
  publications and FAQs, the invented application rules and the TBD counters
  are all withheld pending real content. Figures that survived (3 consortium
  partners, 4 areas of work, 16 human security sub-projects, Dec 2025 - Dec
  2027) are verified against docs/ and the bundled draft copy.

Verified: pnpm typecheck (web + CMS) and pnpm --filter web build pass; all 20
page/locale URLs render 200 through the built SSR handler, including with the
CMS unreachable; no raw hex or physical-direction utility in components or
routes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 20:32:56 +02:00
co-authored by Claude Opus 5
parent 8464c24fb1
commit aedef6daeb
63 changed files with 7276 additions and 1768 deletions
+83 -102
View File
@@ -36,12 +36,14 @@ import type {
import {
DEFAULT_FOOTER_COPY,
DEFAULT_FOOTER_PARTNERS,
DEFAULT_FOOTER_SOCIAL_LINKS,
defaultFooterSocialLinks,
FOOTER_ASSETS,
FOOTER_EU_EMBLEM_SIZE,
FOOTER_WORDMARK_SIZE,
defaultFooterLinkGroups,
} from "./content";
import "./Footer.css";
import { EuBadge } from "../EuBadge";
import { FooterSocialIcon } from "./FooterSocialIcon";
const cx = (...parts: readonly (string | false | undefined)[]): string =>
@@ -69,18 +71,20 @@ const TRANSITION =
/** Body/Large Bold links - 18/28, as the footer frame sets them. */
const LINK_CLASS = cx(
FOCUS_RING,
TRANSITION,
"body-lg-bold inline-block text-[var(--text-primary)]",
"hover:text-[var(--text-link)] hover:underline underline-offset-4",
/* Colour AND the 4px nudge, both eased, both in ./Footer.css - see the note
there on why this is not a `translate-x` utility. */
"sh-footer-link",
"body-md-bold inline-block no-underline text-[var(--text-primary)]",
"hover:text-[var(--text-accent)]",
);
/** Label/Medium - the "implemented by" label (figma 228:3035). */
const LABEL_CLASS = "label-md uppercase";
/** Body/Small Bold - the sitemap column headings (figma 140:7823 / 140:7834).
/** The drafts set the column headings as a small uppercase label.
`--text-tertiary` is already remapped to a readable step on the light
surface, so the same token is correct on both. */
const GROUP_HEADING_CLASS = "body-sm-bold uppercase text-[var(--text-tertiary)]";
const GROUP_HEADING_CLASS = "label-md uppercase text-[var(--text-tertiary)]";
/**
* Internal links go through the router so the footer navigates client-side;
@@ -106,7 +110,7 @@ export function Footer({
locale = "en",
dir,
linkGroups,
socialLinks = DEFAULT_FOOTER_SOCIAL_LINKS,
socialLinks,
partners = DEFAULT_FOOTER_PARTNERS,
copy: copyOverrides,
homeHref,
@@ -118,6 +122,9 @@ export function Footer({
// would blank the default, so only defined overrides are applied.
const copy: FooterCopy = { ...DEFAULT_FOOTER_COPY[locale], ...definedOnly(copyOverrides) };
const groups = linkGroups ?? defaultFooterLinkGroups(locale);
/* `[]` from a caller hides the row deliberately; `undefined` means "nobody
said", so the project's own confirmed channels stand in. */
const social = socialLinks ?? defaultFooterSocialLinks(locale);
const home = homeHref ?? `/${locale}`;
return (
@@ -126,26 +133,17 @@ export function Footer({
dir={dir}
data-surface={surface}
className={cx(
// data-surface sets the palette, but the frame puts the footer on its
// own ground (base/black on dark) with a rule separating it from the
// page, so those two are stated here rather than inherited.
"w-full border-t border-[var(--border-default)] bg-[var(--surface-footer)]",
/* The drafts stand the footer on its own ground (#141413) under a
hairline, rather than letting it inherit the page surface. */
"w-full border-t border-[var(--border-subtle)] bg-[var(--surface-footer)]",
"px-lg pb-2xl pt-[var(--layout-section-gap)] md:px-xl",
className,
)}
>
<div className="mx-auto flex w-full max-w-[var(--layout-content-max)] flex-col gap-2xl">
{/* ---- brand + sitemap (figma 140:7684 / 140:8686) ---------------- */}
<div className="flex flex-col gap-2xl lg:flex-row lg:gap-3xl">
{/* --- brand column (figma 140:7685 / 140:8687) ----------------- */}
<div
className={cx(
"flex flex-col gap-md",
// the divider sits on the inline-end of the brand block, which is
// the left edge once <html dir="rtl"> mirrors the row.
"lg:w-[345px] lg:shrink-0 lg:border-e lg:border-[var(--border-subtle)] lg:pe-2xl",
)}
>
{/* ---- brand column + sitemap --------------------------------- */}
<div className="grid gap-xl lg:grid-cols-[minmax(11.875rem,1fr)_2fr]">
<div className="flex flex-col items-start gap-md">
<Link to={home} aria-label={copy.homeLabel} className={cx(FOCUS_RING, "w-fit")}>
<img
src={isDark ? FOOTER_ASSETS.wordmarkInverse : FOOTER_ASSETS.wordmark}
@@ -158,46 +156,43 @@ export function Footer({
/>
</Link>
<p className="body-sm text-[var(--text-secondary)]">{copy.tagline}</p>
<p className="body-sm max-w-[46ch] text-[var(--text-primary)]">
{copy.description}
{/* One line: the full name and the dates the project runs. */}
<p className="body-sm max-w-[13.75rem] text-[var(--text-secondary)]">
{copy.brandLine}
</p>
{/* --- EU visibility (figma 140:7688 / 238:2325) -------------- */}
<div className="flex items-center gap-2xs pt-2xs">
<img
src={isDark ? FOOTER_ASSETS.euEmblemInverse : FOOTER_ASSETS.euEmblem}
alt={copy.euEmblemAlt}
width={FOOTER_EU_EMBLEM_SIZE.width}
height={FOOTER_EU_EMBLEM_SIZE.height}
className="block h-[32px] w-[48px] shrink-0"
loading="lazy"
decoding="async"
/>
<p className="body-sm-bold max-w-[24ch] text-[var(--text-primary)]">
{copy.euStatement}
</p>
</div>
{/* The draft draws each channel as a bordered button with its
glyph and a written label, not as a bare icon. */}
{social.length > 0 && (
<>
<h2 className="sr-only">{copy.socialHeading}</h2>
<ul className="flex flex-wrap gap-xs">
{social.map((link) => (
<li key={link.platform}>
<SocialButton link={link} />
</li>
))}
</ul>
</>
)}
{socialLinks.length > 0 && <SocialRow copy={copy} links={socialLinks} />}
{/* EU visibility, through the shared lockup the hero uses. */}
<EuBadge
statement={copy.euStatement}
locale={locale}
surface={surface}
size="sm"
/>
</div>
{/* --- sitemap columns (figma 380:12402) ------------------------ */}
<nav aria-label={copy.navLabel} className="flex flex-1 flex-col gap-2xl sm:flex-row sm:gap-0">
{groups.map((group, index) => (
<div
key={group.id}
className={cx(
"flex min-w-0 flex-col gap-md sm:flex-1",
// the rule sits on the inline-start of every column but the
// first, so it mirrors with the row under dir="rtl".
index > 0 && "sm:border-s sm:border-[var(--border-subtle)] sm:ps-xl",
index < groups.length - 1 && "sm:pe-xl",
)}
>
<nav
aria-label={copy.navLabel}
className="grid gap-xl sm:grid-cols-2 lg:grid-cols-3"
>
{groups.map((group) => (
<div key={group.id} className="flex min-w-0 flex-col gap-sm">
<h2 className={GROUP_HEADING_CLASS}>{group.heading}</h2>
<ul className="flex flex-col gap-lg">
<ul className="flex flex-col gap-sm">
{group.links.map((link: FooterLink, linkIndex: number) => (
<li key={`${link.href}-${linkIndex}`}>
<FooterNavLink link={link} className={LINK_CLASS} />
@@ -209,19 +204,15 @@ export function Footer({
</nav>
</div>
{/* ---- implemented-by bar (figma 228:3062 / 140:8710) ------------- */}
{/* ---- implemented by ------------------------------------------ */}
{partners.length > 0 && (
<div className="flex flex-col gap-lg border-t border-[var(--border-default)] pt-lg sm:flex-row sm:items-center sm:justify-between">
{/* Figma paints this label background/accent on dark and
text/secondary on light; --text-accent is the readable green. */}
<p
className={cx(
LABEL_CLASS,
"text-[var(--text-secondary)] surface-dark:text-[var(--text-accent)]",
)}
>
<div className="flex flex-wrap items-center justify-between gap-lg border-t border-[var(--border-subtle)] pt-lg">
<p className={cx(LABEL_CLASS, "text-[var(--text-secondary)]")}>
{copy.implementedByLabel}
</p>
{/* The draft prints the three names in bordered chips because it
cannot embed artwork. The real marks are shown instead - they
are contractual visibility and must not be substituted. */}
<ul className="flex flex-wrap items-center gap-lg">
{partners.map((partner) => (
<li key={partner.id} className="flex items-center">
@@ -231,6 +222,11 @@ export function Footer({
</ul>
</div>
)}
{/* ---- the funding disclaimer closes the footer ----------------- */}
<p className="body-sm max-w-[48.75rem] text-[var(--text-tertiary)]">
{copy.disclaimer}
</p>
</div>
</footer>
);
@@ -270,44 +266,29 @@ function PartnerMark({ isDark, partner }: { isDark: boolean; partner: FooterPart
}
/* ---------------------------------------------------------------------------
Social row. Not in the published frames - the draft copy asks for it, so it
closes the brand column. The negative inline-start margin cancels the icon
padding so the glyphs optically align with the text above them.
One social channel, drawn as the draft's bordered button: the glyph, then
the written label ("Follow us on Facebook"), so the control names itself
instead of relying on an icon alone.
------------------------------------------------------------------------ */
function SocialRow({
copy,
links,
}: {
copy: FooterCopy;
links: readonly FooterSocialLink[];
}) {
function SocialButton({ link }: { link: FooterSocialLink }) {
return (
<div className="pt-2xs">
<h2 className="sr-only">{copy.socialHeading}</h2>
<ul className="-ms-2xs flex flex-wrap items-center gap-2xs">
{links.map((link) => (
<li key={link.platform}>
<a
href={link.href}
aria-label={link.label}
target="_blank"
rel="noopener noreferrer"
className={cx(
FOCUS_RING,
TRANSITION,
"inline-flex items-center justify-center p-2xs",
"text-[var(--text-secondary)] hover:text-[var(--text-link)]",
)}
>
<FooterSocialIcon
platform={link.platform}
className="h-[var(--control-icon-size)] w-[var(--control-icon-size)]"
/>
</a>
</li>
))}
</ul>
</div>
<a
href={link.href}
target="_blank"
rel="noopener noreferrer"
className={cx(
FOCUS_RING,
TRANSITION,
"inline-flex items-center gap-xs rounded-control no-underline",
"border border-[var(--border-subtle)] py-xs ps-sm pe-md",
"text-[var(--text-primary)]",
"hover:border-[var(--border-accent)] hover:bg-[var(--surface-page)]",
"hover:text-[var(--text-accent)]",
)}
>
<FooterSocialIcon platform={link.platform} className="h-[17px] w-[17px]" />
<span className="body-sm-bold">{link.label}</span>
</a>
);
}