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>
267 lines
11 KiB
CSS
267 lines
11 KiB
CSS
/* ============================================================================
|
|
BUTTON - figma frame 14:487 (Variant x Size x State)
|
|
----------------------------------------------------------------------------
|
|
Only TIER-2 semantic tokens are referenced here. The `--sh-btn-*` custom
|
|
properties are component-local plumbing: each variant block binds them to a
|
|
tier-2 token once, and the state rules below read them, so the whole 5x3x3
|
|
matrix is expressed as five small blocks instead of forty-five selectors.
|
|
|
|
Everything lives in @layer components so a Tailwind utility passed through
|
|
`className` (`w-full`, `mt-lg`, `text-text-secondary`, ...) still wins.
|
|
The layer statement is a defensive no-op when app.css has already declared
|
|
the same order, and makes this file correct on its own if it ever loads first.
|
|
|
|
HEIGHTS. Figma draws the stroke *inside* the frame, so a Secondary button is
|
|
the same height as a Primary one. Borders are therefore subtracted from the
|
|
padding, which lands every size on its designed box:
|
|
sm 6.5*2 + 20 + 1.5*2 = 36 md 10.5*2 + 24 + 1.5*2 = 48
|
|
lg 10.5*2 + 28 + 1.5*2 = 52
|
|
The 20/24/28 line-heights come from the type role the component puts on the
|
|
element (body-sm-bold / body-md-bold / body-lg-bold).
|
|
|
|
DEVIATIONS FROM FIGMA, deliberate:
|
|
* Focus-visible is not drawn in Figma at all. WCAG 2.4.7 requires it, so the
|
|
semantic focus ring is applied here; filled variants switch to the inverse
|
|
ring colour because a green ring on a green fill is invisible.
|
|
* "Ghost / Large / Default" is the one cell in the matrix with no padding
|
|
while its own Hover and Disabled cells have it - an authoring slip that
|
|
would make the button jump on hover. Padding is kept on all ghost states.
|
|
* Disabled uses the opaque `-bg-disabled` / `-fg-disabled` tokens rather than
|
|
Figma's 40% opacity, so the page never shows through a disabled control.
|
|
`--control-opacity-disabled` still holds the 0.4 if that is ever wanted.
|
|
========================================================================= */
|
|
|
|
@layer theme, base, components, utilities;
|
|
|
|
@layer components {
|
|
/* ---------------------------------------------------------------------
|
|
Base - shared by every variant and size.
|
|
A one-cell grid stacks the label and the loading spinner without either
|
|
transforms or absolute offsets, so it stays correct under dir="rtl".
|
|
------------------------------------------------------------------ */
|
|
.sh-btn {
|
|
display: inline-grid;
|
|
grid-template-areas: "sh-btn-stack";
|
|
place-items: center;
|
|
border-style: solid;
|
|
border-width: var(--control-border-width);
|
|
border-radius: var(--radius-control);
|
|
background-color: var(--sh-btn-bg);
|
|
color: var(--sh-btn-fg);
|
|
border-color: var(--sh-btn-border);
|
|
text-align: center;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
transition:
|
|
background-color var(--motion-duration-fast) var(--motion-ease-standard),
|
|
border-color var(--motion-duration-fast) var(--motion-ease-standard),
|
|
color var(--motion-duration-fast) var(--motion-ease-standard),
|
|
transform var(--motion-duration-base) var(--motion-ease-entrance);
|
|
}
|
|
|
|
/* The Claude Design drafts lift a button 3px under the pointer and set it
|
|
back down on press. Pointer devices only - a touch "hover" would leave the
|
|
control raised after the tap - and never while disabled or busy, where the
|
|
control must not look live. app.css collapses the transition under
|
|
prefers-reduced-motion, so the lift becomes an instant, harmless offset.
|
|
|
|
The drafts also put a coloured glow behind the lifted button; that is
|
|
deliberately not shipped (client decision). */
|
|
@media (hover: hover) {
|
|
.sh-btn:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
|
|
transform: translateY(-3px);
|
|
}
|
|
}
|
|
|
|
.sh-btn:active:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
|
|
transform: none;
|
|
}
|
|
|
|
.sh-btn--block {
|
|
display: grid;
|
|
inline-size: 100%;
|
|
}
|
|
|
|
.sh-btn__content {
|
|
grid-area: sh-btn-stack;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--control-gap);
|
|
transition: opacity var(--motion-duration-fast) var(--motion-ease-standard);
|
|
}
|
|
|
|
/* The 24px box the icon is drawn inside in Figma. Kept on the slot rather
|
|
than on the icon so a consumer's own glyph inherits the same geometry. */
|
|
.sh-btn__icon {
|
|
display: inline-flex;
|
|
flex: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
inline-size: var(--control-icon-size);
|
|
block-size: var(--control-icon-size);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Sizes - Figma: sm 16/8 (space.md/space.xs), md + lg 24/12 (space.lg/sm)
|
|
------------------------------------------------------------------ */
|
|
.sh-btn[data-size="sm"] {
|
|
padding-inline: calc(var(--control-padding-inline-sm) - var(--control-border-width));
|
|
padding-block: calc(var(--control-padding-block-sm) - var(--control-border-width));
|
|
}
|
|
|
|
.sh-btn[data-size="md"] {
|
|
padding-inline: calc(var(--control-padding-inline-md) - var(--control-border-width));
|
|
padding-block: calc(var(--control-padding-block-md) - var(--control-border-width));
|
|
}
|
|
|
|
.sh-btn[data-size="lg"] {
|
|
padding-inline: calc(var(--control-padding-inline-lg) - var(--control-border-width));
|
|
padding-block: calc(var(--control-padding-block-lg) - var(--control-border-width));
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Variants - the only place a token name appears twice.
|
|
------------------------------------------------------------------ */
|
|
.sh-btn[data-variant="primary"] {
|
|
--sh-btn-bg: var(--interactive-primary-bg);
|
|
--sh-btn-bg-hover: var(--interactive-primary-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-primary-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-primary-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-primary-fg);
|
|
--sh-btn-fg-hover: var(--interactive-primary-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-primary-fg-disabled);
|
|
--sh-btn-border: var(--interactive-primary-border);
|
|
--sh-btn-border-hover: var(--interactive-primary-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-primary-border-disabled);
|
|
}
|
|
|
|
.sh-btn[data-variant="secondary"] {
|
|
--sh-btn-bg: var(--interactive-secondary-bg);
|
|
--sh-btn-bg-hover: var(--interactive-secondary-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-secondary-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-secondary-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-secondary-fg);
|
|
--sh-btn-fg-hover: var(--interactive-secondary-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-secondary-fg-disabled);
|
|
--sh-btn-border: var(--interactive-secondary-border);
|
|
--sh-btn-border-hover: var(--interactive-secondary-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-secondary-border-disabled);
|
|
}
|
|
|
|
.sh-btn[data-variant="dark"] {
|
|
--sh-btn-bg: var(--interactive-dark-bg);
|
|
--sh-btn-bg-hover: var(--interactive-dark-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-dark-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-dark-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-dark-fg);
|
|
--sh-btn-fg-hover: var(--interactive-dark-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-dark-fg-disabled);
|
|
--sh-btn-border: var(--interactive-dark-border);
|
|
--sh-btn-border-hover: var(--interactive-dark-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-dark-border-disabled);
|
|
}
|
|
|
|
.sh-btn[data-variant="danger"] {
|
|
--sh-btn-bg: var(--interactive-danger-bg);
|
|
--sh-btn-bg-hover: var(--interactive-danger-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-danger-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-danger-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-danger-fg);
|
|
--sh-btn-fg-hover: var(--interactive-danger-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-danger-fg-disabled);
|
|
--sh-btn-border: var(--interactive-danger-border);
|
|
--sh-btn-border-hover: var(--interactive-danger-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-danger-border-disabled);
|
|
}
|
|
|
|
.sh-btn[data-variant="ghost"] {
|
|
--sh-btn-bg: var(--interactive-ghost-bg);
|
|
--sh-btn-bg-hover: var(--interactive-ghost-bg-hover);
|
|
--sh-btn-bg-active: var(--interactive-ghost-bg-active);
|
|
--sh-btn-bg-disabled: var(--interactive-ghost-bg-disabled);
|
|
--sh-btn-fg: var(--interactive-ghost-fg);
|
|
--sh-btn-fg-hover: var(--interactive-ghost-fg-hover);
|
|
--sh-btn-fg-disabled: var(--interactive-ghost-fg-disabled);
|
|
--sh-btn-border: var(--interactive-ghost-border);
|
|
--sh-btn-border-hover: var(--interactive-ghost-border-hover);
|
|
--sh-btn-border-disabled: var(--interactive-ghost-border-disabled);
|
|
}
|
|
|
|
/* Figma gives the ghost md/lg cells radius.md while every other cell uses
|
|
radius.sm - reproduced rather than normalised. */
|
|
.sh-btn[data-variant="ghost"][data-size="md"],
|
|
.sh-btn[data-variant="ghost"][data-size="lg"] {
|
|
border-radius: var(--radius-control-lg);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
States
|
|
------------------------------------------------------------------ */
|
|
@media (hover: hover) {
|
|
.sh-btn:hover:not(:disabled, [aria-disabled="true"]) {
|
|
background-color: var(--sh-btn-bg-hover);
|
|
color: var(--sh-btn-fg-hover);
|
|
border-color: var(--sh-btn-border-hover);
|
|
}
|
|
}
|
|
|
|
/* No `-fg-active` / `-border-active` exists in the token contract, so the
|
|
pressed state deepens the fill only. */
|
|
.sh-btn:active:not(:disabled, [aria-disabled="true"]) {
|
|
background-color: var(--sh-btn-bg-active);
|
|
}
|
|
|
|
.sh-btn:focus-visible {
|
|
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
outline-offset: var(--focus-ring-offset);
|
|
}
|
|
|
|
/* A green ring on a green (or red, or near-black) fill reads as nothing. */
|
|
.sh-btn[data-variant="primary"]:focus-visible,
|
|
.sh-btn[data-variant="danger"]:focus-visible,
|
|
.sh-btn[data-variant="dark"]:focus-visible {
|
|
outline-color: var(--focus-ring-color-inverse);
|
|
}
|
|
|
|
.sh-btn:disabled,
|
|
.sh-btn[aria-disabled="true"] {
|
|
background-color: var(--sh-btn-bg-disabled);
|
|
color: var(--sh-btn-fg-disabled);
|
|
border-color: var(--sh-btn-border-disabled);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
Loading - the label keeps its box (no reflow) and stays in the
|
|
accessibility tree, which `visibility: hidden` would not allow.
|
|
------------------------------------------------------------------ */
|
|
.sh-btn[data-loading="true"] {
|
|
cursor: progress;
|
|
}
|
|
|
|
.sh-btn[data-loading="true"] .sh-btn__content {
|
|
opacity: 0;
|
|
}
|
|
|
|
.sh-btn__spinner {
|
|
grid-area: sh-btn-stack;
|
|
inline-size: 1em;
|
|
block-size: 1em;
|
|
border: var(--control-border-width) solid currentColor;
|
|
border-block-start-color: transparent;
|
|
border-radius: var(--radius-pill);
|
|
/* The contract publishes fast/base/slow only; two slows is the ~640ms a
|
|
spinner wants without inventing a duration token. */
|
|
animation: sh-btn-spin calc(var(--motion-duration-slow) * 2) linear infinite;
|
|
}
|
|
}
|
|
|
|
@keyframes sh-btn-spin {
|
|
to {
|
|
rotate: 1turn;
|
|
}
|
|
}
|