Files
portal/DESIGN.md
T

17 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 (photography-led hero, nebula motion, generous spacing, 15/30/60 rounding, quiet-fade hovers). No frameworks, no JS for presentation.

Color tokens (brand-fixed — never add hues)

Token Value Use
--galaxy #00004A Headings, header/footer bands, hero scrim
--cherenkov #00E4D8 ONLY: primary CTA, flash, hero subtitle, logo cluster (+ functional: <mark>, TOC active rule). Never small text on light bg
--moon #F5F5F5 Utility surfaces (bands, neutral hovers)
--azure #007AFF Focus rings and field-focus borders ONLY (no hover borders)
--indigo #00009C Gradient depth, link hover
--text / --muted / --border / --link derived Body ink; --link #0057C2 is azure darkened to pass AA

Accent discipline: Cherenkov is rationed to the four brand moments above. Status pills, category/date chips and metadata are plain muted text — the label carries the meaning, not the chrome.

Typography

Role Font Rule
h1, h2 Space Grotesk (--font-display) Headlines ONLY (brand D.3.2), weight 500 (D.3.4 regular/medium), letter-spacing −0.025em, line-height 1.1
Everything else Inter (--font-body) Includes h3+ (weight 600), cards, UI

Type scale — the ONLY font sizes in the codebase (no ad-hoc rem values):

Token Value Use
--fs-xs 0.8125rem Meta, badges, labels, eyebrow, dates
--fs-s 0.9rem Secondary text, nav, forms, footer, TOC
(base) 1rem Body, line-height 1.5 (1.65 only on .prose; 1.55 .standfirst; 1.45 cards/footer)
--fs-l 1.125rem Card titles, standfirst, panel titles, hero subtitle
--fs-xl 1.5rem Prose h2, featured title, auth h1
--fs-h2 clamp(1.75–2.5rem) Section heads
--fs-h1 clamp(2.25–3.5rem) Page/article headlines
--fs-display clamp(2.5–4.75rem) Home hero only

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 24px Card/panel padding, grid gaps
--space-l 24–40px Article top, meta/cover margins, featured body
--space-xl clamp(56px, 7vw, 120px) Section rhythm: every .page block, footer top
--space-band clamp(72px, 10vw, 160px) Hero/auth band + landing-head 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, tuned soft/large/low-alpha (2025/26 elevation convention: big blur, single-digit alpha at rest):

Token Use
--shadow-ambient Resting cards, panels, search rows
--shadow-lifted Hover/focus cards, dropdowns (suggest, menu, search row), auth card

Never write a literal box-shadow; compose focus rings as 0 0 0 2px var(--azure), var(--shadow-lifted).

Surfaces

ONE surface per page — surface differentiation is by page archetype, never a mid-page seam (consistency heuristic: a surface change must read as deliberate system, not "why is this one gray?"):

  • Editorial pages (/news, articles, static pages, home news section) are white — reading surfaces.
  • Utility/app pages (/search, /proposals, /dashboard, /support) are .band--moon from the landing head down to the footer — the app canvas against which borderless white cards/panels pop (standard app-vs-editorial surface split; home's Services section uses the same Moon band).
  • / and /login own the navy bands (photo hero / nebula).

Cards and panels are always white; ON MOON they are borderless (white-on-moon separates by itself), on white they keep the hairline. Chrome is single-signal: hover = the elevate gesture (shadow + 2px lift as ONE motion); no hover borders, no cover zoom.

Motion policy (CSS-only, Apple-tier restraint)

ctao.org drifts looping nebula videos under Galaxy vignettes; our translation:

  1. Hero glow (home) — the photo hero keeps its scrimmed photograph; a --nebula-glow layer (Cherenkov .30 / Azure .28, no indigo over photography) drifts above it inside the .band-bg clip frame: 16s alternate, transform-only, clearly perceptible within ~10s. .band-bg (first child of the band) owns overflow: hidden — NEVER put overflow on the band itself (it would clip the search-suggest dropdown).
  2. Auth nebula (/login) — the full --nebula stack (Cherenkov .38 / Azure .33 / Indigo .80), same 16s drifting layer; static gradient is the reduced-motion fallback.
  3. Whole-page ambient — .has-ambient::before: fixed brand-hue wash (Cherenkov .16 / Azure .12 / Indigo .10) drifting on a 36s alternate cycle. Opt-in via <Base ambient>: home only. Body text NEVER sits on a moving gradient — cards, panels and bands are solid layers above it.
  4. One-shot hero rise stagger; unified hover system (below). Nothing else.

One hover language (mid-pass audit: hovers were inconsistent per element; ctao.org uses a single quiet-fade token — we do the same): every interactive element transitions color/background/border/shadow on --dur: 0.25s + --ease: ease-out — buttons and links fade, cards/search rows elevate (shadow + 2px lift), pagination fills Moon. No width/layout animation.

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); never background-position/box-shadow in keyframes. No will-change. No filter: blur on large layers and no background-attachment: fixed (broken on iOS). No JS, no scroll-triggered motion.
  • If an effect calls attention to itself as an effect, cut it.

