diff --git a/.gitignore b/.gitignore index de35ccf..605b2a3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,11 @@ node_modules/ dist/ .astro/ .DS_Store + +# Content lives in the ctao/content repo (build overlays it; scripts/link-content.sh locally) +src/content/news/ +src/content/pages/ +public/uploads/ + +# Session scratch +.tmp/ diff --git a/DESIGN.md b/DESIGN.md index c281ca5..0a4990c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,337 +1,32 @@ -# CTAO Science Portal — design system +# Design handoff -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. +All styling lives in **`src/styles/global.css`** — design tokens in `:root` +(colors, type scale, spacing, radius, shadows, `--header-h`) + plain-CSS +components below them. No frameworks, no preprocessors, no runtime JS for +presentation. Pages are `.astro` templates (plain HTML). -## Color tokens (brand-fixed — never add hues) +## Brand (official: CTAO Brand Guidelines, April 2024 — PDF in the project docs) -| Token | Value | Use | -|---|---|---| -| `--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 | +- Colors: Galaxy Blue `#00004A`, Cherenkov Blue `#00E4D8`, Moon Gray + `#F5F5F5`, Cosmic Azure `#007AFF` (links/focus), Interstellar Indigo + `#00009C`. Cherenkov is an ACCENT (brand book rations it — currently: + primary CTA, hero subtitle, logo flash); it fails AA as text on white. +- Type: **Inter** for everything, **Space Grotesk** (weight 500) only for + communicative headlines (h1/h2). Sizes come from the `--fs-*` scale. +- Logo files in `public/brand/`; always "CTAO", min 100px wide, no effects. -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. +## Practical notes (the non-obvious ones) -## 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 | -| Exception: `.card--featured h3` | Space Grotesk | The news-lead card IS a communicative headline (D.3.2 role, not outline level) — weight 500, `--fs-xl` | -| Exception: `.panel-title` (h2) | Inter | Panel titles are UI labels, not headlines — weight 600, `--fs-l`. These are the ONLY two exceptions | - -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, article headlines (news titles run long — full `--fs-h1` would fill the viewport) | -| `--fs-h1` | clamp(2.25–3.5rem) | Short page heads only ("News & Announcements") | -| `--fs-display` | clamp(2.5–4.75rem) | Home hero only | - -Font tokens end with a generic (`sans-serif`); use them bare — -`var(--font-body), sans-serif` doubles the generic. - -Font loading (prevents any navigation blink): BOTH families are inlined as -base64 `data:` URIs inside the render-blocking `global.css` — no separate -font fetch exists, so there is no swap window and no font flash between -navigations. Metric-matched Arial fallbacks (`Inter Fallback`, -`Space Grotesk Fallback` — size-adjust/ascent/descent computed from the real -font metrics, capsize method) cover only the pre-CSS paint. The standalone -`public/fonts/*.woff2` files are referenced ONLY by -`public/admin/preview.css` (CMS entry preview) — they look unused but aren't. - -## 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 ``: **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). -5. **CSS scroll-driven effects** (2026 pass; `animation-timeline`, no JS — - "no scroll-triggered motion" below means no JS/observer motion): card - reveal on viewport entry (`view()`, entry 0–35%), header shadow fading in - over the first 80px of scroll, article reading-progress bar (2px Cherenkov - under the header — functional navigation state, same budget line as the - TOC scrollspy). All behind `@supports (animation-timeline: view())`, - compositor-only (the header shadow animates a pseudo-element's OPACITY, - never box-shadow in keyframes), auto-killed by the reduce block. -6. Micro-interactions: nav underline-grow (2px white rule, `scaleX` 0→1 on - hover, held on `aria-current`) and 1px press-down on `.btn`/navlink - `:active`. 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. -- NO cross-document view transitions (`@view-transition`). The root crossfade - snapshots the incoming page at its first render opportunity — on slow - networks that is a half-parsed page, so every navigation read as a - full-page blink (the diagnosed symptom); scrolled→top navigations crossfade - two unrelated frames even when it works. Navigation feel comes from static - HTML + Astro prefetch, not from animating the swap. -- 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: - -- `