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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user