Files
portal/DESIGN.md
T

211 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.