6.3 KiB
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:
- Band nebula — one
--nebulastack (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::beforelayer drifting viatransformonly, 22s alternate. Clearly visible glow, never neon..band-bg(first child of every band) owns theoverflow: hiddenclip — NEVER put overflow on the band itself: it would clip overlays like the search-suggest dropdown. - 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. - One-shot hero
risestagger; hover lift/transitions. Nothing else.
Rules:
- ALL
animation/transitiondeclarations live inside the single@media (prefers-reduced-motion: no-preference)block at the end of global.css; thereduceblock additionally force-disables everything. - Animate
transform/opacityonly (compositor-only, per web.dev/MDN); never background-position/box-shadow in keyframes. Nowill-change(animated properties are auto-promoted — MDN). Nofilter: bluron large layers and nobackground-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
#C7CDE95.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:
--link6.1:1,--muted7.1:1,--text16.9:1. - Ambient wash worst overlap (
rgb(188,237,249)):--link5.3:1,--muted6.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.
Do not
- No new hues, tints, or grays — derive via
color-mixfrom 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-preferenceblock; 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.