feat: Apple-grade pass — scale, type, photographic hero, de-noise, single-signal chrome, lighter header

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jWfn3RwPfFGTtBddm36Uy
This commit is contained in:
ctao
2026-07-25 11:47:30 +02:00
committed by ctao
co-authored by Claude Fable 5
parent 3202b853a7
commit eaf511a976
11 changed files with 525 additions and 429 deletions
+197 -143
View File
@@ -2,26 +2,44 @@
Single source of truth for look-and-feel. All values live as tokens in 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 `src/styles/global.css`; this file states the rules. Brand basis: CTAO Brand
Guidelines (Apr 2024) + techniques observed on ctao.org (nebula motion, generous Guidelines (Apr 2024) + techniques observed on ctao.org (photography-led hero,
spacing, 15/30/60 rounding). No frameworks, no JS for presentation. nebula motion, generous spacing, 15/30/60 rounding, quiet-fade hovers).
No frameworks, no JS for presentation.
## Color tokens (brand-fixed — never add hues) ## Color tokens (brand-fixed — never add hues)
| Token | Value | Use | | Token | Value | Use |
|---|---|---| |---|---|---|
| `--galaxy` | `#00004A` | Headings, header/footer/hero bands | | `--galaxy` | `#00004A` | Headings, header/footer bands, hero scrim |
| `--cherenkov` | `#00E4D8` | Accents, flash, CTAs, hero subtitle. Never small text on light bg | | `--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, TOC, chips) | | `--moon` | `#F5F5F5` | Utility surfaces (bands, neutral hovers) |
| `--azure` | `#007AFF` | Focus rings, hover borders (UI only on light) | | `--azure` | `#007AFF` | Focus rings and field-focus borders ONLY (no hover borders) |
| `--indigo` | `#00009C` | Gradient depth, link hover | | `--indigo` | `#00009C` | Gradient depth, link hover |
| `--text` / `--muted` / `--border` / `--link` | derived | Body ink; `--link #0057C2` is azure darkened to pass AA | | `--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 ## Typography
| Role | Font | Rule | | Role | Font | Rule |
|---|---|---| |---|---|---|
| h1, h2 | Space Grotesk (`--font-display`) | Headlines ONLY (brand D.3.2) | | 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+, cards, UI | | 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. 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 | | `--gutter` | `clamp(18px, 4vw, 28px)` | Container/menu side padding |
| `--space-s` | `12px` | Chip gaps, small stacks | | `--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-l` | `24–40px` | Article top, meta/cover margins, featured body |
| `--space-xl` | `40–72px` | Section rhythm: every `.page` block, footer top | | `--space-xl` | `clamp(56px, 7vw, 120px)` | Section rhythm: every `.page` block, footer top |
| `--space-band` | `48–96px` | Hero + auth band vertical padding | | `--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. 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. 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). 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 | | Token | Use |
|---|---| |---|---|
| `--shadow-ambient` | Resting cards, panels, search rows | | `--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 Never write a literal `box-shadow`; compose focus rings as
`0 0 0 2px var(--azure), var(--shadow-lifted)`. `0 0 0 2px var(--azure), var(--shadow-lifted)`.
## Surfaces ## Surfaces
Rhythm per page: Galaxy hero band → content → Galaxy footer (flush, no margin). ONE surface per page — surface differentiation is by page archetype, never a
White = editorial (news grids, articles). `.band--moon` = service/utility mid-page seam (consistency heuristic: a surface change must read as deliberate
sections (home Services, /search, /support, /proposals, /dashboard). Cards and system, not "why is this one gray?"):
panels are always white and pop against Moon via border + ambient shadow.
- **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) ## 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 / 1. **Hero glow (home)** — the photo hero keeps its scrimmed photograph; a
Indigo .80 radial fields) used by hero + auth bands, static and animated `--nebula-glow` layer (Cherenkov .30 / Azure .28, no indigo over
alike; the animated variant is an oversized `.band-bg::before` layer photography) drifts above it inside the `.band-bg` clip frame: 16s
drifting via `transform` only, 22s alternate. Clearly visible glow, never alternate, transform-only, clearly perceptible within ~10s. `.band-bg`
neon. `.band-bg` (first child of every band) owns the `overflow: hidden` (first child of the band) owns `overflow: hidden` — NEVER put overflow on
clip — NEVER put overflow on the band itself: it would clip overlays like the band itself (it would clip the search-suggest dropdown).
the search-suggest dropdown. 2. **Auth nebula (/login)** — the full `--nebula` stack (Cherenkov .38 /
2. **Whole-page ambient** — `.has-ambient::before`: fixed brand-hue wash Azure .33 / Indigo .80), same 16s drifting layer; static gradient is the
(Cherenkov .16 / Azure .12 / Indigo .10 — visible pastel fields, white reduced-motion fallback.
cards pop against them) drifting on a 60s alternate cycle. Opt-in per page 3. **Whole-page ambient** — `.has-ambient::before`: fixed brand-hue wash
via `<Base ambient>`: **home only**. Login's full-viewport auth nebula (Cherenkov .16 / Azure .12 / Indigo .10) drifting on a 36s alternate
already is its ambient; news/article/utility pages stay calm (reading cycle. Opt-in via `<Base ambient>`: **home only**. Body text NEVER sits on
focus). Body text NEVER sits on a moving gradient — cards, panels and a moving gradient — cards, panels and bands are solid layers above it.
bands are solid layers above it. 4. One-shot hero `rise` stagger; unified hover system (below). Nothing else.
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. **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: Rules:
- ALL `animation`/`transition` declarations live inside the single - ALL `animation`/`transition` declarations live inside the single
`@media (prefers-reduced-motion: no-preference)` block at the end of `@media (prefers-reduced-motion: no-preference)` block at the end of
global.css; the `reduce` block additionally force-disables everything. global.css; the `reduce` block additionally force-disables everything.
- Animate `transform`/`opacity` only (compositor-only, per web.dev/MDN); never - Animate `transform`/`opacity` only (compositor-only); never
background-position/box-shadow in keyframes. No `will-change` (animated background-position/box-shadow in keyframes. No `will-change`. No
properties are auto-promoted — MDN). No `filter: blur` on large layers and no `filter: blur` on large layers and no `background-attachment: fixed`
`background-attachment: fixed` (broken on iOS) — the wash is a fixed-position (broken on iOS). No JS, no scroll-triggered motion.
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. - 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) ## WCAG 2.1 AA notes (computed, worst-case animation frames)
- Nebula blob peaks over Galaxy: Cherenkov α=.38 → white 7.9:1, Cherenkov - Photo hero: see table above. Auth nebula over Galaxy: white 7.9:1,
subtitle 4.9:1, eyebrow `#C7CDE9` 5.0:1. Azure α=.33 → all ≥ 7.7:1. Cherenkov subtitle 4.9:1 at the worst blob frame; Cherenkov/Azure CORES stay
Indigo α=.80 → all ≥ 9.6:1. Cherenkov/Azure CORES must stay in opposite in opposite corners. Ceilings: nebula ≤ .38 Cherenkov / ≤ .35 Azure; glow
corners (core-over-core would fail; fringe overlap ≈ 6:1). ≤ .30/.28; wash ≤ .16/.12/.10 — recompute if any alpha or placement changes.
- On Moon: `--link` 6.1:1, `--muted` 7.1:1, `--text` 16.9:1. - Translucent header (`color-mix` 88% Galaxy + backdrop blur, solid fallback
- Ambient wash worst overlap (`rgb(188,237,249)`): `--link` 5.3:1, `--muted` via `@supports`): worst underlying content (white) → bar ≈ rgb(31,31,96);
6.2:1, Galaxy h2 15.2:1. white text 14.8:1, muted nav `#C7CDE9` 9.4:1, Cherenkov sub-brand 9.2:1.
- Ceilings: nebula ≤ .38 Cherenkov / ≤ .35 Azure; wash ≤ .16/.12/.10 — - On Moon: `--link` 6.1:1, `--muted` 7.1:1, `--text` 16.9:1. Ambient wash
recompute (WCAG relative luminance) if any alpha or hue placement changes. worst overlap: `--link` 5.3:1, `--muted` 6.2:1.
- Focus visible everywhere; tap targets ≥ 44px; skip link first. - Focus visible everywhere; tap targets ≥ 44px; skip link first.
## Article TOC pattern ## Article TOC pattern
Rendered only when ≥3 h2/h3. Two renderings of the same list (only one ever 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 displayed), both in the quiet Stripe/Apple-docs voice — muted small links,
(< 1200px) and `<nav class="toc toc--rail">` sticky right rail (≥ 1200px, Galaxy on hover, no heavy box:
outside the 70ch column via `.article-layout` grid). MDN/Stripe/NN-g pattern.
Scrollspy (Starlight's starlight-toc pattern, minimal vanilla): an - `<nav class="toc toc--rail">` (≥1200px): sticky whitespace column with a
IntersectionObserver band under the sticky header maps the visible prose block hairline LEFT rule, outside the 70ch column via the `.article-layout` grid;
to its governing heading and sets `aria-current="true"` on the TOC link sticky offset 80px (56px header + 24), own scroll when long.
(Galaxy + 600 weight). Functional state, not motion — the "no JS motion" rule - `<details class="toc toc--inline">` (<1200px): light disclosure row
is untouched; the TOC works fully without JS. Same precedent covers the search ("On this page" + chevron from the `.ico` family), comfortable tap rows.
`<mark>` term highlight (Cherenkov tint) and the `.pagination` pill nav
(Galaxy pill = current page, 44px targets).
## Header search & one-row width math 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.
Search lives in the header. Desktop (≥1025px): compact GET-form pill (flex ## Header
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 — 56px bar (translucent Galaxy via `color-mix` + backdrop blur where supported,
navigation mirrors user goals, not org tooling). The content editor is solid Galaxy fallback — see WCAG notes). One row always: `.nav > *` is
internal-team tooling (SPEC §3.2), so it lives as a discreet "Content editor" `flex: none`, only the spacer flexes; nothing can wrap. Tiers: ≥1025px full
utility link in the footer, pointed to by the demo note; /admin keeps working nav · ≤1024px disclosure menu (`<details>`) · ≤640px sub-brand hidden ·
as the direct URL. ≤480px icon-only menu · ≤360px compact paddings. Logo ≥22px tall everywhere
(≈105px wide — brand minimum 100px digital).
One-row invariant: `.nav > *` is `flex: none`; only the spacer and the search Header search — decided by research, not aesthetics: NN/g's magnifying-glass
field flex, so the row can never wrap or overflow. Tier math (Inter widths -icon findings (icon-only search measurably reduces discoverability and use;
estimated ±8%; logo 153:32 → 124px at 26px height; inner width = viewport − show a visible box when search is an important task) beat the icon-only
2×gutter, container caps at 1120): 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.
| Tier | Budget | Available | 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
| ≥1100 | brand+sub 247 + nav 415 + search 186 + Sign in 91 + gaps 16 ≈ 955 | 1044 @1100 | editor" utility link in the footer; /admin keeps working as the direct URL.
| 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 ## Content & microcopy
- Card metadata must differentiate (NN/g: omit non-differentiating metadata): - Card metadata must differentiate (NN/g): cards show date · reading time
category and author are identical on all articles, so cards show (body words ÷ 220 wpm, computed at build). Category and author remain on
date · reading time (body words ÷ 220 wpm, computed at build). Category and the article page as one quiet muted meta line (category · author · date ·
author remain on the article page; meta order there is read time — plain text, no chips).
category · author · date · read time. - Honesty lives at the INTERACTION POINT (REQUIREMENTS §5): whisper badges
- Badges: "Mock — <what's missing>" (bare "Mock" only on tiles); "External — ("Mock — <what's missing>", "Planned integration — <team>") appear where a
<team>"; "Planned — not yet available". Mock badges are never removed. mock interaction actually happens — /proposals panel, /dashboard, /login,
- Terminology is fixed: "Sign in" (never Log in), "Editor", "Search", "News". /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 "...". Buttons start with a verb, sentence case. Real ellipsis (…), never "...".
- Search links (`.suggest a`, `.search-results a`) lay out as flowing text — - Search links (`.suggest a`, `.search-results a`) lay out as flowing text —
never `display: flex` on the `<a>` itself: `<mark>` term-highlight splits the never `display: flex` on the `<a>` (mark-splitting). Dates are plain muted
title into fragments, and flex would turn each into its own item. text, not chips.
- Overflow containment happens at the source: wide `.prose` tables scroll in - Overflow containment happens at the source: wide `.prose` tables scroll in
their own box, long words/URLs wrap via `overflow-wrap`. The their own box, long words/URLs wrap via `overflow-wrap`. The
`html, body { overflow-x: clip }` rule is a regression guard, not a fix — `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). Lead image is borderless
- Articles end with `.post-nav` (Newer ← → Older from the date-sorted and breaks out of the text column ≥900px. Image captions: italic muted line
collection). Image captions: italic line under the image under the image.
(`.prose img + em` / `p > em:only-child`), muted and smaller.
### Element rules (condensed) ### Element rules (condensed)
| Element | Rule | Source | | 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 | NN/g serial-position |
| 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 | Icon-reveal row, all widths; plain GET /search | NN/g site search |
| Header search | Compact field ≥1025px, magnifier reveal ≤1024px; both 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 |
| 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, Galaxy dot on mocks; ONLY at mock interaction points | REQUIREMENTS §5 |
| 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; borderless on Moon; hover = elevate only | NN/g metadata |
| 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, Moon hover fill | NN/g pagination |
| Pagination | Newer/Older + windowed numbers, 44px targets, aria-current | NN/g pagination | | Article | Back "← All news"; h1 → standfirst → muted meta line → borderless breakout cover → inline TOC → prose → post-nav | Guardian/BBC anatomy |
| 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 | | 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 | | 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; no implementation talk in UI copy | NN/g footers | | 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); date chip; "Press Enter for all results" hint | — | | Search results | Links lay out as flowing text (mark-safe); plain muted date; "Press Enter for all results" hint | — |
## Page composition (archetypes) ## Page composition (archetypes)
Every page follows one of three archetypes; a new page picks one instead of Every page follows one of three archetypes; a new page picks one instead of
inventing a section order. inventing a section order. The navy band is reserved for `/` and `/login`.
| Archetype | Pages | 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) | | 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` | Galaxy hero band carrying the h1 + one-line subtitle → single content band (Moon = utility, white = editorial listing) → footer | | 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 | | 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 - Home is task-first (NN/g): Services directly under the hero; the news
top 1–4 tasks — for scientists that's proposals/data/support, not reading section below is the browse layer.
news). The hero teaser is the freshness signal (deep-links the newest - Document order is press anatomy (Guardian/BBC/Reuters). The inline TOC sits
article); the 3-card section below is the browse layer — different between lead image and body.
granularity, not redundant. - Back affordances: articles "← All news", static pages "← Home". Auth
- Document order is press anatomy (Guardian/BBC/Reuters): headline → (`/login`) is the sole exception to the archetypes: one focused card on the
standfirst → byline/meta → lead image → body. The inline TOC sits between nebula band, no hero, no sections.
lead image and body (Wikipedia/GOV.UK contents position) so the editorial - Service tiles carry a stroke glyph (`.ico` family: currentColor, round
head stays unbroken and the list is adjacent to what it indexes. caps, aria-hidden; 26px Galaxy) ABOVE the label — never instead of it.
- Back affordances: articles "← All news", static pages "← Home" (footer- and Glyphs are wayfinding, not decoration.
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 ## Do not
- No new hues, tints, or grays — derive via `color-mix` from brand tokens only. - No new hues, tints, or grays — derive via `color-mix` from brand tokens only.
- No Space Grotesk outside h1/h2. No dark theme. - No Space Grotesk outside h1/h2; headline weight stays 500 (brand D.3.4).
- No ad-hoc spacing/shadow/radius values; extend the token tables instead. - 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 animation/transition outside the `no-preference` block; no JS motion;
- No dependencies/frameworks (CSS or JS). No removal of "Mock" badges. no transition durations/easings outside `--dur`/`--ease`.
- Don't lighten text over navy bands below the alphas listed above. - 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 - No clip-path/CSS approximations of brand assets — the flash is the real SVG
path from the official logo, or nothing. path from the official logo, or nothing.
- Decoration budget: max ONE accent element per view (currently: footer flash, - Decoration budget: max ONE accent element per view (currently: footer
static). No twinkles, floating shapes, or stacked decorations. 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).
Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

+11 -10
View File
@@ -62,9 +62,11 @@ const links = [
))} ))}
</nav> </nav>
<span class="spacer"></span> <span class="spacer"></span>
{/* Site search lives in the header (desktop: compact field; ≤1024px: the {/* Site search. Desktop ≥1025px: a VISIBLE quiet field — NN/g's
magnifier <details> below). Plain GET form — works without JS; with JS magnifying-glass-icon research: hiding search behind an icon
the shared suggest dropdown appears (search-client.js). */} measurably reduces discoverability and usage; a visible input is
recommended when search matters (200+ articles, spec-required).
Plain GET form (works without JS); suggestions via search-client.js. */}
<form class="nav-search" role="search" aria-label="Site search" action="/search" method="get"> <form class="nav-search" role="search" aria-label="Site search" action="/search" method="get">
<label class="sr-only" for="hdr-q">Search news</label> <label class="sr-only" for="hdr-q">Search news</label>
<svg class="ico search-ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg> <svg class="ico search-ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
@@ -72,10 +74,9 @@ const links = [
<ul class="suggest" id="hdr-suggest"></ul> <ul class="suggest" id="hdr-suggest"></ul>
<p class="sr-only" id="hdr-suggest-status" role="status" aria-live="polite"></p> <p class="sr-only" id="hdr-suggest-status" role="status" aria-live="polite"></p>
</form> </form>
<a class="login-mock" href="/login">Sign in</a> {/* ≤1024px: the magnifier reveals a search row under the header (NN/g
{/* ≤1024px: magnifier button reveals a full-width search row. name="header-panel" mobile-search pattern). name="header-panel" pairs it with the menu as
pairs it with the menu as a native exclusive group — opening one closes a native exclusive group — opening one closes the other, no JS. */}
the other, no JS (exclusive-accordion behavior of <details name>). */}
<details class="search-pop" name="header-panel"> <details class="search-pop" name="header-panel">
<summary aria-label="Search"> <summary aria-label="Search">
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg> <svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
@@ -88,6 +89,7 @@ const links = [
<p class="sr-only" id="pop-suggest-status" role="status" aria-live="polite"></p> <p class="sr-only" id="pop-suggest-status" role="status" aria-live="polite"></p>
</form> </form>
</details> </details>
<a class="login-mock" href="/login">Sign in</a>
{/* Mobile disclosure menu — native <details>/<summary>, no JS; desktop hides it via CSS */} {/* Mobile disclosure menu — native <details>/<summary>, no JS; desktop hides it via CSS */}
<details class="menu" name="header-panel"> <details class="menu" name="header-panel">
<summary aria-label="Menu"> <summary aria-label="Menu">
@@ -106,9 +108,8 @@ const links = [
</header> </header>
<script is:inline src="/search-client.js"></script> <script is:inline src="/search-client.js"></script>
<script is:inline> <script is:inline>
// Header search suggestions (desktop field + mobile reveal); the forms are // Header search suggestions (icon-reveal row); the form is a plain
// plain GET /search, so everything below is progressive enhancement. // GET /search, so everything below is progressive enhancement.
newsSearch({ input: 'hdr-q', list: 'hdr-suggest', status: 'hdr-suggest-status', limit: 6, unit: 'suggestion' });
newsSearch({ input: 'pop-q', list: 'pop-suggest', status: 'pop-suggest-status', limit: 6, unit: 'suggestion' }); newsSearch({ input: 'pop-q', list: 'pop-suggest', status: 'pop-suggest-status', limit: 6, unit: 'suggestion' });
var pop = document.querySelector('.search-pop'); var pop = document.querySelector('.search-pop');
pop.addEventListener('toggle', function () { pop.addEventListener('toggle', function () {
+4 -9
View File
@@ -19,16 +19,11 @@ const tickets = [
]; ];
--- ---
<Base title="CTAO Science Portal — Dashboard" description="User dashboard (mock)"> <Base title="CTAO Science Portal — Dashboard" description="User dashboard (mock)">
<section class="hero-band">
<div class="band-bg" aria-hidden="true"></div>
<div class="container hero">
<div class="eyebrow">User area</div>
<h1>Dashboard</h1>
<p>Your proposals, data products and support tickets in one place</p>
</div>
</section>
<section class="band--moon"> <section class="band--moon">
<header class="container page-head">
<h1>Dashboard</h1>
<p class="standfirst">Your proposals, data products and support tickets in one place.</p>
</header>
<div class="container page"> <div class="container page">
<span class="badge-mock">Mock — sample data, no live services connected</span> <span class="badge-mock">Mock — sample data, no live services connected</span>
<div class="dash-grid"> <div class="dash-grid">
+15 -14
View File
@@ -10,27 +10,34 @@ const iso = (d) => d.toISOString().slice(0, 10);
// Reading time from the Markdown body (~220 wpm) — differentiating card metadata; // Reading time from the Markdown body (~220 wpm) — differentiating card metadata;
// category/author are identical across all articles, so cards omit them (NN/g). // category/author are identical across all articles, so cards omit them (NN/g).
const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220)); const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220));
// Services per the Science Portal spec; unbuilt ones are labelled, never faked. // Services per the Science Portal spec. Tiles carry no status chrome — honesty
// lives at the interaction point (REQUIREMENTS §5): each target page opens with
// its own whisper badge. Planned services stay in the grid but subdued (muted,
// no link) with availability folded into the description text.
// Tile icons: hand-drawn stroke glyphs in the header-icon family (.ico — // Tile icons: hand-drawn stroke glyphs in the header-icon family (.ico —
// currentColor, round caps), aria-hidden. NN/g icon research: icons alone are // currentColor, round caps), aria-hidden. NN/g icon research: icons alone are
// ambiguous, but NEXT TO an always-visible label they differentiate otherwise // ambiguous, but NEXT TO an always-visible label they differentiate otherwise
// uniform tiles and speed up grid scanning. // uniform tiles and speed up grid scanning.
const services = [ const services = [
{ title: 'Proposals', href: '/proposals', desc: 'Submit observation proposals, including ToO / MWL requests.', badge: 'mock', { title: 'Proposals', href: '/proposals', desc: 'Submit observation proposals, including ToO / MWL requests.',
icon: '<path d="M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5z"/><path d="M13.5 3v4.5H18"/><path d="M9 12.5h6M9 16h4"/>' }, icon: '<path d="M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5z"/><path d="M13.5 3v4.5H18"/><path d="M9 12.5h6M9 16h4"/>' },
{ title: 'Data Explorer', href: 'https://padc-ctao-data-explorer.obspm.fr/', ext: true, desc: 'Search and download CTAO science data products.', badge: 'ext', { title: 'Data Explorer', href: 'https://padc-ctao-data-explorer.obspm.fr/', ext: true, desc: 'Search and download CTAO science data products.',
icon: '<ellipse cx="12" cy="5.5" rx="7" ry="2.5"/><path d="M5 5.5V18c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5V5.5"/><path d="M5 11.8c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5"/>' }, icon: '<ellipse cx="12" cy="5.5" rx="7" ry="2.5"/><path d="M5 5.5V18c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5V5.5"/><path d="M5 11.8c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5"/>' },
{ title: 'Dashboard', href: '/dashboard', desc: 'Your proposals, data products and support tickets in one place.', badge: 'mock', { title: 'Dashboard', href: '/dashboard', desc: 'Your proposals, data products and support tickets in one place.',
icon: '<rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/>' }, icon: '<rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/>' },
{ title: 'User Support', href: '/support', desc: 'Help desk, FAQ, user forum and mailing lists.', badge: 'mock', { title: 'User Support', href: '/support', desc: 'Help desk, FAQ, user forum and mailing lists.',
icon: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/><path d="m5.7 5.7 3.5 3.5m5.6 0 3.5-3.5m0 12.6-3.5-3.5m-5.6 0-3.5 3.5"/>' }, icon: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/><path d="m5.7 5.7 3.5 3.5m5.6 0 3.5-3.5m0 12.6-3.5-3.5m-5.6 0-3.5 3.5"/>' },
{ title: 'Software', desc: 'Science analysis tools and pipelines.', badge: 'tbd', { title: 'Software', planned: true, desc: 'Science analysis tools and pipelines — planned.',
icon: '<rect x="3" y="4.5" width="18" height="15" rx="2"/><path d="m7 9.5 3 2.5-3 2.5M12.5 15H17"/>' }, icon: '<rect x="3" y="4.5" width="18" height="15" rx="2"/><path d="m7 9.5 3 2.5-3 2.5M12.5 15H17"/>' },
{ title: 'Documentation', desc: 'User guides and technical documentation.', badge: 'tbd', { title: 'Documentation', planned: true, desc: 'User guides and technical documentation — planned.',
icon: '<path d="M12 6.7C10.6 5.2 8.6 4.5 5.7 4.5H4v13.6h1.7c2.9 0 4.9.7 6.3 2.2 1.4-1.5 3.4-2.2 6.3-2.2H20V4.5h-1.7c-2.9 0-4.9.7-6.3 2.2z"/><path d="M12 6.7v13.6"/>' }, icon: '<path d="M12 6.7C10.6 5.2 8.6 4.5 5.7 4.5H4v13.6h1.7c2.9 0 4.9.7 6.3 2.2 1.4-1.5 3.4-2.2 6.3-2.2H20V4.5h-1.7c-2.9 0-4.9.7-6.3 2.2z"/><path d="M12 6.7v13.6"/>' },
]; ];
--- ---
<Base title="CTAO Science Portal" description="News, observation proposals, data and user services of the Cherenkov Telescope Array Observatory" ambient> <Base title="CTAO Science Portal" description="News, observation proposals, data and user services of the Cherenkov Telescope Array Observatory" ambient>
{/* Photographic hero: LST-1 under the La Palma night sky (copied from the
content library to /brand/hero.jpg) beneath the Galaxy scrim — text
contrast computed worst-case in DESIGN.md. Three elements only:
headline, brand subtitle, CTA pair (Apple hero restraint). */}
<section class="hero-band"> <section class="hero-band">
<div class="band-bg" aria-hidden="true"></div> <div class="band-bg" aria-hidden="true"></div>
<div class="container hero"> <div class="container hero">
@@ -43,9 +50,6 @@ const services = [
<a class="btn external" href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener">Explore the data</a> <a class="btn external" href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener">Explore the data</a>
<a class="btn btn--ghost" href="/proposals">Submit a proposal</a> <a class="btn btn--ghost" href="/proposals">Submit a proposal</a>
</div> </div>
{posts[0] && (
<p class="hero-teaser">Latest: <a href={`/news/${posts[0].id}`}>{posts[0].data.title}</a></p>
)}
</div> </div>
</section> </section>
@@ -60,7 +64,7 @@ const services = [
<div class="section-head"><h2>Services</h2></div> <div class="section-head"><h2>Services</h2></div>
<div class="grid"> <div class="grid">
{services.map((s) => ( {services.map((s) => (
<article class="card tile"> <article class={s.planned ? 'card tile tile--planned' : 'card tile'}>
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24" set:html={s.icon} /> <svg class="ico" aria-hidden="true" viewBox="0 0 24 24" set:html={s.icon} />
<h3>{s.href <h3>{s.href
? (s.ext ? (s.ext
@@ -68,9 +72,6 @@ const services = [
: <a href={s.href}>{s.title}</a>) : <a href={s.href}>{s.title}</a>)
: s.title}</h3> : s.title}</h3>
<p class="desc">{s.desc}</p> <p class="desc">{s.desc}</p>
{s.badge === 'mock' && <span class="badge-mock">Mock</span>}
{s.badge === 'ext' && <span class="badge-ext">External — LUX team</span>}
{s.badge === 'tbd' && <span class="badge-ext">Planned — not yet available</span>}
</article> </article>
))} ))}
</div> </div>
+5 -7
View File
@@ -30,14 +30,12 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
title={first ? 'CTAO Science Portal — News' : `CTAO Science Portal — News, page ${page.currentPage}`} title={first ? 'CTAO Science Portal — News' : `CTAO Science Portal — News, page ${page.currentPage}`}
description={first ? 'News and announcements of the CTAO' : `News and announcements of the CTAO — page ${page.currentPage} of ${page.lastPage}`} description={first ? 'News and announcements of the CTAO' : `News and announcements of the CTAO — page ${page.currentPage} of ${page.lastPage}`}
> >
<section class="hero-band"> {/* Section-landing archetype: white head, display Galaxy h1 + standfirst
<div class="band-bg" aria-hidden="true"></div> (the navy band is reserved for / and /login — DESIGN.md). */}
<div class="container hero"> <header class="container page-head">
<div class="eyebrow">Cherenkov Telescope Array Observatory</div>
<h1>News &amp; Announcements</h1> <h1>News &amp; Announcements</h1>
<p>Science highlights and updates from the observatory and its partners</p> <p class="standfirst">Science highlights and updates from the observatory and its partners.</p>
</div> </header>
</section>
<section class="container page"> <section class="container page">
<h2 class="sr-only">{first ? 'All news' : `All news — page ${page.currentPage}`}</h2> <h2 class="sr-only">{first ? 'All news' : `All news — page ${page.currentPage}`}</h2>
+5 -5
View File
@@ -31,15 +31,15 @@ const readMin = Math.max(1, Math.round((post.body ?? '').split(/\s+/).length / 2
description made visible — cards already show it, the article should too. */} description made visible — cards already show it, the article should too. */}
<h1>{post.data.title}</h1> <h1>{post.data.title}</h1>
<p class="standfirst">{post.data.description}</p> <p class="standfirst">{post.data.description}</p>
{/* Meta order: category · author · date · reading time */} {/* Meta — one quiet muted line: category · author · date · reading time */}
<div class="meta"><span class="cat">{post.data.category}</span><span>{post.data.author}</span>·<time datetime={post.data.date.toISOString().slice(0, 10)}>{fmt(post.data.date)}</time>·<span>{readMin} min read</span></div> <div class="meta"><span class="cat">{post.data.category}</span>·<span>{post.data.author}</span>·<time datetime={post.data.date.toISOString().slice(0, 10)}>{fmt(post.data.date)}</time>·<span>{readMin} min read</span></div>
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" />} {post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" />}
{/* TOC sits between lead image and body (Wikipedia/GOV.UK contents {/* TOC sits between lead image and body (Wikipedia/GOV.UK contents
position): the press head above stays unbroken, and the list is position): the press head above stays unbroken, and the list is
adjacent to the content it indexes. */} adjacent to the content it indexes. */}
{toc.length >= 3 && ( {toc.length >= 3 && (
<details class="toc toc--inline"> <details class="toc toc--inline">
<summary>On this page</summary> <summary>On this page <svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><path d="m5 9 7 7 7-7" /></svg></summary>
<ol> <ol>
{toc.map((h) => ( {toc.map((h) => (
<li class={h.depth === 3 ? 'd3' : undefined}><a href={`#${h.slug}`}>{h.text}</a></li> <li class={h.depth === 3 ? 'd3' : undefined}><a href={`#${h.slug}`}>{h.text}</a></li>
@@ -112,8 +112,8 @@ const readMin = Math.max(1, Math.round((post.body ?? '').split(/\s+/).length / 2
let observer; let observer;
const observe = () => { const observe = () => {
observer?.disconnect(); observer?.disconnect();
// Narrow band below the sticky header (64px) — Starlight's rootMargin trick // Narrow band below the sticky header (56px) — Starlight's rootMargin trick
const top = 76, band = 64; const top = 68, band = 64;
observer = new IntersectionObserver(onIntersect, { observer = new IntersectionObserver(onIntersect, {
rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`, rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`,
}); });
+4 -9
View File
@@ -2,16 +2,11 @@
import Base from '../layouts/Base.astro'; import Base from '../layouts/Base.astro';
--- ---
<Base title="CTAO Science Portal — Proposals" description="Observation proposal submission (mock)"> <Base title="CTAO Science Portal — Proposals" description="Observation proposal submission (mock)">
<section class="hero-band">
<div class="band-bg" aria-hidden="true"></div>
<div class="container hero">
<div class="eyebrow">Proposal Submission</div>
<h1>Observation proposals</h1>
<p>Submit observation proposals, including ToO and MWL requests</p>
</div>
</section>
<section class="band--moon"> <section class="band--moon">
<header class="container page-head">
<h1>Observation proposals</h1>
<p class="standfirst">Submit observation proposals, including ToO and MWL requests.</p>
</header>
<div class="container page"> <div class="container page">
<span class="badge-mock">Mock — no submission logic</span> <span class="badge-mock">Mock — no submission logic</span>
<span class="badge-ext">Planned integration: Proposal Handling System (APC team)</span> <span class="badge-ext">Planned integration: Proposal Handling System (APC team)</span>
+7 -9
View File
@@ -2,16 +2,14 @@
import Base from '../layouts/Base.astro'; import Base from '../layouts/Base.astro';
--- ---
<Base title="CTAO Science Portal — Search" description="Search CTAO news and announcements"> <Base title="CTAO Science Portal — Search" description="Search CTAO news and announcements">
<section class="hero-band"> {/* Utility archetype: ONE surface per page — Moon canvas from the landing
<div class="band-bg" aria-hidden="true"></div> head down (borderless cards/panels pop against it); editorial pages stay
<div class="container hero"> all-white. No mid-page surface seams (DESIGN.md). */}
<div class="eyebrow">Science Portal</div>
<h1>Search</h1>
<p>Find news and announcements</p>
</div>
</section>
<section class="band--moon"> <section class="band--moon">
<header class="container page-head">
<h1>Search</h1>
<p class="standfirst">Find news and announcements.</p>
</header>
<div class="container page"> <div class="container page">
<form class="search-form" role="search" action="/search" method="get"> <form class="search-form" role="search" action="/search" method="get">
<label class="sr-only" for="q">Search news</label> <label class="sr-only" for="q">Search news</label>
+6 -10
View File
@@ -9,24 +9,20 @@ const channels = [
]; ];
--- ---
<Base title="CTAO Science Portal — Support" description="User support: help desk, FAQ, forum, mailing lists (mock)"> <Base title="CTAO Science Portal — Support" description="User support: help desk, FAQ, forum, mailing lists (mock)">
<section class="hero-band">
<div class="band-bg" aria-hidden="true"></div>
<div class="container hero">
<div class="eyebrow">User services</div>
<h1>User Support</h1>
<p>Help desk, FAQ, user forum and mailing lists</p>
</div>
</section>
<section class="band--moon"> <section class="band--moon">
<header class="container page-head">
<h1>User Support</h1>
<p class="standfirst">Help desk, FAQ, user forum and mailing lists.</p>
</header>
<div class="container page"> <div class="container page">
{/* One page-level whisper badge — the tiles below stay clean (they are
not interactive; honesty lives here, at the point users arrive). */}
<span class="badge-mock">Mock — support services not yet connected</span> <span class="badge-mock">Mock — support services not yet connected</span>
<div class="grid"> <div class="grid">
{channels.map((c) => ( {channels.map((c) => (
<article class="card tile"> <article class="card tile">
<h3>{c.title}</h3> <h3>{c.title}</h3>
<p class="desc">{c.desc}</p> <p class="desc">{c.desc}</p>
<span class="badge-mock">Mock</span>
</article> </article>
))} ))}
</div> </div>
+270 -212
View File
@@ -1,7 +1,8 @@
/* ========================================================================== /* ==========================================================================
CTAO Science Portal — design tokens per the official CTAO Brand Guidelines CTAO Science Portal — design tokens per the official CTAO Brand Guidelines
(April 2024): colors D.2, typography D.3 (Inter body / Space Grotesk display), (April 2024): colors D.2, typography D.3 (Inter body / Space Grotesk display
flash D.4. All look-and-feel lives in the tokens below; WCAG 2.1 AA contrast. at medium weight per D.3.4), flash D.4. All look-and-feel lives in the tokens
below; WCAG 2.1 AA contrast (photo-hero worst cases computed — see DESIGN.md).
Light theme only (brand is light-first; navy bands carry the dark identity). Light theme only (brand is light-first; navy bands carry the dark identity).
System rules (spacing / elevation / surfaces / motion): see DESIGN.md. System rules (spacing / elevation / surfaces / motion): see DESIGN.md.
========================================================================== */ ========================================================================== */
@@ -9,9 +10,9 @@
color-scheme: light; color-scheme: light;
/* Brand palette — hex values straight from the brand book */ /* Brand palette — hex values straight from the brand book */
--galaxy: #00004a; /* Galaxy Blue — headings, header/footer bands */ --galaxy: #00004a; /* Galaxy Blue — headings, header/footer bands */
--cherenkov: #00e4d8; /* Cherenkov Blue — accents, flash, CTAs; NOT for small text on light */ --cherenkov: #00e4d8; /* Cherenkov Blue — primary CTA, flash, hero subtitle, logo cluster ONLY */
--moon: #f5f5f5; /* Moon Gray — surfaces */ --moon: #f5f5f5; /* Moon Gray — surfaces */
--azure: #007aff; /* Cosmic Azure — brand hyperlink hue (large/UI only on light) */ --azure: #007aff; /* Cosmic Azure — focus rings (UI only on light) */
--indigo: #00009c; /* Interestellar Indigo — gradient depth */ --indigo: #00009c; /* Interestellar Indigo — gradient depth */
/* Derived, WCAG-adjusted */ /* Derived, WCAG-adjusted */
--text: #101228; /* near-Galaxy body ink */ --text: #101228; /* near-Galaxy body ink */
@@ -19,35 +20,58 @@
--border: #e2e5ee; --border: #e2e5ee;
--link: #0057c2; /* Cosmic Azure darkened to pass AA (4.5:1) for body links */ --link: #0057c2; /* Cosmic Azure darkened to pass AA (4.5:1) for body links */
--on-galaxy-muted: #c7cde9; --on-galaxy-muted: #c7cde9;
--tint-cherenkov: color-mix(in srgb, var(--cherenkov) 16%, transparent); /* chip/badge fill */ --tint-cherenkov: color-mix(in srgb, var(--cherenkov) 16%, transparent); /* <mark> highlight only */
--cherenkov-hover: color-mix(in oklab, var(--cherenkov), var(--galaxy) 12%); /* CTA hover state */ --cherenkov-hover: color-mix(in oklab, var(--cherenkov), var(--galaxy) 12%); /* CTA hover state */
--ico-ext: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cpath d="M7 17 17 7M9 7h8v8" fill="none" stroke="black" stroke-width="2.5"/%3E%3C/svg%3E'); --ico-ext: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cpath d="M7 17 17 7M9 7h8v8" fill="none" stroke="black" stroke-width="2.5"/%3E%3C/svg%3E');
/* Type & layout */ /* Type — fonts (BRAND D.3.1/D.3.2) and the ONLY font sizes in this codebase */
--font-body: "Inter", "Inter Fallback", Arial, system-ui, sans-serif; /* BRAND D.3.1 */ --font-body: "Inter", "Inter Fallback", Arial, system-ui, sans-serif;
--font-display: "Space Grotesk", var(--font-body); /* BRAND D.3.2 — headlines only */ --font-display: "Space Grotesk", var(--font-body); /* headlines only */
--fs-xs: 0.8125rem; /* chips, meta, badges, labels */
--fs-s: 0.9rem; /* secondary text, nav, forms, footer */
--fs-l: 1.125rem; /* card titles, standfirst, panel titles */
--fs-xl: 1.5rem; /* prose h2, featured title, auth h1 */
--fs-h2: clamp(1.75rem, 3.5vw, 2.5rem); /* section heads */
--fs-h1: clamp(2.25rem, 5vw, 3.5rem); /* page/article headlines */
--fs-display: clamp(2.5rem, 7vw, 4.75rem); /* home hero only */
/* ctao.org uses generous rounding (15/30/60px tokens) and pill buttons */ /* ctao.org uses generous rounding (15/30/60px tokens) and pill buttons */
--radius: 16px; --radius: 16px;
--radius-pill: 999px; --radius-pill: 999px;
--maxw: 1120px; --maxw: 1120px;
/* Fluid spacing scale (ctao.org rhythm: sections breathe, mobile stays compact). /* Fluid spacing scale (Apple-scale rhythm: sections breathe, mobile stays compact).
Every section / container / card padding sits on this scale — no ad-hoc values. */ Every section / container / card padding sits on this scale — no ad-hoc values. */
--gutter: clamp(18px, 4vw, 28px); /* page side padding */ --gutter: clamp(18px, 4vw, 28px); /* page side padding */
--space-s: 12px; --space-s: 12px;
--space-m: 20px; --space-m: 24px;
--space-l: clamp(24px, 2vw + 16px, 40px); --space-l: clamp(24px, 2vw + 16px, 40px);
--space-xl: clamp(40px, 3vw + 24px, 72px); /* section rhythm */ --space-xl: clamp(56px, 7vw, 120px); /* section rhythm */
--space-band: clamp(48px, 5vw + 28px, 96px); /* hero / auth band padding */ --space-band: clamp(72px, 10vw, 160px); /* hero / auth / landing-head padding */
/* Elevation — navy-tinted; the ONLY two shadows allowed in this codebase */ /* Elevation — navy-tinted, soft/large/low-alpha; the ONLY two shadows allowed */
--shadow-ambient: 0 1px 2px rgba(0, 0, 74, 0.05), 0 4px 14px rgba(0, 0, 74, 0.07); --shadow-ambient: 0 1px 2px rgba(0, 0, 74, 0.04), 0 8px 24px rgba(0, 0, 74, 0.06);
--shadow-lifted: 0 4px 10px rgba(0, 0, 74, 0.08), 0 18px 44px rgba(0, 0, 74, 0.18); --shadow-lifted: 0 2px 6px rgba(0, 0, 74, 0.05), 0 24px 48px rgba(0, 0, 74, 0.14);
/* Nebula glow (BRAND D.5) — one stack for hero + auth bands, static AND animated. /* Motion — ONE hover language everywhere (ctao.org: quiet .25s fades) */
Alphas are contrast ceilings (DESIGN.md): Cherenkov ≤.38, Azure ≤.35 keep the --dur: 0.25s;
Cherenkov subtitle/eyebrow ≥4.5:1 even with a blob core right under the text. --ease: ease-out;
Cherenkov and Azure cores sit in opposite corners — cores never meet. */ /* Nebula glow (BRAND D.5) — auth band, static AND animated. Alphas are
contrast ceilings (DESIGN.md): Cherenkov ≤.38 / Azure ≤.35; cores sit in
opposite corners — cores never meet. */
--nebula: --nebula:
radial-gradient(52% 58% at 78% 76%, rgba(0, 228, 216, 0.38), transparent 66%), radial-gradient(52% 58% at 78% 76%, rgba(0, 228, 216, 0.38), transparent 66%),
radial-gradient(46% 52% at 12% 18%, rgba(0, 122, 255, 0.33), transparent 64%), radial-gradient(46% 52% at 12% 18%, rgba(0, 122, 255, 0.33), transparent 64%),
radial-gradient(75% 85% at 40% 55%, rgba(0, 0, 156, 0.8), transparent 72%); radial-gradient(75% 85% at 40% 55%, rgba(0, 0, 156, 0.8), transparent 72%);
/* Photo-hero variant: no indigo depth blob over photography, Cherenkov ≤.30
with its core pinned ≥80% band width (only ~.15 fringe reaches text). */
--nebula-glow:
radial-gradient(52% 58% at 82% 80%, rgba(0, 228, 216, 0.3), transparent 66%),
radial-gradient(46% 52% at 10% 15%, rgba(0, 122, 255, 0.28), transparent 64%);
/* Hero scrim over the photo — worst-case AA computed in DESIGN.md:
small text lives left of 52% band width (α ≥ .76 there). */
--hero-scrim: linear-gradient(to right,
rgba(0, 0, 74, 0.84), rgba(0, 0, 74, 0.76) 52%,
rgba(0, 0, 74, 0.44) 78%, rgba(0, 0, 74, 0.26));
}
/* ≤640px the hero text spans the full width — flat scrim, computed ≥5.28:1 */
@media (max-width: 640px) {
:root { --hero-scrim: linear-gradient(rgba(0, 0, 74, 0.8), rgba(0, 0, 74, 0.8)); }
} }
/* Self-hosted brand fonts — latin subset, variable weight, swap + metric-matched fallback */ /* Self-hosted brand fonts — latin subset, variable weight, swap + metric-matched fallback */
@@ -73,7 +97,7 @@
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-padding-top: 84px; /* sticky header + anchor jumps */ } html { -webkit-text-size-adjust: 100%; scroll-padding-top: 76px; /* sticky header + anchor jumps */ }
/* Guard, not a solution (DESIGN.md): every wide element is contained at its own /* Guard, not a solution (DESIGN.md): every wide element is contained at its own
level (tables scroll in their box, long words wrap); clip only catches level (tables scroll in their box, long words wrap); clip only catches
regressions so one offender can never remove the page gutters. `clip`, unlike regressions so one offender can never remove the page gutters. `clip`, unlike
@@ -84,11 +108,14 @@ body {
font-family: var(--font-body); font-family: var(--font-body);
background: #fff; background: #fff;
color: var(--text); color: var(--text);
line-height: 1.65; line-height: 1.5;
min-height: 100dvh; min-height: 100dvh;
} }
h1, h2 { font-family: var(--font-display); letter-spacing: -0.01em; } /* Display type: Space Grotesk at medium weight (BRAND D.3.4 — regular/medium),
tight tracking at display sizes, compact leading. Hierarchy comes from size. */
h1, h2 { font-family: var(--font-display); font-weight: 500; letter-spacing: -0.025em; line-height: 1.1; }
h3 { font-weight: 600; }
h1, h2, h3, .hero p { text-wrap: balance; } h1, h2, h3, .hero p { text-wrap: balance; }
/* Overlong unbroken words (slugs, codes) may break rather than overflow the page */ /* Overlong unbroken words (slugs, codes) may break rather than overflow the page */
h1, h2, h3 { overflow-wrap: break-word; } h1, h2, h3 { overflow-wrap: break-word; }
@@ -97,6 +124,10 @@ h1, h2, h3 { overflow-wrap: break-word; }
/* Inline SVG icons — stroke inherits currentColor */ /* Inline SVG icons — stroke inherits currentColor */
.ico { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; flex: none; } .ico { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; flex: none; }
/* All <summary> disclosures (menu, search, TOC) share the plain-button look */
summary { cursor: pointer; list-style: none; }
summary::-webkit-details-marker { display: none; }
a { color: var(--link); text-decoration: underline; text-underline-offset: 3px; } a { color: var(--link); text-decoration: underline; text-underline-offset: 3px; }
a:hover { color: var(--indigo); } a:hover { color: var(--indigo); }
/* Placeholder links (<a> without href) don't pretend to be clickable */ /* Placeholder links (<a> without href) don't pretend to be clickable */
@@ -120,7 +151,8 @@ img { max-width: 100%; height: auto; display: block; }
} }
.container { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); } .container { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); }
/* Surface rhythm: white (editorial) → Moon Gray (service/utility bands) → Galaxy (hero/footer) */ /* Surface rhythm: white (editorial + landing heads) → Moon Gray (service/utility
bands) → Galaxy (home hero, auth, footer) */
.band--moon { background: var(--moon); } .band--moon { background: var(--moon); }
/* Flash — the REAL brand star (BRAND D.4): inline SVG path lifted from the /* Flash — the REAL brand star (BRAND D.4): inline SVG path lifted from the
@@ -128,69 +160,99 @@ img { max-width: 100%; height: auto; display: block; }
.flash { width: 12px; height: 12px; color: var(--cherenkov); flex: none; } .flash { width: 12px; height: 12px; color: var(--cherenkov); flex: none; }
/* Header — Galaxy Blue band, reverse (white) logo per BRAND B.1.2 */ /* Header — Galaxy Blue band, reverse (white) logo per BRAND B.1.2; translucent
over content where backdrop blur is supported (AA computed: worst underlying
white → bar (31,31,96), muted nav text 9.4:1). Solid Galaxy fallback. */
.site-header { background: var(--galaxy); position: sticky; top: 0; z-index: 10; } .site-header { background: var(--galaxy); position: sticky; top: 0; z-index: 10; }
.nav { display: flex; align-items: center; gap: 4px; min-height: 64px; flex-wrap: nowrap; } @supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
.site-header {
background: color-mix(in srgb, var(--galaxy) 88%, transparent);
-webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
}
.nav { display: flex; align-items: center; gap: 4px; min-height: 56px; flex-wrap: nowrap; }
.nav > nav { display: flex; align-items: center; gap: 4px; } .nav > nav { display: flex; align-items: center; gap: 4px; }
/* One-row guarantee: only the search field and the spacer flex — everything /* One-row guarantee: everything keeps its natural size except the spacer
else keeps its natural size (links are single words, they can never wrap) */ (links are single words, they can never wrap) */
.nav > * { flex: none; } .nav > * { flex: none; }
/* Header tiers (one row, always — width math in DESIGN.md): /* Header tiers, one row always: ≥1025px full nav · ≤1024px disclosure menu ·
≥1100px full nav + inline search + sub-brand · 1025–1099px sub-brand hidden · ≤640px sub-brand hidden · ≤480px icon-only menu, smaller logo · ≤360px compact.
≤1024px disclosure menu + magnifier reveal (both native <details>, CSS-only) · Search is the same icon-reveal at every width (one pattern, no width math). */
≤480px icon-only menu button, smaller logo · ≤360px compact paddings */
@media (max-width: 1099px) { .brand .brand-sub { display: none; } }
.menu { display: none; } .menu { display: none; }
.menu .i-close { display: none; } .menu .i-close { display: none; }
.menu[open] .i-menu { display: none; } .menu[open] .i-menu { display: none; }
.menu[open] .i-close { display: block; } .menu[open] .i-close { display: block; }
.brand { display: flex; align-items: center; gap: 10px; margin-right: 10px; } .brand { display: flex; align-items: center; gap: 10px; margin-right: 10px; text-decoration: none; }
.brand img { height: 26px; width: auto; } .brand img { height: 26px; width: auto; }
.brand .brand-sub { .brand .brand-sub {
color: var(--cherenkov); font-size: 0.78rem; font-weight: 500; color: var(--cherenkov); font-size: var(--fs-xs); font-weight: 500;
border-left: 1px solid rgba(255,255,255,0.25); padding-left: 10px; border-left: 1px solid rgba(255, 255, 255, 0.25); padding-left: 10px;
} }
.nav .spacer { flex: 1; } .nav .spacer { flex: 1; }
.nav a.navlink { .nav a.navlink {
color: var(--on-galaxy-muted); font-weight: 500; font-size: 0.95rem; color: var(--on-galaxy-muted); font-weight: 500; font-size: var(--fs-s);
padding: 10px 10px; text-decoration: none; border-bottom: 2px solid transparent; padding: 10px 10px; text-decoration: none; border-bottom: 2px solid transparent;
} }
.nav a.navlink:hover { color: #fff; } .nav a.navlink:hover { color: #fff; }
.nav a.navlink[aria-current="page"] { color: #fff; border-bottom-color: var(--cherenkov); } .nav a.navlink[aria-current="page"] { color: #fff; border-bottom-color: #fff; }
/* AAI sign-in entry — leads to the /login mock page */ /* AAI sign-in entry — ghost/outline: Cherenkov fill is reserved for the page's
primary CTA, the header only whispers. */
.nav .login-mock { .nav .login-mock {
color: var(--galaxy); background: var(--cherenkov); font: inherit; font-weight: 600; color: #fff; background: transparent; font: inherit; font-weight: 600; font-size: var(--fs-s);
border: 0; border-radius: var(--radius-pill); padding: 8px 18px; margin-left: 6px; border: 1px solid rgba(255, 255, 255, 0.55); border-radius: var(--radius-pill);
text-decoration: none; white-space: nowrap; padding: 7px 18px; margin-left: 6px; text-decoration: none; white-space: nowrap;
} }
.nav .login-mock:hover { background: var(--cherenkov-hover); color: var(--galaxy); } .nav .login-mock:hover { background: rgba(255, 255, 255, 0.12); color: #fff; border-color: #fff; }
/* Tap targets ≥44px (WCAG 2.5.5); inline-flex keeps the active bottom border */ /* Tap targets ≥44px (WCAG 2.5.5); inline-flex keeps the active bottom border */
.nav a.navlink, .nav .login-mock { .nav a.navlink, .nav .login-mock {
display: inline-flex; align-items: center; gap: 6px; min-height: 44px; display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
} }
/* Header search (desktop) — compact pill in the utility cluster; a plain GET /* Site search. Desktop ≥1025px: VISIBLE quiet field — NN/g magnifying-glass-
form to /search (works without JS), suggestions reuse the shared .suggest icon research: icon-only search reduces discoverability/usage; a visible
dropdown. Flexes 130–180px so the row can never overflow (width math: DESIGN.md). */ input is recommended when search is an important task (content-heavy portal,
.nav-search { position: relative; display: flex; align-items: center; flex: 0 1 180px; min-width: 130px; margin-left: 6px; } spec-required search). Chrome stays whisper-quiet: hairline on navy.
.nav-search .search-ico { left: 12px; width: 15px; height: 15px; } Flexes 140-200px (only it and the spacer flex — one-row invariant). */
.nav-search { position: relative; display: flex; align-items: center; flex: 0 1 200px; min-width: 140px; margin-left: 6px; }
.nav-search .search-ico { left: 11px; width: 14px; height: 14px; color: rgba(255, 255, 255, 0.7); }
.nav-search input { .nav-search input {
width: 100%; min-width: 0; font: inherit; font-size: 0.9rem; color: var(--text); width: 100%; min-width: 0; font: inherit; font-size: var(--fs-s); color: #fff;
background: #fff; border: 1px solid transparent; border-radius: var(--radius-pill); background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.45); /* 3.55:1 vs Galaxy — WCAG 1.4.11 */
padding: 8px 14px 8px 34px; border-radius: var(--radius-pill); padding: 7px 14px 7px 32px;
} }
.nav-search input:focus { border-color: var(--azure); } .nav-search input::placeholder { color: rgba(255, 255, 255, 0.65); } /* 6.9:1 */
.nav-search input:focus { border-color: #fff; }
.nav-search .suggest { left: auto; right: 0; width: min(380px, 92vw); top: calc(100% + 10px); } .nav-search .suggest { left: auto; right: 0; width: min(380px, 92vw); top: calc(100% + 10px); }
/* ≤1024px replacement: magnifier <details> reveal — see the menu media block */ /* ≤1024px replacement: a 44px magnifier <details> reveals a search row under
the header (NN/g mobile-search pattern) — see the menu media block. */
.search-pop { display: none; } .search-pop { display: none; }
.search-pop summary {
display: inline-flex; align-items: center; justify-content: center;
min-width: 44px; min-height: 44px; color: #fff;
}
.search-pop form {
position: absolute; left: 0; right: 0; top: 100%;
display: flex; justify-content: center; gap: 8px; padding: 12px var(--gutter) 16px;
background: var(--galaxy); border-top: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: var(--shadow-lifted);
}
.search-pop input {
flex: 0 1 560px; min-width: 0; font: inherit; color: var(--text);
background: #fff; border: 1px solid transparent; border-radius: var(--radius-pill);
padding: 10px 16px;
}
.search-pop input:focus { border-color: var(--azure); }
.search-pop .btn { padding: 10px 18px; }
.search-pop .suggest {
left: max(var(--gutter), 50vw - 330px); right: max(var(--gutter), 50vw - 330px);
top: calc(100% + 4px);
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.nav > nav { display: none; } .nav > nav { display: none; }
.menu { display: block; } .menu { display: block; }
.menu summary { .menu summary {
display: inline-flex; align-items: center; gap: 8px; min-height: 44px; display: inline-flex; align-items: center; gap: 8px; min-height: 44px;
padding: 0 10px; margin-left: 6px; color: #fff; font-weight: 500; padding: 0 10px; margin-left: 6px; color: #fff; font-weight: 500; white-space: nowrap;
cursor: pointer; list-style: none; white-space: nowrap;
} }
.menu summary::-webkit-details-marker { display: none; }
.menu nav { .menu nav {
position: absolute; left: 0; right: 0; top: 100%; position: absolute; left: 0; right: 0; top: 100%;
display: flex; flex-direction: column; padding: 6px var(--gutter) 14px; display: flex; flex-direction: column; padding: 6px var(--gutter) 14px;
@@ -203,32 +265,9 @@ img { max-width: 100%; height: auto; display: block; }
border-bottom: 1px solid rgba(255, 255, 255, 0.12); border-bottom: 1px solid rgba(255, 255, 255, 0.12);
} }
.menu nav a:last-child { border-bottom: 0; } .menu nav a:last-child { border-bottom: 0; }
.menu nav a[aria-current="page"] { color: var(--cherenkov); } .menu nav a[aria-current="page"] { font-weight: 700; }
/* Magnifier icon-button reveals a full-width search row under the header
(visible search beats search buried in the menu — NN/g mobile-search).
Same native-<details> panel pattern as the menu. */
.nav-search { display: none; }
.search-pop { display: block; }
.search-pop summary {
display: inline-flex; align-items: center; justify-content: center;
min-width: 44px; min-height: 44px; color: #fff; cursor: pointer; list-style: none;
}
.search-pop summary::-webkit-details-marker { display: none; }
.search-pop form {
position: absolute; left: 0; right: 0; top: 100%;
display: flex; gap: 8px; padding: 10px var(--gutter) 14px;
background: var(--galaxy); border-top: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: var(--shadow-lifted);
}
.search-pop input {
flex: 1; min-width: 0; font: inherit; color: var(--text);
background: #fff; border: 1px solid var(--border); border-radius: var(--radius-pill);
padding: 10px 16px;
}
.search-pop input:focus { border-color: var(--azure); }
.search-pop .btn { padding: 10px 18px; }
.search-pop .suggest { left: 10px; right: 10px; top: calc(100% + 4px); }
} }
@media (max-width: 640px) { .brand .brand-sub { display: none; } }
@media (max-width: 480px) { @media (max-width: 480px) {
.brand img { height: 22px; } .brand img { height: 22px; }
.menu summary .menu-label { display: none; } .menu summary .menu-label { display: none; }
@@ -237,8 +276,8 @@ img { max-width: 100%; height: auto; display: block; }
@media (max-width: 360px) { @media (max-width: 360px) {
.nav { padding: 0 14px; gap: 2px; } .nav { padding: 0 14px; gap: 2px; }
.brand { margin-right: 4px; } .brand { margin-right: 4px; }
.brand img { height: 20px; } /* logo stays 22px — 22px tall ≈ 105px wide, brand minimum is 100px digital */
.nav .login-mock { padding: 8px 12px; } .nav .login-mock { padding: 7px 12px; }
} }
/* External-link marker — SVG arrow via mask, inherits currentColor */ /* External-link marker — SVG arrow via mask, inherits currentColor */
.external::after { .external::after {
@@ -248,34 +287,37 @@ img { max-width: 100%; height: auto; display: block; }
mask: var(--ico-ext) center / contain no-repeat; mask: var(--ico-ext) center / contain no-repeat;
} }
/* Hero — navy band with a nebula glow (BRAND D.5). The static gradient below is /* Hero (home only) — image-led: LST-1 under the night sky, full-bleed beneath a
the reduced-motion fallback; with motion allowed, the glow moves to an animated Galaxy scrim (--hero-scrim; worst-case AA computed in DESIGN.md). With motion
pseudo-layer (see the motion block at the end of this file). */ allowed a Cherenkov/Azure glow layer drifts above the photo (see motion block). */
.hero-band { .hero-band {
background: var(--nebula), var(--galaxy); background: var(--hero-scrim), url("/brand/hero.jpg") center / cover, var(--galaxy);
color: #fff; padding: var(--space-band) 0; position: relative; color: #fff; padding: var(--space-band) 0; position: relative;
/* NO overflow:hidden here — it would clip the search-suggest dropdown. /* NO overflow:hidden here — it would clip the search-suggest dropdown.
The animated nebula is clipped by its own .band-bg frame instead. */ The animated glow is clipped by its own .band-bg frame instead. */
} }
.band-bg { position: absolute; inset: 0; overflow: hidden; pointer-events: none; } .band-bg { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.hero { position: relative; } /* content above the animated nebula layer */ .hero { position: relative; } /* content above the animated glow layer */
.hero h1 { font-size: clamp(2rem, 6vw, 3.6rem); margin: 0 0 10px; line-height: 1.12; font-weight: 700; } .hero h1 { font-size: var(--fs-display); margin: 0 0 12px; }
.hero p { color: var(--cherenkov); font-size: 1.08rem; max-width: 62ch; margin: 0; } /* subtitle per BRAND D.3.4 */ .hero p { color: var(--cherenkov); font-size: var(--fs-l); max-width: 62ch; margin: 0; } /* subtitle per BRAND D.3.4 */
.eyebrow { .eyebrow {
color: var(--on-galaxy-muted); font-weight: 600; text-transform: uppercase; color: var(--on-galaxy-muted); font-weight: 600; text-transform: uppercase;
letter-spacing: 0.14em; font-size: 0.72rem; margin-bottom: 8px; letter-spacing: 0.14em; font-size: var(--fs-xs); margin-bottom: 8px;
} }
/* Hero CTAs — the two core scientist tasks (SPEC §3.1): one Cherenkov primary, /* Hero CTAs — the two core scientist tasks (SPEC §3.1): one Cherenkov primary,
one quiet ghost. Links styled as buttons because they navigate. */ one quiet ghost. Links styled as buttons because they navigate. */
.hero-cta { display: flex; flex-wrap: wrap; gap: var(--space-s); margin-top: var(--space-m); } .hero-cta { display: flex; flex-wrap: wrap; gap: var(--space-s); margin-top: var(--space-l); }
a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; } a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
.btn--ghost { background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, 0.55); } .btn--ghost { background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, 0.55); }
.btn--ghost:hover { background: rgba(255, 255, 255, 0.1); color: #fff; } .btn--ghost:hover { background: rgba(255, 255, 255, 0.12); color: #fff; border-color: #fff; }
/* One-line latest-news teaser under the CTAs (editorial touch, no widgets) */
.hero .hero-teaser { margin-top: var(--space-m); font-size: 0.9rem; color: var(--on-galaxy-muted); max-width: none; } /* Section-landing head (news/search/proposals/dashboard/support) — white page,
.hero .hero-teaser a { color: #fff; text-underline-offset: 3px; } display-scale Galaxy h1 + standfirst; the navy band is reserved for / and /login. */
.hero .hero-teaser a:hover { color: var(--cherenkov); } .page-head { padding: var(--space-band) 0 var(--space-l); }
.page-head h1 { color: var(--galaxy); font-size: var(--fs-h1); margin: 0 0 10px; }
.page-head .standfirst { margin: 0; max-width: 62ch; }
.page-head + .page { padding-top: 0; } /* white-on-white: head already spaces */
/* Search field (/search page) — large pill input with inline magnifier; real GET form */ /* Search field (/search page) — large pill input with inline magnifier; real GET form */
.search-form { position: relative; display: flex; gap: 8px; max-width: 560px; } .search-form { position: relative; display: flex; gap: 8px; max-width: 560px; }
@@ -286,7 +328,7 @@ a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: no
border-radius: var(--radius-pill); padding: 13px 18px 13px 44px; border-radius: var(--radius-pill); padding: 13px 18px 13px 44px;
} }
.search-form input:focus { border-color: var(--azure); } .search-form input:focus { border-color: var(--azure); }
/* Live suggestions under the hero search — plain links, Tab-accessible */ /* Live suggestions — plain links, Tab-accessible */
.suggest { .suggest {
position: absolute; top: calc(100% + 8px); left: 0; right: 0; position: absolute; top: calc(100% + 8px); left: 0; right: 0;
z-index: 11; /* above the sticky header (10) — never slides under it on scroll */ z-index: 11; /* above the sticky header (10) — never slides under it on scroll */
@@ -303,14 +345,14 @@ a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: no
stop titles from wrapping). ≥44px target comes from the block padding. */ stop titles from wrapping). ≥44px target comes from the block padding. */
.suggest a { .suggest a {
flex: 1; min-width: 0; display: block; padding-block: 10px; flex: 1; min-width: 0; display: block; padding-block: 10px;
color: var(--text); font-weight: 500; font-size: 0.94rem; text-decoration: none; color: var(--text); font-weight: 500; font-size: var(--fs-s); text-decoration: none;
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.suggest small { margin-top: 12px; } /* date chip tracks the first title line */ .suggest small { margin-top: 12px; } /* date tracks the first title line */
.suggest::after { .suggest::after {
content: "Press Enter for all results"; display: block; margin-top: 4px; content: "Press Enter for all results"; display: block; margin-top: 4px;
padding: 8px 12px 2px; border-top: 1px solid var(--border); padding: 8px 12px 2px; border-top: 1px solid var(--border);
color: var(--muted); font-size: 0.78rem; color: var(--muted); font-size: var(--fs-xs);
} }
/* Section header row (h2 + optional "All news →" link) */ /* Section header row (h2 + optional "All news →" link) */
@@ -319,7 +361,7 @@ a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: no
gap: var(--space-s); margin: var(--space-l) 0 var(--space-m); gap: var(--space-s); margin: var(--space-l) 0 var(--space-m);
} }
.section-head:first-child { margin-top: 0; } .section-head:first-child { margin-top: 0; }
.section-head h2 { margin: 0; color: var(--galaxy); font-size: clamp(1.3rem, 2.5vw, 1.6rem); } .section-head h2 { margin: 0; color: var(--galaxy); font-size: var(--fs-h2); }
.more { font-weight: 600; text-decoration: none; white-space: nowrap; } .more { font-weight: 600; text-decoration: none; white-space: nowrap; }
.more:hover { text-decoration: underline; } .more:hover { text-decoration: underline; }
@@ -333,28 +375,22 @@ a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: no
.grid > .card:not(.card--featured, .tile) .body { display: grid; grid-template-rows: subgrid; grid-row: span 3; gap: 10px; } .grid > .card:not(.card--featured, .tile) .body { display: grid; grid-template-rows: subgrid; grid-row: span 3; gap: 10px; }
} }
/* News cards — image + category + clamped title/description + <time> meta; /* News cards — image + clamped title/description + <time> meta; whole card
whole card clickable via stretched link (research: NN/g & card-UI guides 2025). */ clickable via stretched link. Chrome is single-signal: hairline on white,
borderless on Moon (white-on-moon separates); hover = one elevate gesture. */
.card { .card {
position: relative; background: #fff; border: 1px solid var(--border); position: relative; background: #fff; border: 1px solid var(--border);
border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-ambient); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow-ambient);
display: flex; flex-direction: column; display: flex; flex-direction: column; line-height: 1.45;
} }
.card:hover, .card:focus-within { border-color: var(--azure); box-shadow: var(--shadow-lifted); } .band--moon :is(.card, .panel) { border-color: transparent; }
.card:hover, .card:focus-within { box-shadow: var(--shadow-lifted); }
/* Visible focus ring for the whole-card stretched link (WCAG 2.4.7) */ /* Visible focus ring for the whole-card stretched link (WCAG 2.4.7) */
.card:focus-within { box-shadow: 0 0 0 2px var(--azure), var(--shadow-lifted); } .card:focus-within { box-shadow: 0 0 0 2px var(--azure), var(--shadow-lifted); }
.card .cover { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } .card .cover { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.card .body { padding: var(--space-m); display: flex; flex-direction: column; gap: 10px; flex: 1; } .card .body { padding: var(--space-m); display: flex; flex-direction: column; gap: 10px; flex: 1; }
/* Cover-less cards get a Cherenkov top accent instead of an image */
.card:not(:has(.cover)) .body { border-top: 3px solid var(--cherenkov); }
.cat {
display: inline-block; align-self: flex-start; font-size: 0.72rem; text-transform: uppercase;
letter-spacing: 0.08em; font-weight: 600;
color: var(--galaxy); background: var(--tint-cherenkov);
padding: 4px 10px; border-radius: var(--radius-pill);
}
.card h3 { .card h3 {
margin: 2px 0 0; font-size: 1.1rem; line-height: 1.35; font-family: var(--font-body), sans-serif; margin: 2px 0 0; font-size: var(--fs-l); line-height: 1.3; font-family: var(--font-body), sans-serif;
display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
} }
.card h3 a { color: var(--galaxy); text-decoration: none; } .card h3 a { color: var(--galaxy); text-decoration: none; }
@@ -363,21 +399,20 @@ a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: no
.card h3 a::before { content: ""; position: absolute; inset: 0; } .card h3 a::before { content: ""; position: absolute; inset: 0; }
.card h3 a:hover { color: var(--link); } .card h3 a:hover { color: var(--link); }
.card .desc { .card .desc {
margin: 0; color: var(--muted); font-size: 0.94rem; margin: 0; color: var(--muted); font-size: var(--fs-s);
display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
} }
.card .meta { margin-top: auto; color: var(--muted); font-size: 0.8rem; } .card .meta { margin-top: auto; color: var(--muted); font-size: var(--fs-xs); }
/* Lead story — larger, splits when the grid itself is wide enough (container query) */ /* Lead story — larger, splits when the grid itself is wide enough (container query) */
@container (min-width: 640px) { @container (min-width: 640px) {
.card--featured { grid-column: 1 / -1; display: grid; grid-template-columns: 3fr 2fr; } .card--featured { grid-column: 1 / -1; display: grid; grid-template-columns: 3fr 2fr; }
.card--featured .cover { height: 100%; aspect-ratio: auto; } .card--featured .cover { height: 100%; aspect-ratio: auto; }
.card--featured .body { padding: var(--space-l); justify-content: center; } .card--featured .body { padding: var(--space-l); justify-content: center; }
.card--featured h3 { font-size: 1.6rem; font-family: var(--font-display), sans-serif; -webkit-line-clamp: 3; line-clamp: 3; } .card--featured h3 { font-size: var(--fs-xl); font-family: var(--font-display), sans-serif; font-weight: 500; -webkit-line-clamp: 3; line-clamp: 3; }
.card--featured .desc { -webkit-line-clamp: 3; line-clamp: 3; } .card--featured .desc { -webkit-line-clamp: 3; line-clamp: 3; }
} }
/* Pagination (news archive) — prev/next + windowed page numbers; ≥44px targets. /* Pagination (news archive) — prev/next + windowed page numbers; ≥44px targets. */
Pattern mined from AstroPaper/Astro blog templates, numbers added for 9 pages. */
.pagination { .pagination {
display: flex; align-items: center; justify-content: center; flex-wrap: wrap; display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
gap: 4px var(--space-s); margin-top: var(--space-xl); gap: 4px var(--space-s); margin-top: var(--space-xl);
@@ -388,7 +423,7 @@ a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: no
min-width: 44px; min-height: 44px; padding: 0 12px; min-width: 44px; min-height: 44px; padding: 0 12px;
border-radius: var(--radius-pill); text-decoration: none; font-weight: 500; color: var(--text); border-radius: var(--radius-pill); text-decoration: none; font-weight: 500; color: var(--text);
} }
a.page-link:hover { background: var(--moon); color: var(--indigo); } a.page-link:hover { background: var(--moon); }
.page-link[aria-current="page"] { background: var(--galaxy); color: #fff; } .page-link[aria-current="page"] { background: var(--galaxy); color: #fff; }
.page-step { font-weight: 600; } .page-step { font-weight: 600; }
a.page-step { color: var(--link); } a.page-step { color: var(--link); }
@@ -396,80 +431,96 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
.page-gap { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; min-height: 44px; color: var(--muted); } .page-gap { display: inline-flex; align-items: center; justify-content: center; min-width: 24px; min-height: 44px; color: var(--muted); }
/* Service tiles — card tokens; stroke glyph above an always-visible label /* Service tiles — card tokens; stroke glyph above an always-visible label
(NN/g: icons differentiate and speed scanning only when labelled) */ (NN/g: icons differentiate and speed scanning only when labelled).
Planned (not-yet-available) tiles are subdued: muted, no link, no chrome —
availability lives in the description text ("— planned"). */
.tile { padding: var(--space-m); gap: 8px; } .tile { padding: var(--space-m); gap: 8px; }
.tile .ico { width: 26px; height: 26px; stroke-width: 1.75; stroke-linejoin: round; color: var(--galaxy); } .tile .ico { width: 26px; height: 26px; stroke-width: 1.75; stroke-linejoin: round; color: var(--galaxy); }
.tile h3 { margin: 0; } .tile h3 { margin: 0; }
.tile .badge-mock, .tile .badge-ext { margin: auto 0 0; align-self: flex-start; } .tile--planned :is(h3, .ico) { color: var(--muted); }
/* Article — 70ch column; on ≥1200px a sticky "On this page" rail sits to the right */ /* Article — 70ch column; on ≥1200px a sticky "On this page" rail sits to the right */
.article-layout { display: grid; grid-template-columns: minmax(0, 70ch); } .article-layout { display: grid; grid-template-columns: minmax(0, 70ch); }
.article { padding: var(--space-l) 0 var(--space-xl); max-width: 70ch; } .article { padding: var(--space-l) 0 var(--space-xl); max-width: 70ch; }
.article .back { color: var(--muted); font-size: 0.9rem; } .article .back { color: var(--muted); font-size: var(--fs-s); }
.article h1 { .article h1 { color: var(--galaxy); font-size: var(--fs-h1); margin: 14px 0 10px; }
color: var(--galaxy); font-size: clamp(2rem, 5vw, 3rem);
margin: 14px 0 10px; line-height: 1.12;
}
.article .meta { .article .meta {
display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px;
color: var(--muted); font-size: 0.9rem; margin-bottom: var(--space-l); color: var(--muted); font-size: var(--fs-s); margin-bottom: var(--space-l);
}
/* Lead image — borderless, breaking slightly out of the text column on wide
screens (editorial breakout; the column stays 70ch for reading) */
.article .cover { border-radius: var(--radius); margin-bottom: var(--space-l); }
@media (min-width: 900px) {
.article .cover { margin-inline: -40px; width: calc(100% + 80px); max-width: none; }
} }
.article .cover { border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: var(--space-l); }
/* Standfirst — the description shown as the visible lead under the headline /* Standfirst — the description shown as the visible lead under the headline
(Guardian/BBC anatomy). Replaces the old first-prose-paragraph lead styling (Guardian/BBC anatomy); also the intro line under section-landing h1s. */
so an article never shows two competing leads. */ .standfirst { margin: 0 0 var(--space-s); font-size: var(--fs-l); line-height: 1.55; color: var(--muted); }
.article .standfirst { margin: 0 0 var(--space-s); font-size: 1.12rem; line-height: 1.55; color: var(--muted); } /* Article TOC (Stripe/Apple-docs pattern, rendered only when ≥3 h2/h3):
/* Article TOC (MDN/Stripe pattern, rendered only when ≥3 h2/h3): sticky right rail quiet "On this page" — sticky hairline-rule rail on wide screens, a light
on wide screens, collapsed <details> under the title otherwise. */ disclosure row otherwise. Links are muted, Galaxy on hover; the scrollspy
.toc { active state (Galaxy text + Cherenkov rule segment) is navigation state,
background: var(--moon); border: 1px solid var(--border); border-radius: var(--radius); not decoration — it sits outside the one-accent budget. */
padding: var(--space-s) var(--space-m); font-size: 0.92rem; .toc { font-size: var(--fs-s); }
}
.toc strong, .toc summary { .toc strong, .toc summary {
font-size: 0.72rem; text-transform: uppercase; font-weight: 600; font-size: var(--fs-xs); text-transform: uppercase; font-weight: 600;
letter-spacing: 0.08em; color: var(--muted); letter-spacing: 0.08em; color: var(--muted);
} }
.toc strong { display: block; margin-bottom: 4px; } .toc strong { display: block; margin-bottom: 10px; }
.toc summary { cursor: pointer; padding: 6px 0; }
.toc ol { list-style: none; margin: 0; padding: 0; } .toc ol { list-style: none; margin: 0; padding: 0; }
.toc li { margin: 8px 0; } .toc li { margin: 6px 0; }
.toc .d3 { padding-left: var(--space-s); } .toc .d3 { padding-left: var(--space-s); }
.toc a { text-decoration: none; display: inline-flex; align-items: center; min-height: 24px; } .toc a {
.toc a:hover { text-decoration: underline; } color: var(--muted); text-decoration: none;
display: inline-flex; align-items: center; min-height: 24px; position: relative;
}
.toc a:hover { color: var(--galaxy); }
/* Scrollspy state (set by the [slug].astro script) — functional, not motion */ /* Scrollspy state (set by the [slug].astro script) — functional, not motion */
.toc a[aria-current="true"] { color: var(--galaxy); font-weight: 600; } .toc a[aria-current="true"] { color: var(--galaxy); font-weight: 600; }
.toc--inline { margin-bottom: var(--space-l); } /* Inline rendering (< 1200px): light disclosure row with a chevron */
.toc--inline { border: 1px solid var(--border); border-radius: var(--radius); padding: 4px var(--space-m); margin-bottom: var(--space-l); }
.toc--inline summary { display: flex; align-items: center; justify-content: space-between; gap: var(--space-s); min-height: 40px; }
.toc--inline summary .ico { width: 14px; height: 14px; }
.toc--inline[open] summary .ico { transform: rotate(180deg); }
.toc--inline ol { padding-bottom: var(--space-s); }
.toc--rail { display: none; } .toc--rail { display: none; }
@media (min-width: 1200px) { @media (min-width: 1200px) {
.article-layout { grid-template-columns: minmax(0, 70ch) 15rem; column-gap: var(--space-xl); justify-content: space-between; } .article-layout { grid-template-columns: minmax(0, 70ch) 15rem; column-gap: var(--space-xl); justify-content: space-between; }
.toc--inline { display: none; } .toc--inline { display: none; }
.toc--rail { .toc--rail {
display: block; position: sticky; top: 88px; align-self: start; display: block; position: sticky; top: 80px; align-self: start;
margin-top: calc(var(--space-l) + 44px); /* aligns with the article h1 */ margin-top: calc(var(--space-l) + 44px); /* aligns with the article h1 */
max-height: calc(100dvh - 112px); overflow: auto; max-height: calc(100dvh - 104px); overflow: auto;
border-left: 1px solid var(--border); padding-left: var(--space-m);
}
/* Active rule segment: 2px Cherenkov over the hairline, tracking the link */
.toc--rail a[aria-current="true"]::before {
content: ""; position: absolute; left: calc(-1 * var(--space-m) - 1px);
top: 3px; bottom: 3px; width: 2px; background: var(--cherenkov);
} }
} }
/* Long unbroken strings (URLs, identifiers) wrap instead of forcing page overflow */ /* Long unbroken strings (URLs, identifiers) wrap instead of forcing page overflow */
.prose { overflow-wrap: break-word; } .prose { line-height: 1.65; overflow-wrap: break-word; }
.prose a { overflow-wrap: anywhere; } .prose a { overflow-wrap: anywhere; }
.prose h2 { color: var(--galaxy); margin: 34px 0 10px; font-size: clamp(1.25rem, 2.6vw, 1.45rem); } .prose h2 { color: var(--galaxy); margin: 44px 0 12px; font-size: var(--fs-xl); }
.prose h3 { color: var(--galaxy); margin: 26px 0 8px; font-size: 1.15rem; } .prose h3 { color: var(--galaxy); margin: 30px 0 8px; font-size: var(--fs-l); }
.prose p { margin: 0 0 16px; } .prose p { margin: 0 0 16px; }
.prose ul, .prose ol { margin: 0 0 16px; padding-left: 22px; } .prose ul, .prose ol { margin: 0 0 16px; padding-left: 22px; }
.prose li { margin: 6px 0; } .prose li { margin: 6px 0; }
.prose blockquote { .prose blockquote {
margin: 22px 0; padding: 12px 20px; border-left: 3px solid var(--cherenkov); margin: 22px 0; padding: 12px 20px; border-left: 3px solid var(--galaxy);
background: var(--moon); color: var(--text); background: var(--moon); color: var(--text);
} }
.prose img { border: 1px solid var(--border); border-radius: var(--radius); } .prose img { border: 1px solid var(--border); border-radius: var(--radius); }
.prose img + em, .prose p > em:only-child { color: var(--muted); font-size: 0.85rem; } .prose img + em, .prose p > em:only-child { color: var(--muted); font-size: var(--fs-s); }
.prose hr { border: 0; border-top: 1px solid var(--border); margin: 32px 0; } .prose hr { border: 0; border-top: 1px solid var(--border); margin: 32px 0; }
/* Wide tables scroll inside their own box, never the page (mobile guard) */ /* Wide tables scroll inside their own box, never the page (mobile guard) */
.prose table { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; margin: 0 0 16px; font-size: 0.94rem; } .prose table { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; margin: 0 0 16px; font-size: var(--fs-s); }
.prose th { text-align: left; font-weight: 600; color: var(--galaxy); } .prose th { text-align: left; font-weight: 600; color: var(--galaxy); }
.prose th, .prose td { padding: 10px 12px; border-bottom: 1px solid var(--border); } .prose th, .prose td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
.prose thead th { border-bottom: 2px solid var(--cherenkov); } .prose thead th { border-bottom: 2px solid var(--galaxy); }
/* Prev/next article links — sequential nav from the date-sorted collection */ /* Prev/next article links — sequential nav from the date-sorted collection */
.post-nav { .post-nav {
@@ -480,7 +531,7 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
.post-nav .post-nav-prev { grid-column: 1; } .post-nav .post-nav-prev { grid-column: 1; }
.post-nav .post-nav-next { grid-column: 2; text-align: right; } .post-nav .post-nav-next { grid-column: 2; text-align: right; }
.post-nav small { .post-nav small {
display: block; color: var(--muted); font-size: 0.72rem; font-weight: 600; display: block; color: var(--muted); font-size: var(--fs-xs); font-weight: 600;
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 4px;
} }
.post-nav .post-nav-title { .post-nav .post-nav-title {
@@ -497,7 +548,7 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
} }
.panel--narrow { max-width: 640px; } .panel--narrow { max-width: 640px; }
.form-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; } .form-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.form-row label { font-size: 0.88rem; font-weight: 500; } .form-row label { font-size: var(--fs-s); font-weight: 500; }
.form-row input, .form-row select, .form-row textarea { .form-row input, .form-row select, .form-row textarea {
background: #fff; border: 1px solid var(--border); border-radius: calc(var(--radius) - 4px); background: #fff; border: 1px solid var(--border); border-radius: calc(var(--radius) - 4px);
color: var(--text); padding: 11px 12px; font: inherit; color: var(--text); padding: 11px 12px; font: inherit;
@@ -509,35 +560,33 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
font: inherit; font-weight: 600; cursor: pointer; font: inherit; font-weight: 600; cursor: pointer;
} }
.btn:hover { background: var(--cherenkov-hover); } .btn:hover { background: var(--cherenkov-hover); }
.notice { color: var(--muted); font-size: 0.85rem; margin-top: 10px; } .notice { color: var(--muted); font-size: var(--fs-s); margin-top: 10px; }
/* Search results (list built by the inline script on /search) — card-like rows */ /* Search results (list built by the inline script on /search) — card-like rows;
same single-signal chrome as cards (borderless on Moon, shadow hover) */
.search-results { list-style: none; margin: var(--space-m) 0 0; padding: 0; max-width: 720px; display: grid; gap: var(--space-s); } .search-results { list-style: none; margin: var(--space-m) 0 0; padding: 0; max-width: 720px; display: grid; gap: var(--space-s); }
.search-results li { .search-results li {
display: flex; align-items: flex-start; gap: var(--space-s); display: flex; align-items: flex-start; gap: var(--space-s);
background: #fff; border: 1px solid var(--border); border-radius: var(--radius); background: #fff; border-radius: var(--radius);
padding: 12px 18px; box-shadow: var(--shadow-ambient); padding: 12px 18px; box-shadow: var(--shadow-ambient);
} }
.search-results li:hover { border-color: var(--azure); box-shadow: var(--shadow-lifted); } .search-results li:hover { box-shadow: var(--shadow-lifted); }
/* Title flows as inline text (never display:flex — see .suggest a note) */ /* Title flows as inline text (never display:flex — see .suggest a note) */
.search-results a { flex: 1; min-width: 0; overflow-wrap: break-word; text-decoration: none; font-weight: 500; } .search-results a { flex: 1; min-width: 0; overflow-wrap: break-word; text-decoration: none; font-weight: 500; }
.search-results a:hover { text-decoration: underline; } .search-results a:hover { text-decoration: underline; }
/* Date chips on results and suggestions */ /* Dates on results and suggestions — plain muted text, no chip chrome */
.search-results small, .suggest small { .search-results small, .suggest small { color: var(--muted); white-space: nowrap; font-size: var(--fs-xs); }
color: var(--muted); white-space: nowrap; font-size: 0.78rem; .search-results small { margin-top: 5px; } /* optical align with the first title line */
background: var(--moon); border-radius: var(--radius-pill); padding: 2px 10px;
}
.search-results small { margin-top: 3px; } /* optical align with the first title line */
/* Matched-term highlight in results/suggestions (Starlight search pattern) — /* Matched-term highlight in results/suggestions (Starlight search pattern) —
brand tint only, no new hues */ functional state, brand tint, no new hues */
mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(var(--radius) - 12px); padding: 0 2px; } mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(var(--radius) - 12px); padding: 0 2px; }
/* Mock/status badges — every mock stays labelled (REQUIREMENTS.md §5), but the /* Mock/status badges — whisper-quiet hairline pill, ONLY where a mock
Demo phase banner is loud once, so per-element badges only whisper: quiet interaction actually happens (/proposals, /dashboard, /login, /support head);
hairline pill, muted text (7.1:1 on Moon), sentence case, Galaxy dot on mocks. */ REQUIREMENTS §5 honesty lives at the interaction point, not on every tile. */
.badge-mock, .badge-ext { .badge-mock, .badge-ext {
display: inline-flex; align-items: center; gap: 6px; max-width: 100%; display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
font-size: 0.75rem; font-weight: 500; color: var(--muted); font-size: var(--fs-xs); font-weight: 500; color: var(--muted);
background: var(--moon); border: 1px solid var(--border); background: var(--moon); border: 1px solid var(--border);
border-radius: var(--radius-pill); padding: 2px 10px; margin-bottom: 14px; border-radius: var(--radius-pill); padding: 2px 10px; margin-bottom: 14px;
} }
@@ -546,43 +595,49 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
/* Dashboard mock — aggregation panels with status pills */ /* Dashboard mock — aggregation panels with status pills */
.dash-grid { display: grid; gap: var(--space-m); grid-template-columns: 1fr; margin-top: var(--space-s); } .dash-grid { display: grid; gap: var(--space-m); grid-template-columns: 1fr; margin-top: var(--space-s); }
@media (min-width: 940px) { .dash-grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 940px) { .dash-grid { grid-template-columns: repeat(2, 1fr); } }
.panel-title { color: var(--galaxy); font-size: 1.1rem; margin: 0 0 var(--space-s); font-family: var(--font-body), sans-serif; font-weight: 600; } .panel-title { color: var(--galaxy); font-size: var(--fs-l); margin: 0 0 var(--space-s); font-family: var(--font-body), sans-serif; font-weight: 600; }
.rows { list-style: none; margin: 0; padding: 0; } .rows { list-style: none; margin: 0; padding: 0; }
.rows li { .rows li {
display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 0.92rem; padding: 10px 0; border-bottom: 1px solid var(--border); font-size: var(--fs-s);
} }
.rows li:last-child { border-bottom: 0; } .rows li:last-child { border-bottom: 0; }
.row-id { color: var(--muted); font-size: 0.8rem; min-width: 7.5em; } .row-id { color: var(--muted); font-size: var(--fs-xs); min-width: 7.5em; }
.row-main { flex: 1; min-width: 12em; } .row-main { flex: 1; min-width: 12em; }
.row-date { color: var(--muted); font-size: 0.8rem; } .row-date { color: var(--muted); font-size: var(--fs-xs); }
/* Status pills — neutral chrome; the label text carries the state */
.pill { .pill {
font-size: 0.72rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
padding: 3px 10px; border-radius: var(--radius-pill); border: 1px solid var(--border); padding: 3px 10px; border-radius: var(--radius-pill); border: 1px solid var(--border);
} }
.pill--ok { color: var(--galaxy); background: var(--tint-cherenkov); } .pill--ok { color: var(--galaxy); background: var(--moon); }
.pill--review { color: var(--galaxy); background: var(--moon); } .pill--review { color: var(--text); background: #fff; }
.pill--draft { color: var(--muted); background: #fff; } .pill--draft { color: var(--muted); background: #fff; }
/* Auth (login mock) — nebula backdrop + floating card, modern auth pattern */ /* Auth (login mock) — nebula backdrop + floating card; the ONLY navy band
besides the home hero */
.auth-band { .auth-band {
min-height: calc(100dvh - 64px); position: relative; min-height: calc(100dvh - 56px); position: relative;
display: grid; place-items: center; padding: var(--space-band) var(--gutter); display: grid; place-items: center; padding: var(--space-band) var(--gutter);
background: var(--nebula), var(--galaxy); background: var(--nebula), var(--galaxy);
} }
.auth-card { .auth-card {
position: relative; background: #fff; border-radius: calc(var(--radius) + 8px); position: relative; background: #fff; border-radius: calc(var(--radius) + 8px);
padding: clamp(24px, 5vw, 40px) clamp(20px, 4.5vw, 36px); max-width: 440px; width: 100%; padding: var(--space-l); max-width: 440px; width: 100%;
box-shadow: var(--shadow-lifted); box-shadow: var(--shadow-lifted);
} }
.auth-logo { height: 24px; width: auto; margin-bottom: var(--space-m); } .auth-logo { height: 24px; width: auto; margin-bottom: var(--space-m); }
.auth-card h1 { color: var(--galaxy); margin: 6px 0 10px; font-size: 1.8rem; } /* Whisper badge sits top-right, off the reading flow (back in flow ≤480px
.auth-card .muted { color: var(--muted); font-size: 0.95rem; } where it would collide with the logo) */
.auth-card .badge-mock { position: absolute; top: var(--space-l); right: var(--space-l); margin: 0; }
@media (max-width: 480px) { .auth-card .badge-mock { position: static; margin-bottom: 14px; } }
.auth-card h1 { color: var(--galaxy); margin: 6px 0 10px; font-size: var(--fs-xl); }
.auth-card .muted { color: var(--muted); font-size: var(--fs-s); }
.btn-wide { width: 100%; margin-top: var(--space-s); } .btn-wide { width: 100%; margin-top: var(--space-s); }
/* Footer — brand band: logo + descriptor / spec links (§3.3.1) / demo note + flash. /* Footer — brand band: logo + descriptor / spec links (§3.3.1) / demo note + flash.
No top margin: Moon/Galaxy bands meet it flush; white pages pad via .page/.article. */ No top margin: Moon/Galaxy bands meet it flush; white pages pad via .page/.article. */
.footer { background: var(--galaxy); color: var(--on-galaxy-muted); font-size: 0.85rem; } .footer { background: var(--galaxy); color: var(--on-galaxy-muted); font-size: var(--fs-s); line-height: 1.45; }
.footer .container { display: grid; gap: var(--space-m); padding-top: var(--space-xl); padding-bottom: var(--space-l); } .footer .container { display: grid; gap: var(--space-m); padding-top: var(--space-xl); padding-bottom: var(--space-l); }
.foot-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-s) var(--space-m); } .foot-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-s) var(--space-m); }
.foot-top img { height: 30px; width: auto; } .foot-top img { height: 30px; width: auto; }
@@ -593,11 +648,11 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
.foot-links a:not([href]) { color: var(--on-galaxy-muted); } .foot-links a:not([href]) { color: var(--on-galaxy-muted); }
.foot-note { display: flex; align-items: center; gap: var(--space-s); border-top: 1px solid rgba(255, 255, 255, 0.15); padding-top: var(--space-m); } .foot-note { display: flex; align-items: center; gap: var(--space-s); border-top: 1px solid rgba(255, 255, 255, 0.15); padding-top: var(--space-m); }
/* Whole-page ambient (2026 pattern) — a fixed brand-hue wash behind everything, /* Whole-page ambient — a fixed brand-hue wash behind everything, opt-in per
opt-in per page via <Base ambient> (home only). Clearly visible pastel fields page via <Base ambient> (home only). Clearly visible pastel fields (alphas
(alphas .16/.12/.10 — ceilings: links on the worst blob overlap stay ≥ 5.3:1); .16/.12/.10 — ceilings: links on the worst blob overlap stay ≥ 5.3:1); all
all reading surfaces (cards, panels, bands) are solid and cover it. Static reading surfaces (cards, panels, bands) are solid and cover it. Static when
when motion is reduced. */ motion is reduced. */
.has-ambient::before { .has-ambient::before {
content: ""; position: fixed; inset: -30%; z-index: -1; pointer-events: none; content: ""; position: fixed; inset: -30%; z-index: -1; pointer-events: none;
background: background:
@@ -608,44 +663,47 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
/* ========================================================================== /* ==========================================================================
Motion — ALL animation/transition lives in this block (WCAG 2.3.3; the Motion — ALL animation/transition lives in this block (WCAG 2.3.3; the
reduce block above is a second safety net). Nebula translation of ctao.org: reduce block above is a second safety net). One hover language: every
they drift looping videos under Galaxy-Blue vignettes; here an oversized interactive element fades color/background/border/shadow on the SAME two
gradient layer drifts via transform only (compositor-friendly, no repaints). tokens (--dur/--ease — ctao.org's quiet-fade feel); cards elevate as one
Worst-case blob-over-text contrast verified ≥4.5:1 (see DESIGN.md). gesture (shadow + 2px lift). Nebula/ambient drift is transform-only and
sized to be clearly perceptible within ~10s of looking.
========================================================================== */ ========================================================================== */
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
@view-transition { navigation: auto; } @view-transition { navigation: auto; }
html { scroll-behavior: smooth; } html { scroll-behavior: smooth; }
/* Bands hand their glow to a slowly roaming layer inside the .band-bg clip /* The auth band hands its glow to a roaming layer inside the .band-bg clip
frame (same --nebula stack) — the band itself never clips its overlays */ frame (same --nebula stack); the photo hero keeps its scrimmed photo and
.hero-band, .auth-band { background: var(--galaxy); } gets the lighter --nebula-glow drifting above it. */
.auth-band { background: var(--galaxy); }
.band-bg::before { .band-bg::before {
content: ""; position: absolute; inset: -45% -30%; content: ""; position: absolute; inset: -45% -30%;
background: var(--nebula); background: var(--nebula);
animation: nebula 22s ease-in-out infinite alternate; animation: nebula 16s ease-in-out infinite alternate;
} }
/* Whole-page wash — slow drift, clearly alive within ~15s of looking */ .hero-band .band-bg::before { background: var(--nebula-glow); }
.has-ambient::before { animation: ambient 60s ease-in-out infinite alternate; } /* Whole-page wash — slow drift, clearly alive */
.has-ambient::before { animation: ambient 36s ease-in-out infinite alternate; }
/* Hero content rises in once, gently staggered */ /* Hero content rises in once, gently staggered */
.hero > * { animation: rise 0.5s ease-out backwards; } .hero > * { animation: rise 0.5s var(--ease) backwards; }
.hero > :nth-child(2) { animation-delay: 0.08s; } .hero > :nth-child(2) { animation-delay: 0.08s; }
.hero > :nth-child(3) { animation-delay: 0.16s; } .hero > :nth-child(3) { animation-delay: 0.16s; }
.hero > :nth-child(4) { animation-delay: 0.24s; } .hero > :nth-child(4) { animation-delay: 0.24s; }
.hero > :nth-child(5) { animation-delay: 0.32s; } .hero > :nth-child(5) { animation-delay: 0.32s; }
/* Hover feedback */ /* Unified hover feedback — single-property fades, shared tokens */
.card, .search-results li { transition: border-color 0.15s ease, box-shadow 0.2s ease, transform 0.2s ease; } a, .btn, .post-nav-title {
.card:hover { transform: translateY(-2px); } transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease),
/* Editorial cover micro-zoom on card hover (AstroWind pattern; card clips it) */ border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
.card .cover { transition: transform 0.3s ease; } }
.card:hover .cover, .card:focus-within .cover { transform: scale(1.03); } .card, .search-results li { transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease); }
.btn, .login-mock, .navlink { transition: background-color 0.15s ease, color 0.15s ease; } .card:hover { transform: translateY(-2px); } /* the one elevate gesture (no cover zoom) */
} }
@keyframes nebula { /* inert unless applied inside the block above */ @keyframes nebula { /* inert unless applied inside the block above */
from { transform: translate3d(-6%, -4%, 0) rotate(-5deg) scale(1); } from { transform: translate3d(-8%, -5%, 0) rotate(-6deg) scale(1); }
to { transform: translate3d(6%, 5%, 0) rotate(5deg) scale(1.22); } to { transform: translate3d(8%, 6%, 0) rotate(6deg) scale(1.3); }
} }
@keyframes ambient { @keyframes ambient {
from { transform: translate3d(-4%, -3%, 0) rotate(-9deg) scale(1); opacity: 0.85; } from { transform: translate3d(-5%, -4%, 0) rotate(-10deg) scale(1); opacity: 0.85; }
to { transform: translate3d(4%, 4%, 0) rotate(9deg) scale(1.14); opacity: 1; } to { transform: translate3d(5%, 5%, 0) rotate(10deg) scale(1.16); opacity: 1; }
} }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } } @keyframes rise { from { opacity: 0; transform: translateY(14px); } }