diff --git a/DESIGN.md b/DESIGN.md index db61cc0..3af81cf 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2,26 +2,44 @@ 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. +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/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) | +| `--galaxy` | `#00004A` | Headings, header/footer bands, hero scrim | +| `--cherenkov` | `#00E4D8` | ONLY: primary CTA, flash, hero subtitle, logo cluster (+ functional: ``, 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) | -| Everything else | Inter (`--font-body`) | Includes h3+, cards, UI | +| 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. @@ -31,10 +49,10 @@ Always end `font-family` with a generic (`sans-serif`) — lint requirement. |---|---|---| | `--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-m` | `24px` | 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 | +| `--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. @@ -43,197 +61,233 @@ Prose-internal margins (h2/p/blockquote) are content-local and stay as written. Radius: `--radius 16px` (cards/panels), `--radius-pill` (buttons, chips, inputs). -Only two shadows exist, both Galaxy-tinted (`rgba(0,0,74,…)`): +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, mobile menu), auth card | +| `--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 -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. +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 animates nebula videos under Galaxy vignettes; our translation: +ctao.org drifts looping 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 ``: **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. +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 ``: **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, 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). +- 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) -- 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. +- 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): `
` collapsed under the title -(< 1200px) and `