Photographic hero (home)

Image-led hero per brand D.6 (photography theme: the project): LST-1 under the night sky, copied from the content library to public/brand/hero.jpg (NEVER link public/uploads directly — that directory is content-synced). Text safety comes from --hero-scrim (Galaxy, to right: .84 → .76 @52% → .44 @78% → .26); ≤640px a flat .80 scrim replaces it (text spans the full width on mobile). The logo never sits on the photograph — it stays in the header bar (B.1.2 satisfied by construction).

Computed worst-case contrast over photo + scrim + drifting glow (glow fringe α .15 Cherenkov / core .28 Azure over the text zone; Cherenkov core is pinned ≥80% band width by the blob geometry + drift range):

  • Small-text zone (eyebrow/subtitle/CTA labels, ≤52% band width): white 7.68:1 · Cherenkov subtitle 4.79:1 · eyebrow #C7CDE9 4.88:1 (all ≥4.5).
  • Display h1 zone (≤72% band width): white ≥5.20:1 (large text needs 3.0).
  • Mobile flat scrim: white 8.47:1 · subtitle 5.28:1 · eyebrow 5.38:1.

Recompute (script kept in the session scratchpad pattern: composite scrim → glow → WCAG luminance per pixel) if the photo, scrim stops, glow alphas or blob positions change.

WCAG 2.1 AA notes (computed, worst-case animation frames)

  • Photo hero: see table above. Auth nebula over Galaxy: white 7.9:1, Cherenkov subtitle 4.9:1 at the worst blob frame; Cherenkov/Azure CORES stay in opposite corners. Ceilings: nebula ≤ .38 Cherenkov / ≤ .35 Azure; glow ≤ .30/.28; wash ≤ .16/.12/.10 — recompute if any alpha or placement changes.
  • Translucent header (color-mix 88% Galaxy + backdrop blur, solid fallback via @supports): worst underlying content (white) → bar ≈ rgb(31,31,96); white text 14.8:1, muted nav #C7CDE9 9.4:1, Cherenkov sub-brand 9.2:1.
  • On Moon: --link 6.1:1, --muted 7.1:1, --text 16.9:1. Ambient wash worst overlap: --link 5.3:1, --muted 6.2:1.
  • 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), both in the quiet Stripe/Apple-docs voice — muted small links, Galaxy on hover, no heavy box:

  • <nav class="toc toc--rail"> (≥1200px): sticky whitespace column with a hairline LEFT rule, outside the 70ch column via the .article-layout grid; sticky offset 80px (56px header + 24), own scroll when long.
  • <details class="toc toc--inline"> (<1200px): light disclosure row ("On this page" + chevron from the .ico family), comfortable tap rows.

