211 lines
12 KiB
Markdown
211 lines
12 KiB
Markdown
# CTAO Science Portal — design system
|
||
|
||
Single source of truth for look-and-feel. All values live as tokens in
|
||
`src/styles/global.css`; this file states the rules. Brand basis: CTAO Brand
|
||
Guidelines (Apr 2024) + techniques observed on ctao.org (nebula motion, generous
|
||
spacing, 15/30/60 rounding). No frameworks, no JS for presentation.
|
||
|
||
## Color tokens (brand-fixed — never add hues)
|
||
|
||
| Token | Value | Use |
|
||
|---|---|---|
|
||
| `--galaxy` | `#00004A` | Headings, header/footer/hero bands |
|
||
| `--cherenkov` | `#00E4D8` | Accents, flash, CTAs, hero subtitle. Never small text on light bg |
|
||
| `--moon` | `#F5F5F5` | Utility surfaces (bands, TOC, chips) |
|
||
| `--azure` | `#007AFF` | Focus rings, hover borders (UI only on light) |
|
||
| `--indigo` | `#00009C` | Gradient depth, link hover |
|
||
| `--text` / `--muted` / `--border` / `--link` | derived | Body ink; `--link #0057C2` is azure darkened to pass AA |
|
||
|
||
## Typography
|
||
|
||
| Role | Font | Rule |
|
||
|---|---|---|
|
||
| h1, h2 | Space Grotesk (`--font-display`) | Headlines ONLY (brand D.3.2) |
|
||
| Everything else | Inter (`--font-body`) | Includes h3+, cards, UI |
|
||
|
||
Always end `font-family` with a generic (`sans-serif`) — lint requirement.
|
||
|
||
## Spacing scale (fluid; mobile ≈ min, desktop ≈ max)
|
||
|
||
| Token | Value | Use |
|
||
|---|---|---|
|
||
| `--gutter` | `clamp(18px, 4vw, 28px)` | Container/menu side padding |
|
||
| `--space-s` | `12px` | Chip gaps, small stacks |
|
||
| `--space-m` | `20px` | Card/panel padding, grid gaps |
|
||
| `--space-l` | `24–40px` | Article top, meta/cover margins, featured body |
|
||
| `--space-xl` | `40–72px` | Section rhythm: every `.page` block, footer top |
|
||
| `--space-band` | `48–96px` | Hero + auth band vertical padding |
|
||
|
||
Rule: section/container/card padding comes from this table — no ad-hoc px.
|
||
Prose-internal margins (h2/p/blockquote) are content-local and stay as written.
|
||
|
||
## Radius & elevation
|
||
|
||
Radius: `--radius 16px` (cards/panels), `--radius-pill` (buttons, chips, inputs).
|
||
|
||
Only two shadows exist, both Galaxy-tinted (`rgba(0,0,74,…)`):
|
||
|
||
| Token | Use |
|
||
|---|---|
|
||
| `--shadow-ambient` | Resting cards, panels, search rows |
|
||
| `--shadow-lifted` | Hover/focus cards, dropdowns (suggest, mobile menu), auth card |
|
||
|
||
Never write a literal `box-shadow`; compose focus rings as
|
||
`0 0 0 2px var(--azure), var(--shadow-lifted)`.
|
||
|
||
## Surfaces
|
||
|
||
Rhythm per page: Galaxy hero band → content → Galaxy footer (flush, no margin).
|
||
White = editorial (news grids, articles). `.band--moon` = service/utility
|
||
sections (home Services, /search, /support, /proposals, /dashboard). Cards and
|
||
panels are always white and pop against Moon via border + ambient shadow.
|
||
|
||
## Motion policy (CSS-only, Apple-tier restraint)
|
||
|
||
ctao.org animates nebula videos under Galaxy vignettes; our translation:
|
||
|
||
1. **Band nebula** — one `--nebula` stack (Cherenkov .38 / Azure .33 /
|
||
Indigo .80 radial fields) used by hero + auth bands, static and animated
|
||
alike; the animated variant is an oversized `.band-bg::before` layer
|
||
drifting via `transform` only, 22s alternate. Clearly visible glow, never
|
||
neon. `.band-bg` (first child of every band) owns the `overflow: hidden`
|
||
clip — NEVER put overflow on the band itself: it would clip overlays like
|
||
the search-suggest dropdown.
|
||
2. **Whole-page ambient** — `.has-ambient::before`: fixed brand-hue wash
|
||
(Cherenkov .16 / Azure .12 / Indigo .10 — visible pastel fields, white
|
||
cards pop against them) drifting on a 60s alternate cycle. Opt-in per page
|
||
via `<Base ambient>`: **home only**. Login's full-viewport auth nebula
|
||
already is its ambient; news/article/utility pages stay calm (reading
|
||
focus). Body text NEVER sits on a moving gradient — cards, panels and
|
||
bands are solid layers above it.
|
||
3. One-shot hero `rise` stagger; hover lift/transitions, incl. the card-cover
|
||
micro-zoom (scale 1.03, clipped by the card — AstroWind pattern). Nothing else.
|
||
|
||
Rules:
|
||
- ALL `animation`/`transition` declarations live inside the single
|
||
`@media (prefers-reduced-motion: no-preference)` block at the end of
|
||
global.css; the `reduce` block additionally force-disables everything.
|
||
- Animate `transform`/`opacity` only (compositor-only, per web.dev/MDN); never
|
||
background-position/box-shadow in keyframes. No `will-change` (animated
|
||
properties are auto-promoted — MDN). No `filter: blur` on large layers and no
|
||
`background-attachment: fixed` (broken on iOS) — the wash is a fixed-position
|
||
layer with pre-blurred radial gradients. No JS, no scroll-triggered motion.
|
||
- Static gradients remain as the reduced-motion fallback (bands swap them for
|
||
the animated layer — never both, keeping contrast bounded; the ambient wash
|
||
simply stops moving).
|
||
- If an effect calls attention to itself as an effect, cut it.
|
||
|
||
## WCAG 2.1 AA notes (computed, worst-case animation frames)
|
||
|
||
- Nebula blob peaks over Galaxy: Cherenkov α=.38 → white 7.9:1, Cherenkov
|
||
subtitle 4.9:1, eyebrow `#C7CDE9` 5.0:1. Azure α=.33 → all ≥ 7.7:1.
|
||
Indigo α=.80 → all ≥ 9.6:1. Cherenkov/Azure CORES must stay in opposite
|
||
corners (core-over-core would fail; fringe overlap ≈ 6:1).
|
||
- On Moon: `--link` 6.1:1, `--muted` 7.1:1, `--text` 16.9:1.
|
||
- Ambient wash worst overlap (`rgb(188,237,249)`): `--link` 5.3:1, `--muted`
|
||
6.2:1, Galaxy h2 15.2:1.
|
||
- Ceilings: nebula ≤ .38 Cherenkov / ≤ .35 Azure; wash ≤ .16/.12/.10 —
|
||
recompute (WCAG relative luminance) if any alpha or hue placement changes.
|
||
- Focus visible everywhere; tap targets ≥ 44px; skip link first.
|
||
|
||
## Article TOC pattern
|
||
|
||
Rendered only when ≥3 h2/h3. Two renderings of the same list (only one ever
|
||
displayed): `<details class="toc toc--inline">` collapsed under the title
|
||
(< 1200px) and `<nav class="toc toc--rail">` sticky right rail (≥ 1200px,
|
||
outside the 70ch column via `.article-layout` grid). MDN/Stripe/NN-g pattern.
|
||
|
||
Scrollspy (Starlight's starlight-toc pattern, minimal vanilla): an
|
||
IntersectionObserver band under the sticky header maps the visible prose block
|
||
to its governing heading and sets `aria-current="true"` on the TOC link
|
||
(Galaxy + 600 weight). Functional state, not motion — the "no JS motion" rule
|
||
is untouched; the TOC works fully without JS. Same precedent covers the search
|
||
`<mark>` term highlight (Cherenkov tint) and the `.pagination` pill nav
|
||
(Galaxy pill = current page, 44px targets).
|
||
|
||
## Header search & one-row width math
|
||
|
||
Search lives in the header. Desktop (≥1025px): compact GET-form pill (flex
|
||
130–180px) in the utility cluster; suggestions reuse `.suggest`. ≤1024px: a
|
||
44px magnifier `<details>` reveals a full-width search row under the header
|
||
(visible search beats search buried in a menu — NN/g mobile search). Both
|
||
forms are plain `GET /search` (full no-JS path); `<details name="header-panel">`
|
||
makes search + menu a native exclusive pair (no JS). The `/search` page stays
|
||
the full-results and deep-link target.
|
||
|
||
Nav content rule: the header nav reflects end-user tasks only (NN/g —
|
||
navigation mirrors user goals, not org tooling). The content editor is
|
||
internal-team tooling (SPEC §3.2), so it lives as a discreet "Content editor"
|
||
utility link in the footer, pointed to by the demo note; /admin keeps working
|
||
as the direct URL.
|
||
|
||
One-row invariant: `.nav > *` is `flex: none`; only the spacer and the search
|
||
field flex, so the row can never wrap or overflow. Tier math (Inter widths
|
||
estimated ±8%; logo 153:32 → 124px at 26px height; inner width = viewport −
|
||
2×gutter, container caps at 1120):
|
||
|
||
| Tier | Budget | Available |
|
||
|---|---|---|
|
||
| ≥1100 | brand+sub 247 + nav 415 + search 186 + Sign in 91 + gaps 16 ≈ 955 | 1044 @1100 |
|
||
| 1025–1099 (sub-brand hidden) | ≈ 842 | 969 @1025 |
|
||
| 481–1024 (menu + magnifier) | brand 134 + magnifier 44 + Sign in 91 + Menu 92 + gaps 16 ≈ 377 | 445 @481 |
|
||
| ≤480 (logo 22px, icon-only menu) | ≈ 312 | 339 @375 |
|
||
| ≤360 (compact paddings, gap 2) | ≈ 277 | 292 @320 |
|
||
|
||
The search field's 50px shrink range (180 → 130) absorbs estimate error in
|
||
every tier. No focus-grow on the field: a width transition would be a layout
|
||
animation (motion policy allows transform/opacity only) and an unanimated
|
||
snap is worse than a calm fixed width.
|
||
|
||
## Content & microcopy
|
||
|
||
- Card metadata must differentiate (NN/g: omit non-differentiating metadata):
|
||
category and author are identical on all articles, so cards show
|
||
date · reading time (body words ÷ 220 wpm, computed at build). Category and
|
||
author remain on the article page; meta order there is
|
||
category · author · date · read time.
|
||
- Badges: "Mock — <what's missing>" (bare "Mock" only on tiles); "External —
|
||
<team>"; "Planned — not yet available". Mock badges are never removed.
|
||
- Terminology is fixed: "Sign in" (never Log in), "Editor", "Search", "News".
|
||
Buttons start with a verb, sentence case. Real ellipsis (…), never "...".
|
||
- Search links (`.suggest a`, `.search-results a`) lay out as flowing text —
|
||
never `display: flex` on the `<a>` itself: `<mark>` term-highlight splits the
|
||
title into fragments, and flex would turn each into its own item.
|
||
- Overflow containment happens at the source: wide `.prose` tables scroll in
|
||
their own box, long words/URLs wrap via `overflow-wrap`. The
|
||
`html, body { overflow-x: clip }` rule is a regression guard, not a fix —
|
||
never rely on it for new components.
|
||
- Articles end with `.post-nav` (Newer ← → Older from the date-sorted
|
||
collection). Image captions: italic line under the image
|
||
(`.prose img + em` / `p > em:only-child`), muted and smaller.
|
||
|
||
### Element rules (condensed)
|
||
|
||
| Element | Rule | Source |
|
||
|---|---|---|
|
||
| Phase banner | THE one loud demo disclosure: Moon bar above header, "Demo" tag + one sentence | GOV.UK phase banner |
|
||
| Header nav | End-user tasks only; order News · Data · Proposals · Dashboard · Support (entry first, help last, personal area next to Sign in) | NN/g serial-position |
|
||
| Header search | Compact field ≥1025px, magnifier reveal ≤1024px; both plain GET /search | NN/g site search |
|
||
| Hero (home) | Headline + brand subtitle + two task CTAs (Cherenkov primary, ghost secondary) + one-line Latest teaser. Never a second search box; no fake live widgets | hero-CTA convention, one-search-location |
|
||
| Badges | Quiet hairline pill, muted sentence-case text, Galaxy dot on mocks; "Mock — reason"; never removed | REQUIREMENTS §5, NN/g hierarchy |
|
||
| Cards | Cover · title (clamp 3) · desc (clamp 2) · date · read time; no non-differentiating metadata | NN/g metadata |
|
||
| Pagination | Newer/Older + windowed numbers, 44px targets, aria-current | NN/g pagination |
|
||
| Article | Back "← All news"; meta category · author · date · read time; italic muted captions; TOC at ≥3 headings; post-nav | editorial convention |
|
||
| Forms | Visible labels above fields; placeholders are examples only; buttons start with a verb | GOV.UK forms, NN/g |
|
||
| Login | One line + button + one small-print line; no reassurance prose | auth brevity |
|
||
| Footer | Identity line → Contact · Search · Disclaimer · Privacy · Site settings · Content editor → © line; no implementation talk in UI copy | NN/g footers |
|
||
| Search results | Links lay out as flowing text (mark-safe); date chip; "Press Enter for all results" hint | — |
|
||
|
||
## Do not
|
||
|
||
- No new hues, tints, or grays — derive via `color-mix` from brand tokens only.
|
||
- No Space Grotesk outside h1/h2. No dark theme.
|
||
- No ad-hoc spacing/shadow/radius values; extend the token tables instead.
|
||
- No animation/transition outside the `no-preference` block; no JS motion.
|
||
- No dependencies/frameworks (CSS or JS). No removal of "Mock" badges.
|
||
- Don't lighten text over navy bands below the alphas listed above.
|
||
- No clip-path/CSS approximations of brand assets — the flash is the real SVG
|
||
path from the official logo, or nothing.
|
||
- Decoration budget: max ONE accent element per view (currently: footer flash,
|
||
static). No twinkles, floating shapes, or stacked decorations.
|