Files
portal/DESIGN.md
T

14 KiB
Raw Blame History

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 — "; "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"; h1 → standfirst (description) → meta (category · author · date · read time) → cover → inline TOC → prose; italic muted captions; TOC at ≥3 headings; post-nav Guardian/BBC anatomy
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 —

Page composition (archetypes)

Every page follows one of three archetypes; a new page picks one instead of inventing a section order.

Archetype Pages Order
Portal landing / Hero (h1 + subtitle + CTA pair + one-line Latest teaser) → Services tiles (Moon) → Latest news 3-up + "All news" (white)
Section landing /news, /search, /proposals, /dashboard, /support Galaxy hero band carrying the h1 + one-line subtitle → single content band (Moon = utility, white = editorial listing) → footer
Document /news/<slug>, /pages/<slug> Back link → h1 → standfirst → meta → cover → inline TOC → prose (→ post-nav on articles); no hero band — the document IS the page
  • Home is task-first (NN/g homepage principles: clear starting points for the top 1–4 tasks — for scientists that's proposals/data/support, not reading news). The hero teaser is the freshness signal (deep-links the newest article); the 3-card section below is the browse layer — different granularity, not redundant.
  • Document order is press anatomy (Guardian/BBC/Reuters): headline → standfirst → byline/meta → lead image → body. The inline TOC sits between lead image and body (Wikipedia/GOV.UK contents position) so the editorial head stays unbroken and the list is adjacent to what it indexes.
  • Back affordances: articles "← All news", static pages "← Home" (footer- and deep-link-reached documents must not be dead ends). Auth (/login) is the sole exception to the archetypes: one focused card, no hero, no sections.
  • Service tiles carry a stroke glyph (.ico family: currentColor, round caps, aria-hidden; 26px Galaxy) ABOVE the label — never instead of it (NN/g icon usability: icons are ambiguous alone; beside an always-visible label they differentiate otherwise-uniform tiles and speed grid scanning). Glyphs are wayfinding, not decoration — they don't count against the one-accent budget.

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.