Scrollspy (Starlight 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, and on the rail a 2px Cherenkov segment over the hairline. Functional navigation state, not decoration (outside the one-accent budget), same precedent as the search <mark> highlight (Cherenkov tint) and the .pagination current pill. The TOC works fully without JS.

Header

56px bar (translucent Galaxy via color-mix + backdrop blur where supported, solid Galaxy fallback — see WCAG notes). One row always: .nav > * is flex: none, only the spacer flexes; nothing can wrap. Tiers: ≥1025px full nav · ≤1024px disclosure menu (<details>) · ≤640px sub-brand hidden · ≤480px icon-only menu · ≤360px compact paddings. Logo ≥22px tall everywhere (≈105px wide — brand minimum 100px digital).

Header search — decided by research, not aesthetics: NN/g's magnifying-glass -icon findings (icon-only search measurably reduces discoverability and use; show a visible box when search is an important task) beat the icon-only minimal header for this content-heavy, search-required portal (SPEC §3.3.1). Desktop ≥1025px: a VISIBLE quiet field (hairline on navy, 140–200px flex — the only flexing item, so the row never wraps). ≤1024px: the 44px magnifier <details> reveals a search row under the bar (NN/g mobile-search pattern). Both are plain GET /search (full no-JS path); <details name="header-panel"> makes search + menu a native exclusive pair. /search stays the full-results and deep-link target. The footer "Search" link STAYS: SPEC §3.3.1 literally places "a search functionality" in the bottom menu, and the footer is the end-of-page fallback (NN/g footers) — one quiet word, clean compliance trail. Sign in is a ghost/outline pill — Cherenkov fill is reserved for the page's primary CTA.

Nav content rule: the header nav reflects end-user tasks only (NN/g). The content editor is internal-team tooling (SPEC §3.2) → discreet "Content editor" utility link in the footer; /admin keeps working as the direct URL.

Content & microcopy

  • Card metadata must differentiate (NN/g): cards show date · reading time (body words ÷ 220 wpm, computed at build). Category and author remain on the article page as one quiet muted meta line (category · author · date · read time — plain text, no chips).
  • Honesty lives at the INTERACTION POINT (REQUIREMENTS §5): whisper badges ("Mock — <what's missing>", "Planned integration — ") appear where a mock interaction actually happens — /proposals panel, /dashboard, /login, /support page head. Navigation tiles carry no status chrome; planned services are subdued tiles (muted, no link) with "— planned" folded into the description. There is NO phase banner (removed as clutter — the footer demo note + interaction-point badges carry the disclosure).
  • Terminology 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> (mark-splitting). Dates are plain muted text, not chips.
  • 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.
  • Articles end with .post-nav (Newer ← → Older). Lead image is borderless and breaks out of the text column ≥900px. Image captions: italic muted line under the image.

Element rules (condensed)

Element Rule Source
Header nav End-user tasks only; order News · Data · Proposals · Dashboard · Support NN/g serial-position
Header search Icon-reveal row, all widths; plain GET /search NN/g site search
Hero (home) Photo (LST under night sky) + Galaxy scrim + eyebrow + headline + Cherenkov subtitle + CTA pair (Cherenkov primary, ghost secondary). Nothing else — no teaser line, no widgets, no second search Apple hero restraint
Badges Quiet hairline pill, muted sentence case, Galaxy dot on mocks; ONLY at mock interaction points REQUIREMENTS §5
Cards Cover · title (clamp 3) · desc (clamp 2) · date · read time; borderless on Moon; hover = elevate only NN/g metadata
Pagination Newer/Older + windowed numbers, 44px targets, aria-current, Moon hover fill NN/g pagination
Article Back "← All news"; h1 → standfirst → muted meta line → borderless breakout cover → inline TOC → prose → 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 + Cherenkov primary CTA + one small-print line; whisper badge top-right of the card (in flow ≤480px); no reassurance prose auth brevity
Footer Identity line → Contact · Search · Disclaimer · Privacy · Site settings · Content editor → © line NN/g footers, SPEC §3.3.1
Search results Links lay out as flowing text (mark-safe); plain muted date; "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. The navy band is reserved for / and /login.

Archetype Pages Order
Portal landing / Photo hero (h1 + subtitle + CTA pair) → Services tiles (Moon) → Latest news 3-up + "All news" (white)
Section landing /news, /search, /proposals, /dashboard, /support Landing head (.page-head: display Galaxy h1 + standfirst, --space-band top padding, no eyebrow, no navy band) → content → footer. The WHOLE page sits on its archetype surface: white (editorial /news) or Moon (utility) — head included, one surface per page
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): Services directly under the hero; the news section below is the browse layer.
  • Document order is press anatomy (Guardian/BBC/Reuters). The inline TOC sits between lead image and body.
  • Back affordances: articles "← All news", static pages "← Home". Auth (/login) is the sole exception to the archetypes: one focused card on the nebula band, 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. Glyphs are wayfinding, not decoration.

Do not

  • No new hues, tints, or grays — derive via color-mix from brand tokens only.
  • No Space Grotesk outside h1/h2; headline weight stays 500 (brand D.3.4).
  • No font sizes outside the type-scale tokens; no ad-hoc spacing/shadow/radius.
  • No animation/transition outside the no-preference block; no JS motion; no transition durations/easings outside --dur/--ease.
  • No dependencies/frameworks (CSS or JS). No removal of mock disclosure at interaction points. No phase banner — do not reintroduce it.
  • No Cherenkov outside: primary CTA, flash, hero subtitle, logo cluster (+ the two functional states listed under Color tokens). No hover borders.
  • Don't lighten text over navy/photo bands below the computed ceilings 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.
  • Never link public/uploads from layout/brand surfaces — copy to public/brand/ instead (uploads is content-synced).