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:
@@ -2,26 +2,44 @@
|
||||
|
||||
Single source of truth for look-and-feel. All values live as tokens in
|
||||
`src/styles/global.css`; this file states the rules. Brand basis: CTAO Brand
|
||||
Guidelines (Apr 2024) + techniques observed on ctao.org (nebula motion, generous
|
||||
spacing, 15/30/60 rounding). No frameworks, no JS for presentation.
|
||||
Guidelines (Apr 2024) + techniques observed on ctao.org (photography-led hero,
|
||||
nebula motion, generous spacing, 15/30/60 rounding, quiet-fade hovers).
|
||||
No frameworks, no JS for presentation.
|
||||
|
||||
## Color tokens (brand-fixed — never add hues)
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--galaxy` | `#00004A` | Headings, header/footer/hero bands |
|
||||
| `--cherenkov` | `#00E4D8` | Accents, flash, CTAs, hero subtitle. Never small text on light bg |
|
||||
| `--moon` | `#F5F5F5` | Utility surfaces (bands, TOC, chips) |
|
||||
| `--azure` | `#007AFF` | Focus rings, hover borders (UI only on light) |
|
||||
| `--galaxy` | `#00004A` | Headings, header/footer bands, hero scrim |
|
||||
| `--cherenkov` | `#00E4D8` | ONLY: primary CTA, flash, hero subtitle, logo cluster (+ functional: `<mark>`, TOC active rule). Never small text on light bg |
|
||||
| `--moon` | `#F5F5F5` | Utility surfaces (bands, neutral hovers) |
|
||||
| `--azure` | `#007AFF` | Focus rings and field-focus borders ONLY (no hover borders) |
|
||||
| `--indigo` | `#00009C` | Gradient depth, link hover |
|
||||
| `--text` / `--muted` / `--border` / `--link` | derived | Body ink; `--link #0057C2` is azure darkened to pass AA |
|
||||
|
||||
Accent discipline: Cherenkov is rationed to the four brand moments above.
|
||||
Status pills, category/date chips and metadata are plain muted text — the
|
||||
label carries the meaning, not the chrome.
|
||||
|
||||
## Typography
|
||||
|
||||
| Role | Font | Rule |
|
||||
|---|---|---|
|
||||
| h1, h2 | Space Grotesk (`--font-display`) | Headlines ONLY (brand D.3.2) |
|
||||
| Everything else | Inter (`--font-body`) | Includes h3+, cards, UI |
|
||||
| h1, h2 | Space Grotesk (`--font-display`) | Headlines ONLY (brand D.3.2), weight **500** (D.3.4 regular/medium), letter-spacing −0.025em, line-height 1.1 |
|
||||
| Everything else | Inter (`--font-body`) | Includes h3+ (weight 600), cards, UI |
|
||||
|
||||
Type scale — the ONLY font sizes in the codebase (no ad-hoc rem values):
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `--fs-xs` | 0.8125rem | Meta, badges, labels, eyebrow, dates |
|
||||
| `--fs-s` | 0.9rem | Secondary text, nav, forms, footer, TOC |
|
||||
| (base) | 1rem | Body, line-height 1.5 (1.65 only on `.prose`; 1.55 `.standfirst`; 1.45 cards/footer) |
|
||||
| `--fs-l` | 1.125rem | Card titles, standfirst, panel titles, hero subtitle |
|
||||
| `--fs-xl` | 1.5rem | Prose h2, featured title, auth h1 |
|
||||
| `--fs-h2` | clamp(1.75–2.5rem) | Section heads |
|
||||
| `--fs-h1` | clamp(2.25–3.5rem) | Page/article headlines |
|
||||
| `--fs-display` | clamp(2.5–4.75rem) | Home hero only |
|
||||
|
||||
Always end `font-family` with a generic (`sans-serif`) — lint requirement.
|
||||
|
||||
@@ -31,10 +49,10 @@ Always end `font-family` with a generic (`sans-serif`) — lint requirement.
|
||||
|---|---|---|
|
||||
| `--gutter` | `clamp(18px, 4vw, 28px)` | Container/menu side padding |
|
||||
| `--space-s` | `12px` | Chip gaps, small stacks |
|
||||
| `--space-m` | `20px` | Card/panel padding, grid gaps |
|
||||
| `--space-m` | `24px` | Card/panel padding, grid gaps |
|
||||
| `--space-l` | `24–40px` | Article top, meta/cover margins, featured body |
|
||||
| `--space-xl` | `40–72px` | Section rhythm: every `.page` block, footer top |
|
||||
| `--space-band` | `48–96px` | Hero + auth band vertical padding |
|
||||
| `--space-xl` | `clamp(56px, 7vw, 120px)` | Section rhythm: every `.page` block, footer top |
|
||||
| `--space-band` | `clamp(72px, 10vw, 160px)` | Hero/auth band + landing-head padding |
|
||||
|
||||
Rule: section/container/card padding comes from this table — no ad-hoc px.
|
||||
Prose-internal margins (h2/p/blockquote) are content-local and stay as written.
|
||||
@@ -43,197 +61,233 @@ Prose-internal margins (h2/p/blockquote) are content-local and stay as written.
|
||||
|
||||
Radius: `--radius 16px` (cards/panels), `--radius-pill` (buttons, chips, inputs).
|
||||
|
||||
Only two shadows exist, both Galaxy-tinted (`rgba(0,0,74,…)`):
|
||||
Only two shadows exist, both Galaxy-tinted, tuned soft/large/low-alpha
|
||||
(2025/26 elevation convention: big blur, single-digit alpha at rest):
|
||||
|
||||
| Token | Use |
|
||||
|---|---|
|
||||
| `--shadow-ambient` | Resting cards, panels, search rows |
|
||||
| `--shadow-lifted` | Hover/focus cards, dropdowns (suggest, mobile menu), auth card |
|
||||
| `--shadow-lifted` | Hover/focus cards, dropdowns (suggest, menu, search row), auth card |
|
||||
|
||||
Never write a literal `box-shadow`; compose focus rings as
|
||||
`0 0 0 2px var(--azure), var(--shadow-lifted)`.
|
||||
|
||||
## Surfaces
|
||||
|
||||
Rhythm per page: Galaxy hero band → content → Galaxy footer (flush, no margin).
|
||||
White = editorial (news grids, articles). `.band--moon` = service/utility
|
||||
sections (home Services, /search, /support, /proposals, /dashboard). Cards and
|
||||
panels are always white and pop against Moon via border + ambient shadow.
|
||||
ONE surface per page — surface differentiation is by page archetype, never a
|
||||
mid-page seam (consistency heuristic: a surface change must read as deliberate
|
||||
system, not "why is this one gray?"):
|
||||
|
||||
- **Editorial pages** (`/news`, articles, static pages, home news section) are
|
||||
white — reading surfaces.
|
||||
- **Utility/app pages** (`/search`, `/proposals`, `/dashboard`, `/support`)
|
||||
are `.band--moon` from the landing head down to the footer — the app canvas
|
||||
against which borderless white cards/panels pop (standard app-vs-editorial
|
||||
surface split; home's Services section uses the same Moon band).
|
||||
- `/` and `/login` own the navy bands (photo hero / nebula).
|
||||
|
||||
Cards and panels are always white; ON MOON they are borderless (white-on-moon
|
||||
separates by itself), on white they keep the hairline. Chrome is
|
||||
single-signal: hover = the elevate gesture (shadow + 2px lift as ONE motion);
|
||||
no hover borders, no cover zoom.
|
||||
|
||||
## Motion policy (CSS-only, Apple-tier restraint)
|
||||
|
||||
ctao.org animates nebula videos under Galaxy vignettes; our translation:
|
||||
ctao.org drifts looping nebula videos under Galaxy vignettes; our translation:
|
||||
|
||||
1. **Band nebula** — one `--nebula` stack (Cherenkov .38 / Azure .33 /
|
||||
Indigo .80 radial fields) used by hero + auth bands, static and animated
|
||||
alike; the animated variant is an oversized `.band-bg::before` layer
|
||||
drifting via `transform` only, 22s alternate. Clearly visible glow, never
|
||||
neon. `.band-bg` (first child of every band) owns the `overflow: hidden`
|
||||
clip — NEVER put overflow on the band itself: it would clip overlays like
|
||||
the search-suggest dropdown.
|
||||
2. **Whole-page ambient** — `.has-ambient::before`: fixed brand-hue wash
|
||||
(Cherenkov .16 / Azure .12 / Indigo .10 — visible pastel fields, white
|
||||
cards pop against them) drifting on a 60s alternate cycle. Opt-in per page
|
||||
via `<Base ambient>`: **home only**. Login's full-viewport auth nebula
|
||||
already is its ambient; news/article/utility pages stay calm (reading
|
||||
focus). Body text NEVER sits on a moving gradient — cards, panels and
|
||||
bands are solid layers above it.
|
||||
3. One-shot hero `rise` stagger; hover lift/transitions, incl. the card-cover
|
||||
micro-zoom (scale 1.03, clipped by the card — AstroWind pattern). Nothing else.
|
||||
1. **Hero glow (home)** — the photo hero keeps its scrimmed photograph; a
|
||||
`--nebula-glow` layer (Cherenkov .30 / Azure .28, no indigo over
|
||||
photography) drifts above it inside the `.band-bg` clip frame: 16s
|
||||
alternate, transform-only, clearly perceptible within ~10s. `.band-bg`
|
||||
(first child of the band) owns `overflow: hidden` — NEVER put overflow on
|
||||
the band itself (it would clip the search-suggest dropdown).
|
||||
2. **Auth nebula (/login)** — the full `--nebula` stack (Cherenkov .38 /
|
||||
Azure .33 / Indigo .80), same 16s drifting layer; static gradient is the
|
||||
reduced-motion fallback.
|
||||
3. **Whole-page ambient** — `.has-ambient::before`: fixed brand-hue wash
|
||||
(Cherenkov .16 / Azure .12 / Indigo .10) drifting on a 36s alternate
|
||||
cycle. Opt-in via `<Base ambient>`: **home only**. Body text NEVER sits on
|
||||
a moving gradient — cards, panels and bands are solid layers above it.
|
||||
4. One-shot hero `rise` stagger; unified hover system (below). Nothing else.
|
||||
|
||||
**One hover language** (mid-pass audit: hovers were inconsistent per element;
|
||||
ctao.org uses a single quiet-fade token — we do the same): every interactive
|
||||
element transitions color/background/border/shadow on `--dur: 0.25s` +
|
||||
`--ease: ease-out` — buttons and links fade, cards/search rows elevate
|
||||
(shadow + 2px lift), pagination fills Moon. No width/layout animation.
|
||||
|
||||
Rules:
|
||||
- ALL `animation`/`transition` declarations live inside the single
|
||||
`@media (prefers-reduced-motion: no-preference)` block at the end of
|
||||
global.css; the `reduce` block additionally force-disables everything.
|
||||
- Animate `transform`/`opacity` only (compositor-only, per web.dev/MDN); never
|
||||
background-position/box-shadow in keyframes. No `will-change` (animated
|
||||
properties are auto-promoted — MDN). No `filter: blur` on large layers and no
|
||||
`background-attachment: fixed` (broken on iOS) — the wash is a fixed-position
|
||||
layer with pre-blurred radial gradients. No JS, no scroll-triggered motion.
|
||||
- Static gradients remain as the reduced-motion fallback (bands swap them for
|
||||
the animated layer — never both, keeping contrast bounded; the ambient wash
|
||||
simply stops moving).
|
||||
- Animate `transform`/`opacity` only (compositor-only); never
|
||||
background-position/box-shadow in keyframes. No `will-change`. No
|
||||
`filter: blur` on large layers and no `background-attachment: fixed`
|
||||
(broken on iOS). No JS, no scroll-triggered motion.
|
||||
- If an effect calls attention to itself as an effect, cut it.
|
||||
|
||||
## Photographic hero (home)
|
||||
|
||||
Image-led hero per brand D.6 (photography theme: the project): LST-1 under
|
||||
the night sky, copied from the content library to `public/brand/hero.jpg`
|
||||
(NEVER link `public/uploads` directly — that directory is content-synced).
|
||||
Text safety comes from `--hero-scrim` (Galaxy, `to right`:
|
||||
.84 → .76 @52% → .44 @78% → .26); ≤640px a flat .80 scrim replaces it (text
|
||||
spans the full width on mobile). The logo never sits on the photograph — it
|
||||
stays in the header bar (B.1.2 satisfied by construction).
|
||||
|
||||
Computed worst-case contrast over photo + scrim + drifting glow (glow fringe
|
||||
α .15 Cherenkov / core .28 Azure over the text zone; Cherenkov core is pinned
|
||||
≥80% band width by the blob geometry + drift range):
|
||||
|
||||
- Small-text zone (eyebrow/subtitle/CTA labels, ≤52% band width): white
|
||||
7.68:1 · Cherenkov subtitle 4.79:1 · eyebrow `#C7CDE9` 4.88:1 (all ≥4.5).
|
||||
- Display h1 zone (≤72% band width): white ≥5.20:1 (large text needs 3.0).
|
||||
- Mobile flat scrim: white 8.47:1 · subtitle 5.28:1 · eyebrow 5.38:1.
|
||||
|
||||
Recompute (script kept in the session scratchpad pattern: composite scrim →
|
||||
glow → WCAG luminance per pixel) if the photo, scrim stops, glow alphas or
|
||||
blob positions change.
|
||||
|
||||
## WCAG 2.1 AA notes (computed, worst-case animation frames)
|
||||
|
||||
- Nebula blob peaks over Galaxy: Cherenkov α=.38 → white 7.9:1, Cherenkov
|
||||
subtitle 4.9:1, eyebrow `#C7CDE9` 5.0:1. Azure α=.33 → all ≥ 7.7:1.
|
||||
Indigo α=.80 → all ≥ 9.6:1. Cherenkov/Azure CORES must stay in opposite
|
||||
corners (core-over-core would fail; fringe overlap ≈ 6:1).
|
||||
- On Moon: `--link` 6.1:1, `--muted` 7.1:1, `--text` 16.9:1.
|
||||
- Ambient wash worst overlap (`rgb(188,237,249)`): `--link` 5.3:1, `--muted`
|
||||
6.2:1, Galaxy h2 15.2:1.
|
||||
- Ceilings: nebula ≤ .38 Cherenkov / ≤ .35 Azure; wash ≤ .16/.12/.10 —
|
||||
recompute (WCAG relative luminance) if any alpha or hue placement changes.
|
||||
- Photo hero: see table above. Auth nebula over Galaxy: white 7.9:1,
|
||||
Cherenkov subtitle 4.9:1 at the worst blob frame; Cherenkov/Azure CORES stay
|
||||
in opposite corners. Ceilings: nebula ≤ .38 Cherenkov / ≤ .35 Azure; glow
|
||||
≤ .30/.28; wash ≤ .16/.12/.10 — recompute if any alpha or placement changes.
|
||||
- Translucent header (`color-mix` 88% Galaxy + backdrop blur, solid fallback
|
||||
via `@supports`): worst underlying content (white) → bar ≈ rgb(31,31,96);
|
||||
white text 14.8:1, muted nav `#C7CDE9` 9.4:1, Cherenkov sub-brand 9.2:1.
|
||||
- On Moon: `--link` 6.1:1, `--muted` 7.1:1, `--text` 16.9:1. Ambient wash
|
||||
worst overlap: `--link` 5.3:1, `--muted` 6.2:1.
|
||||
- Focus visible everywhere; tap targets ≥ 44px; skip link first.
|
||||
|
||||
## Article TOC pattern
|
||||
|
||||
Rendered only when ≥3 h2/h3. Two renderings of the same list (only one ever
|
||||
displayed): `<details class="toc toc--inline">` collapsed under the title
|
||||
(< 1200px) and `<nav class="toc toc--rail">` sticky right rail (≥ 1200px,
|
||||
outside the 70ch column via `.article-layout` grid). MDN/Stripe/NN-g pattern.
|
||||
displayed), both in the quiet Stripe/Apple-docs voice — muted small links,
|
||||
Galaxy on hover, no heavy box:
|
||||
|
||||
Scrollspy (Starlight's starlight-toc pattern, minimal vanilla): an
|
||||
IntersectionObserver band under the sticky header maps the visible prose block
|
||||
to its governing heading and sets `aria-current="true"` on the TOC link
|
||||
(Galaxy + 600 weight). Functional state, not motion — the "no JS motion" rule
|
||||
is untouched; the TOC works fully without JS. Same precedent covers the search
|
||||
`<mark>` term highlight (Cherenkov tint) and the `.pagination` pill nav
|
||||
(Galaxy pill = current page, 44px targets).
|
||||
- `<nav class="toc toc--rail">` (≥1200px): sticky whitespace column with a
|
||||
hairline LEFT rule, outside the 70ch column via the `.article-layout` grid;
|
||||
sticky offset 80px (56px header + 24), own scroll when long.
|
||||
- `<details class="toc toc--inline">` (<1200px): light disclosure row
|
||||
("On this page" + chevron from the `.ico` family), comfortable tap rows.
|
||||
|
||||
## 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
|
||||
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.
|
||||
## Header
|
||||
|
||||
Nav content rule: the header nav reflects end-user tasks only (NN/g —
|
||||
navigation mirrors user goals, not org tooling). The content editor is
|
||||
internal-team tooling (SPEC §3.2), so it lives as a discreet "Content editor"
|
||||
utility link in the footer, pointed to by the demo note; /admin keeps working
|
||||
as the direct URL.
|
||||
56px bar (translucent Galaxy via `color-mix` + backdrop blur where supported,
|
||||
solid Galaxy fallback — see WCAG notes). One row always: `.nav > *` is
|
||||
`flex: none`, only the spacer flexes; nothing can wrap. Tiers: ≥1025px full
|
||||
nav · ≤1024px disclosure menu (`<details>`) · ≤640px sub-brand hidden ·
|
||||
≤480px icon-only menu · ≤360px compact paddings. Logo ≥22px tall everywhere
|
||||
(≈105px wide — brand minimum 100px digital).
|
||||
|
||||
One-row invariant: `.nav > *` is `flex: none`; only the spacer and the search
|
||||
field flex, so the row can never wrap or overflow. Tier math (Inter widths
|
||||
estimated ±8%; logo 153:32 → 124px at 26px height; inner width = viewport −
|
||||
2×gutter, container caps at 1120):
|
||||
Header search — decided by research, not aesthetics: NN/g's magnifying-glass
|
||||
-icon findings (icon-only search measurably reduces discoverability and use;
|
||||
show a visible box when search is an important task) beat the icon-only
|
||||
minimal header for this content-heavy, search-required portal (SPEC §3.3.1).
|
||||
Desktop ≥1025px: a VISIBLE quiet field (hairline on navy, 140–200px flex —
|
||||
the only flexing item, so the row never wraps). ≤1024px: the 44px magnifier
|
||||
`<details>` reveals a search row under the bar (NN/g mobile-search pattern).
|
||||
Both are plain `GET /search` (full no-JS path); `<details name="header-panel">`
|
||||
makes search + menu a native exclusive pair. `/search` stays the full-results
|
||||
and deep-link target. The footer "Search" link STAYS: SPEC §3.3.1 literally
|
||||
places "a search functionality" in the bottom menu, and the footer is the
|
||||
end-of-page fallback (NN/g footers) — one quiet word, clean compliance trail.
|
||||
Sign in is a ghost/outline pill — Cherenkov fill is reserved for the page's
|
||||
primary CTA.
|
||||
|
||||
| Tier | Budget | Available |
|
||||
|---|---|---|
|
||||
| ≥1100 | brand+sub 247 + nav 415 + search 186 + Sign in 91 + gaps 16 ≈ 955 | 1044 @1100 |
|
||||
| 1025–1099 (sub-brand hidden) | ≈ 842 | 969 @1025 |
|
||||
| 481–1024 (menu + magnifier) | brand 134 + magnifier 44 + Sign in 91 + Menu 92 + gaps 16 ≈ 377 | 445 @481 |
|
||||
| ≤480 (logo 22px, icon-only menu) | ≈ 312 | 339 @375 |
|
||||
| ≤360 (compact paddings, gap 2) | ≈ 277 | 292 @320 |
|
||||
|
||||
The search field's 50px shrink range (180 → 130) absorbs estimate error in
|
||||
every tier. No focus-grow on the field: a width transition would be a layout
|
||||
animation (motion policy allows transform/opacity only) and an unanimated
|
||||
snap is worse than a calm fixed width.
|
||||
Nav content rule: the header nav reflects end-user tasks only (NN/g). The
|
||||
content editor is internal-team tooling (SPEC §3.2) → discreet "Content
|
||||
editor" utility link in the footer; /admin keeps working as the direct URL.
|
||||
|
||||
## Content & microcopy
|
||||
|
||||
- Card metadata must differentiate (NN/g: omit non-differentiating metadata):
|
||||
category and author are identical on all articles, so cards show
|
||||
date · reading time (body words ÷ 220 wpm, computed at build). Category and
|
||||
author remain on the article page; meta order there is
|
||||
category · author · date · read time.
|
||||
- Badges: "Mock — <what's missing>" (bare "Mock" only on tiles); "External —
|
||||
<team>"; "Planned — not yet available". Mock badges are never removed.
|
||||
- Terminology is fixed: "Sign in" (never Log in), "Editor", "Search", "News".
|
||||
- Card metadata must differentiate (NN/g): cards show date · reading time
|
||||
(body words ÷ 220 wpm, computed at build). Category and author remain on
|
||||
the article page as one quiet muted meta line (category · author · date ·
|
||||
read time — plain text, no chips).
|
||||
- Honesty lives at the INTERACTION POINT (REQUIREMENTS §5): whisper badges
|
||||
("Mock — <what's missing>", "Planned integration — <team>") appear where a
|
||||
mock interaction actually happens — /proposals panel, /dashboard, /login,
|
||||
/support page head. Navigation tiles carry no status chrome; planned
|
||||
services are subdued tiles (muted, no link) with "— planned" folded into
|
||||
the description. There is NO phase banner (removed as clutter — the footer
|
||||
demo note + interaction-point badges carry the disclosure).
|
||||
- Terminology fixed: "Sign in" (never Log in), "Editor", "Search", "News".
|
||||
Buttons start with a verb, sentence case. Real ellipsis (…), never "...".
|
||||
- Search links (`.suggest a`, `.search-results a`) lay out as flowing text —
|
||||
never `display: flex` on the `<a>` itself: `<mark>` term-highlight splits the
|
||||
title into fragments, and flex would turn each into its own item.
|
||||
never `display: flex` on the `<a>` (mark-splitting). Dates are plain muted
|
||||
text, not chips.
|
||||
- Overflow containment happens at the source: wide `.prose` tables scroll in
|
||||
their own box, long words/URLs wrap via `overflow-wrap`. The
|
||||
`html, body { overflow-x: clip }` rule is a regression guard, not a fix —
|
||||
never rely on it for new components.
|
||||
- Articles end with `.post-nav` (Newer ← → Older from the date-sorted
|
||||
collection). Image captions: italic line under the image
|
||||
(`.prose img + em` / `p > em:only-child`), muted and smaller.
|
||||
`html, body { overflow-x: clip }` rule is a regression guard, not a fix.
|
||||
- Articles end with `.post-nav` (Newer ← → Older). Lead image is borderless
|
||||
and breaks out of the text column ≥900px. Image captions: italic muted line
|
||||
under the image.
|
||||
|
||||
### Element rules (condensed)
|
||||
|
||||
| Element | Rule | Source |
|
||||
|---|---|---|
|
||||
| Phase banner | THE one loud demo disclosure: Moon bar above header, "Demo" tag + one sentence | GOV.UK phase banner |
|
||||
| Header nav | End-user tasks only; order News · Data · Proposals · Dashboard · Support (entry first, help last, personal area next to Sign in) | NN/g serial-position |
|
||||
| Header search | Compact field ≥1025px, magnifier reveal ≤1024px; both plain GET /search | NN/g site search |
|
||||
| Hero (home) | Headline + brand subtitle + two task CTAs (Cherenkov primary, ghost secondary) + one-line Latest teaser. Never a second search box; no fake live widgets | hero-CTA convention, one-search-location |
|
||||
| Badges | Quiet hairline pill, muted sentence-case text, Galaxy dot on mocks; "Mock — reason"; never removed | REQUIREMENTS §5, NN/g hierarchy |
|
||||
| Cards | Cover · title (clamp 3) · desc (clamp 2) · date · read time; no non-differentiating metadata | NN/g metadata |
|
||||
| Pagination | Newer/Older + windowed numbers, 44px targets, aria-current | NN/g pagination |
|
||||
| Article | Back "← All news"; h1 → standfirst (description) → meta (category · author · date · read time) → cover → inline TOC → prose; italic muted captions; TOC at ≥3 headings; post-nav | Guardian/BBC anatomy |
|
||||
| Header nav | End-user tasks only; order News · Data · Proposals · Dashboard · Support | NN/g serial-position |
|
||||
| Header search | Icon-reveal row, all widths; plain GET /search | NN/g site search |
|
||||
| Hero (home) | Photo (LST under night sky) + Galaxy scrim + eyebrow + headline + Cherenkov subtitle + CTA pair (Cherenkov primary, ghost secondary). Nothing else — no teaser line, no widgets, no second search | Apple hero restraint |
|
||||
| Badges | Quiet hairline pill, muted sentence case, Galaxy dot on mocks; ONLY at mock interaction points | REQUIREMENTS §5 |
|
||||
| Cards | Cover · title (clamp 3) · desc (clamp 2) · date · read time; borderless on Moon; hover = elevate only | NN/g metadata |
|
||||
| Pagination | Newer/Older + windowed numbers, 44px targets, aria-current, Moon hover fill | NN/g pagination |
|
||||
| Article | Back "← All news"; h1 → standfirst → muted meta line → borderless breakout cover → inline TOC → prose → post-nav | Guardian/BBC anatomy |
|
||||
| Forms | Visible labels above fields; placeholders are examples only; buttons start with a verb | GOV.UK forms, NN/g |
|
||||
| Login | One line + button + one small-print line; no reassurance prose | auth brevity |
|
||||
| Footer | Identity line → Contact · Search · Disclaimer · Privacy · Site settings · Content editor → © line; no implementation talk in UI copy | NN/g footers |
|
||||
| Search results | Links lay out as flowing text (mark-safe); date chip; "Press Enter for all results" hint | — |
|
||||
| Login | One line + Cherenkov primary CTA + one small-print line; whisper badge top-right of the card (in flow ≤480px); no reassurance prose | auth brevity |
|
||||
| Footer | Identity line → Contact · Search · Disclaimer · Privacy · Site settings · Content editor → © line | NN/g footers, SPEC §3.3.1 |
|
||||
| Search results | Links lay out as flowing text (mark-safe); plain muted date; "Press Enter for all results" hint | — |
|
||||
|
||||
## Page composition (archetypes)
|
||||
|
||||
Every page follows one of three archetypes; a new page picks one instead of
|
||||
inventing a section order.
|
||||
inventing a section order. The navy band is reserved for `/` and `/login`.
|
||||
|
||||
| Archetype | Pages | Order |
|
||||
|---|---|---|
|
||||
| Portal landing | `/` | Hero (h1 + subtitle + CTA pair + one-line Latest teaser) → Services tiles (Moon) → Latest news 3-up + "All news" (white) |
|
||||
| Section landing | `/news`, `/search`, `/proposals`, `/dashboard`, `/support` | Galaxy hero band carrying the h1 + one-line subtitle → single content band (Moon = utility, white = editorial listing) → footer |
|
||||
| Portal landing | `/` | Photo hero (h1 + subtitle + CTA pair) → Services tiles (Moon) → Latest news 3-up + "All news" (white) |
|
||||
| Section landing | `/news`, `/search`, `/proposals`, `/dashboard`, `/support` | Landing head (`.page-head`: display Galaxy h1 + standfirst, `--space-band` top padding, no eyebrow, no navy band) → content → footer. The WHOLE page sits on its archetype surface: white (editorial /news) or Moon (utility) — head included, one surface per page |
|
||||
| Document | `/news/<slug>`, `/pages/<slug>` | Back link → h1 → standfirst → meta → cover → inline TOC → prose (→ post-nav on articles); no hero band — the document IS the page |
|
||||
|
||||
- Home is task-first (NN/g homepage principles: clear starting points for the
|
||||
top 1–4 tasks — for scientists that's proposals/data/support, not reading
|
||||
news). The hero teaser is the freshness signal (deep-links the newest
|
||||
article); the 3-card section below is the browse layer — different
|
||||
granularity, not redundant.
|
||||
- Document order is press anatomy (Guardian/BBC/Reuters): headline →
|
||||
standfirst → byline/meta → lead image → body. The inline TOC sits between
|
||||
lead image and body (Wikipedia/GOV.UK contents position) so the editorial
|
||||
head stays unbroken and the list is adjacent to what it indexes.
|
||||
- Back affordances: articles "← All news", static pages "← Home" (footer- and
|
||||
deep-link-reached documents must not be dead ends). Auth (`/login`) is the
|
||||
sole exception to the archetypes: one focused card, no hero, no sections.
|
||||
- Service tiles carry a stroke glyph (`.ico` family: currentColor, round caps,
|
||||
aria-hidden; 26px Galaxy) ABOVE the label — never instead of it (NN/g icon
|
||||
usability: icons are ambiguous alone; beside an always-visible label they
|
||||
differentiate otherwise-uniform tiles and speed grid scanning). Glyphs are
|
||||
wayfinding, not decoration — they don't count against the one-accent budget.
|
||||
- Home is task-first (NN/g): Services directly under the hero; the news
|
||||
section below is the browse layer.
|
||||
- Document order is press anatomy (Guardian/BBC/Reuters). The inline TOC sits
|
||||
between lead image and body.
|
||||
- Back affordances: articles "← All news", static pages "← Home". Auth
|
||||
(`/login`) is the sole exception to the archetypes: one focused card on the
|
||||
nebula band, no hero, no sections.
|
||||
- Service tiles carry a stroke glyph (`.ico` family: currentColor, round
|
||||
caps, aria-hidden; 26px Galaxy) ABOVE the label — never instead of it.
|
||||
Glyphs are wayfinding, not decoration.
|
||||
|
||||
## Do not
|
||||
|
||||
- No new hues, tints, or grays — derive via `color-mix` from brand tokens only.
|
||||
- No Space Grotesk outside h1/h2. No dark theme.
|
||||
- No ad-hoc spacing/shadow/radius values; extend the token tables instead.
|
||||
- No animation/transition outside the `no-preference` block; no JS motion.
|
||||
- No dependencies/frameworks (CSS or JS). No removal of "Mock" badges.
|
||||
- Don't lighten text over navy bands below the alphas listed above.
|
||||
- No Space Grotesk outside h1/h2; headline weight stays 500 (brand D.3.4).
|
||||
- No font sizes outside the type-scale tokens; no ad-hoc spacing/shadow/radius.
|
||||
- No animation/transition outside the `no-preference` block; no JS motion;
|
||||
no transition durations/easings outside `--dur`/`--ease`.
|
||||
- No dependencies/frameworks (CSS or JS). No removal of mock disclosure at
|
||||
interaction points. No phase banner — do not reintroduce it.
|
||||
- No Cherenkov outside: primary CTA, flash, hero subtitle, logo cluster
|
||||
(+ the two functional states listed under Color tokens). No hover borders.
|
||||
- Don't lighten text over navy/photo bands below the computed ceilings above.
|
||||
- No clip-path/CSS approximations of brand assets — the flash is the real SVG
|
||||
path from the official logo, or nothing.
|
||||
- Decoration budget: max ONE accent element per view (currently: footer flash,
|
||||
static). No twinkles, floating shapes, or stacked decorations.
|
||||
- Decoration budget: max ONE accent element per view (currently: footer
|
||||
flash, static). No twinkles, floating shapes, or stacked decorations.
|
||||
- Never link `public/uploads` from layout/brand surfaces — copy to
|
||||
`public/brand/` instead (uploads is content-synced).
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 353 KiB |
+11
-10
@@ -62,9 +62,11 @@ const links = [
|
||||
))}
|
||||
</nav>
|
||||
<span class="spacer"></span>
|
||||
{/* Site search lives in the header (desktop: compact field; ≤1024px: the
|
||||
magnifier <details> below). Plain GET form — works without JS; with JS
|
||||
the shared suggest dropdown appears (search-client.js). */}
|
||||
{/* Site search. Desktop ≥1025px: a VISIBLE quiet field — NN/g's
|
||||
magnifying-glass-icon research: hiding search behind an icon
|
||||
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">
|
||||
<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>
|
||||
@@ -72,10 +74,9 @@ const links = [
|
||||
<ul class="suggest" id="hdr-suggest"></ul>
|
||||
<p class="sr-only" id="hdr-suggest-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
<a class="login-mock" href="/login">Sign in</a>
|
||||
{/* ≤1024px: magnifier button reveals a full-width search row. name="header-panel"
|
||||
pairs it with the menu as a native exclusive group — opening one closes
|
||||
the other, no JS (exclusive-accordion behavior of <details name>). */}
|
||||
{/* ≤1024px: the magnifier reveals a search row under the header (NN/g
|
||||
mobile-search pattern). name="header-panel" pairs it with the menu as
|
||||
a native exclusive group — opening one closes the other, no JS. */}
|
||||
<details class="search-pop" name="header-panel">
|
||||
<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>
|
||||
@@ -88,6 +89,7 @@ const links = [
|
||||
<p class="sr-only" id="pop-suggest-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
</details>
|
||||
<a class="login-mock" href="/login">Sign in</a>
|
||||
{/* Mobile disclosure menu — native <details>/<summary>, no JS; desktop hides it via CSS */}
|
||||
<details class="menu" name="header-panel">
|
||||
<summary aria-label="Menu">
|
||||
@@ -106,9 +108,8 @@ const links = [
|
||||
</header>
|
||||
<script is:inline src="/search-client.js"></script>
|
||||
<script is:inline>
|
||||
// Header search suggestions (desktop field + mobile reveal); the forms are
|
||||
// plain GET /search, so everything below is progressive enhancement.
|
||||
newsSearch({ input: 'hdr-q', list: 'hdr-suggest', status: 'hdr-suggest-status', limit: 6, unit: 'suggestion' });
|
||||
// Header search suggestions (icon-reveal row); the form is a plain
|
||||
// GET /search, so everything below is progressive enhancement.
|
||||
newsSearch({ input: 'pop-q', list: 'pop-suggest', status: 'pop-suggest-status', limit: 6, unit: 'suggestion' });
|
||||
var pop = document.querySelector('.search-pop');
|
||||
pop.addEventListener('toggle', function () {
|
||||
|
||||
@@ -19,16 +19,11 @@ const tickets = [
|
||||
];
|
||||
---
|
||||
<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">
|
||||
<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">
|
||||
<span class="badge-mock">Mock — sample data, no live services connected</span>
|
||||
<div class="dash-grid">
|
||||
|
||||
+15
-14
@@ -10,27 +10,34 @@ const iso = (d) => d.toISOString().slice(0, 10);
|
||||
// Reading time from the Markdown body (~220 wpm) — differentiating card metadata;
|
||||
// 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));
|
||||
// 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 —
|
||||
// currentColor, round caps), aria-hidden. NN/g icon research: icons alone are
|
||||
// ambiguous, but NEXT TO an always-visible label they differentiate otherwise
|
||||
// uniform tiles and speed up grid scanning.
|
||||
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"/>' },
|
||||
{ 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"/>' },
|
||||
{ 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"/>' },
|
||||
{ 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"/>' },
|
||||
{ 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"/>' },
|
||||
{ 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"/>' },
|
||||
];
|
||||
---
|
||||
<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">
|
||||
<div class="band-bg" aria-hidden="true"></div>
|
||||
<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 btn--ghost" href="/proposals">Submit a proposal</a>
|
||||
</div>
|
||||
{posts[0] && (
|
||||
<p class="hero-teaser">Latest: <a href={`/news/${posts[0].id}`}>{posts[0].data.title}</a></p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -60,7 +64,7 @@ const services = [
|
||||
<div class="section-head"><h2>Services</h2></div>
|
||||
<div class="grid">
|
||||
{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} />
|
||||
<h3>{s.href
|
||||
? (s.ext
|
||||
@@ -68,9 +72,6 @@ const services = [
|
||||
: <a href={s.href}>{s.title}</a>)
|
||||
: s.title}</h3>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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}`}
|
||||
description={first ? 'News and announcements of the CTAO' : `News and announcements of the CTAO — page ${page.currentPage} of ${page.lastPage}`}
|
||||
>
|
||||
<section class="hero-band">
|
||||
<div class="band-bg" aria-hidden="true"></div>
|
||||
<div class="container hero">
|
||||
<div class="eyebrow">Cherenkov Telescope Array Observatory</div>
|
||||
{/* Section-landing archetype: white head, display Galaxy h1 + standfirst
|
||||
(the navy band is reserved for / and /login — DESIGN.md). */}
|
||||
<header class="container page-head">
|
||||
<h1>News & Announcements</h1>
|
||||
<p>Science highlights and updates from the observatory and its partners</p>
|
||||
</div>
|
||||
</section>
|
||||
<p class="standfirst">Science highlights and updates from the observatory and its partners.</p>
|
||||
</header>
|
||||
|
||||
<section class="container page">
|
||||
<h2 class="sr-only">{first ? 'All news' : `All news — page ${page.currentPage}`}</h2>
|
||||
|
||||
@@ -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. */}
|
||||
<h1>{post.data.title}</h1>
|
||||
<p class="standfirst">{post.data.description}</p>
|
||||
{/* Meta order: 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>
|
||||
{/* 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>
|
||||
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" />}
|
||||
{/* TOC sits between lead image and body (Wikipedia/GOV.UK contents
|
||||
position): the press head above stays unbroken, and the list is
|
||||
adjacent to the content it indexes. */}
|
||||
{toc.length >= 3 && (
|
||||
<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>
|
||||
{toc.map((h) => (
|
||||
<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;
|
||||
const observe = () => {
|
||||
observer?.disconnect();
|
||||
// Narrow band below the sticky header (64px) — Starlight's rootMargin trick
|
||||
const top = 76, band = 64;
|
||||
// Narrow band below the sticky header (56px) — Starlight's rootMargin trick
|
||||
const top = 68, band = 64;
|
||||
observer = new IntersectionObserver(onIntersect, {
|
||||
rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`,
|
||||
});
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<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">
|
||||
<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">
|
||||
<span class="badge-mock">Mock — no submission logic</span>
|
||||
<span class="badge-ext">Planned integration: Proposal Handling System (APC team)</span>
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="CTAO Science Portal — Search" description="Search CTAO news and announcements">
|
||||
<section class="hero-band">
|
||||
<div class="band-bg" aria-hidden="true"></div>
|
||||
<div class="container hero">
|
||||
<div class="eyebrow">Science Portal</div>
|
||||
<h1>Search</h1>
|
||||
<p>Find news and announcements</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Utility archetype: ONE surface per page — Moon canvas from the landing
|
||||
head down (borderless cards/panels pop against it); editorial pages stay
|
||||
all-white. No mid-page surface seams (DESIGN.md). */}
|
||||
<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">
|
||||
<form class="search-form" role="search" action="/search" method="get">
|
||||
<label class="sr-only" for="q">Search news</label>
|
||||
|
||||
+6
-10
@@ -9,24 +9,20 @@ const channels = [
|
||||
];
|
||||
---
|
||||
<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">
|
||||
<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">
|
||||
{/* 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>
|
||||
<div class="grid">
|
||||
{channels.map((c) => (
|
||||
<article class="card tile">
|
||||
<h3>{c.title}</h3>
|
||||
<p class="desc">{c.desc}</p>
|
||||
<span class="badge-mock">Mock</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+270
-212
@@ -1,7 +1,8 @@
|
||||
/* ==========================================================================
|
||||
CTAO Science Portal — design tokens per the official CTAO Brand Guidelines
|
||||
(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.
|
||||
(April 2024): colors D.2, typography D.3 (Inter body / Space Grotesk display
|
||||
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).
|
||||
System rules (spacing / elevation / surfaces / motion): see DESIGN.md.
|
||||
========================================================================== */
|
||||
@@ -9,9 +10,9 @@
|
||||
color-scheme: light;
|
||||
/* Brand palette — hex values straight from the brand book */
|
||||
--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 */
|
||||
--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 */
|
||||
/* Derived, WCAG-adjusted */
|
||||
--text: #101228; /* near-Galaxy body ink */
|
||||
@@ -19,35 +20,58 @@
|
||||
--border: #e2e5ee;
|
||||
--link: #0057c2; /* Cosmic Azure darkened to pass AA (4.5:1) for body links */
|
||||
--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 */
|
||||
--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 */
|
||||
--font-body: "Inter", "Inter Fallback", Arial, system-ui, sans-serif; /* BRAND D.3.1 */
|
||||
--font-display: "Space Grotesk", var(--font-body); /* BRAND D.3.2 — headlines only */
|
||||
/* 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;
|
||||
--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 */
|
||||
--radius: 16px;
|
||||
--radius-pill: 999px;
|
||||
--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. */
|
||||
--gutter: clamp(18px, 4vw, 28px); /* page side padding */
|
||||
--space-s: 12px;
|
||||
--space-m: 20px;
|
||||
--space-m: 24px;
|
||||
--space-l: clamp(24px, 2vw + 16px, 40px);
|
||||
--space-xl: clamp(40px, 3vw + 24px, 72px); /* section rhythm */
|
||||
--space-band: clamp(48px, 5vw + 28px, 96px); /* hero / auth band padding */
|
||||
/* Elevation — navy-tinted; the ONLY two shadows allowed in this codebase */
|
||||
--shadow-ambient: 0 1px 2px rgba(0, 0, 74, 0.05), 0 4px 14px rgba(0, 0, 74, 0.07);
|
||||
--shadow-lifted: 0 4px 10px rgba(0, 0, 74, 0.08), 0 18px 44px rgba(0, 0, 74, 0.18);
|
||||
/* Nebula glow (BRAND D.5) — one stack for hero + auth bands, static AND animated.
|
||||
Alphas are contrast ceilings (DESIGN.md): Cherenkov ≤.38, Azure ≤.35 keep the
|
||||
Cherenkov subtitle/eyebrow ≥4.5:1 even with a blob core right under the text.
|
||||
Cherenkov and Azure cores sit in opposite corners — cores never meet. */
|
||||
--space-xl: clamp(56px, 7vw, 120px); /* section rhythm */
|
||||
--space-band: clamp(72px, 10vw, 160px); /* hero / auth / landing-head padding */
|
||||
/* Elevation — navy-tinted, soft/large/low-alpha; the ONLY two shadows allowed */
|
||||
--shadow-ambient: 0 1px 2px rgba(0, 0, 74, 0.04), 0 8px 24px rgba(0, 0, 74, 0.06);
|
||||
--shadow-lifted: 0 2px 6px rgba(0, 0, 74, 0.05), 0 24px 48px rgba(0, 0, 74, 0.14);
|
||||
/* Motion — ONE hover language everywhere (ctao.org: quiet .25s fades) */
|
||||
--dur: 0.25s;
|
||||
--ease: ease-out;
|
||||
/* 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:
|
||||
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(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 */
|
||||
@@ -73,7 +97,7 @@
|
||||
}
|
||||
|
||||
* { 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
|
||||
level (tables scroll in their box, long words wrap); clip only catches
|
||||
regressions so one offender can never remove the page gutters. `clip`, unlike
|
||||
@@ -84,11 +108,14 @@ body {
|
||||
font-family: var(--font-body);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
line-height: 1.65;
|
||||
line-height: 1.5;
|
||||
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; }
|
||||
/* Overlong unbroken words (slugs, codes) may break rather than overflow the page */
|
||||
h1, h2, h3 { overflow-wrap: break-word; }
|
||||
@@ -97,6 +124,10 @@ h1, h2, h3 { overflow-wrap: break-word; }
|
||||
/* Inline SVG icons — stroke inherits currentColor */
|
||||
.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:hover { color: var(--indigo); }
|
||||
/* 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); }
|
||||
/* 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); }
|
||||
|
||||
/* 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; }
|
||||
|
||||
|
||||
/* 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; }
|
||||
.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; }
|
||||
/* One-row guarantee: only the search field and the spacer flex — everything
|
||||
else keeps its natural size (links are single words, they can never wrap) */
|
||||
/* One-row guarantee: everything keeps its natural size except the spacer
|
||||
(links are single words, they can never wrap) */
|
||||
.nav > * { flex: none; }
|
||||
/* Header tiers (one row, always — width math in DESIGN.md):
|
||||
≥1100px full nav + inline search + sub-brand · 1025–1099px sub-brand hidden ·
|
||||
≤1024px disclosure menu + magnifier reveal (both native <details>, CSS-only) ·
|
||||
≤480px icon-only menu button, smaller logo · ≤360px compact paddings */
|
||||
@media (max-width: 1099px) { .brand .brand-sub { display: none; } }
|
||||
/* Header tiers, one row always: ≥1025px full nav · ≤1024px disclosure menu ·
|
||||
≤640px sub-brand hidden · ≤480px icon-only menu, smaller logo · ≤360px compact.
|
||||
Search is the same icon-reveal at every width (one pattern, no width math). */
|
||||
.menu { display: none; }
|
||||
.menu .i-close { display: none; }
|
||||
.menu[open] .i-menu { display: none; }
|
||||
.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 .brand-sub {
|
||||
color: var(--cherenkov); font-size: 0.78rem; font-weight: 500;
|
||||
border-left: 1px solid rgba(255,255,255,0.25); padding-left: 10px;
|
||||
color: var(--cherenkov); font-size: var(--fs-xs); font-weight: 500;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.25); padding-left: 10px;
|
||||
}
|
||||
.nav .spacer { flex: 1; }
|
||||
.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;
|
||||
}
|
||||
.nav a.navlink:hover { color: #fff; }
|
||||
.nav a.navlink[aria-current="page"] { color: #fff; border-bottom-color: var(--cherenkov); }
|
||||
/* AAI sign-in entry — leads to the /login mock page */
|
||||
.nav a.navlink[aria-current="page"] { color: #fff; border-bottom-color: #fff; }
|
||||
/* AAI sign-in entry — ghost/outline: Cherenkov fill is reserved for the page's
|
||||
primary CTA, the header only whispers. */
|
||||
.nav .login-mock {
|
||||
color: var(--galaxy); background: var(--cherenkov); font: inherit; font-weight: 600;
|
||||
border: 0; border-radius: var(--radius-pill); padding: 8px 18px; margin-left: 6px;
|
||||
text-decoration: none; white-space: nowrap;
|
||||
color: #fff; background: transparent; font: inherit; font-weight: 600; font-size: var(--fs-s);
|
||||
border: 1px solid rgba(255, 255, 255, 0.55); border-radius: var(--radius-pill);
|
||||
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 */
|
||||
.nav a.navlink, .nav .login-mock {
|
||||
display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
|
||||
}
|
||||
/* Header search (desktop) — compact pill in the utility cluster; a plain GET
|
||||
form to /search (works without JS), suggestions reuse the shared .suggest
|
||||
dropdown. Flexes 130–180px so the row can never overflow (width math: DESIGN.md). */
|
||||
.nav-search { position: relative; display: flex; align-items: center; flex: 0 1 180px; min-width: 130px; margin-left: 6px; }
|
||||
.nav-search .search-ico { left: 12px; width: 15px; height: 15px; }
|
||||
/* Site search. Desktop ≥1025px: VISIBLE quiet field — NN/g magnifying-glass-
|
||||
icon research: icon-only search reduces discoverability/usage; a visible
|
||||
input is recommended when search is an important task (content-heavy portal,
|
||||
spec-required search). Chrome stays whisper-quiet: hairline on navy.
|
||||
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 {
|
||||
width: 100%; min-width: 0; font: inherit; font-size: 0.9rem; color: var(--text);
|
||||
background: #fff; border: 1px solid transparent; border-radius: var(--radius-pill);
|
||||
padding: 8px 14px 8px 34px;
|
||||
width: 100%; min-width: 0; font: inherit; font-size: var(--fs-s); color: #fff;
|
||||
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 */
|
||||
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); }
|
||||
/* ≤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 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) {
|
||||
.nav > nav { display: none; }
|
||||
.menu { display: block; }
|
||||
.menu summary {
|
||||
display: inline-flex; align-items: center; gap: 8px; min-height: 44px;
|
||||
padding: 0 10px; margin-left: 6px; color: #fff; font-weight: 500;
|
||||
cursor: pointer; list-style: none; white-space: nowrap;
|
||||
padding: 0 10px; margin-left: 6px; color: #fff; font-weight: 500; white-space: nowrap;
|
||||
}
|
||||
.menu summary::-webkit-details-marker { display: none; }
|
||||
.menu nav {
|
||||
position: absolute; left: 0; right: 0; top: 100%;
|
||||
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);
|
||||
}
|
||||
.menu nav a:last-child { border-bottom: 0; }
|
||||
.menu nav a[aria-current="page"] { color: var(--cherenkov); }
|
||||
/* 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); }
|
||||
.menu nav a[aria-current="page"] { font-weight: 700; }
|
||||
}
|
||||
@media (max-width: 640px) { .brand .brand-sub { display: none; } }
|
||||
@media (max-width: 480px) {
|
||||
.brand img { height: 22px; }
|
||||
.menu summary .menu-label { display: none; }
|
||||
@@ -237,8 +276,8 @@ img { max-width: 100%; height: auto; display: block; }
|
||||
@media (max-width: 360px) {
|
||||
.nav { padding: 0 14px; gap: 2px; }
|
||||
.brand { margin-right: 4px; }
|
||||
.brand img { height: 20px; }
|
||||
.nav .login-mock { padding: 8px 12px; }
|
||||
/* logo stays 22px — 22px tall ≈ 105px wide, brand minimum is 100px digital */
|
||||
.nav .login-mock { padding: 7px 12px; }
|
||||
}
|
||||
/* External-link marker — SVG arrow via mask, inherits currentColor */
|
||||
.external::after {
|
||||
@@ -248,34 +287,37 @@ img { max-width: 100%; height: auto; display: block; }
|
||||
mask: var(--ico-ext) center / contain no-repeat;
|
||||
}
|
||||
|
||||
/* Hero — navy band with a nebula glow (BRAND D.5). The static gradient below is
|
||||
the reduced-motion fallback; with motion allowed, the glow moves to an animated
|
||||
pseudo-layer (see the motion block at the end of this file). */
|
||||
/* Hero (home only) — image-led: LST-1 under the night sky, full-bleed beneath a
|
||||
Galaxy scrim (--hero-scrim; worst-case AA computed in DESIGN.md). With motion
|
||||
allowed a Cherenkov/Azure glow layer drifts above the photo (see motion block). */
|
||||
.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;
|
||||
/* 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; }
|
||||
.hero { position: relative; } /* content above the animated nebula layer */
|
||||
.hero h1 { font-size: clamp(2rem, 6vw, 3.6rem); margin: 0 0 10px; line-height: 1.12; font-weight: 700; }
|
||||
.hero p { color: var(--cherenkov); font-size: 1.08rem; max-width: 62ch; margin: 0; } /* subtitle per BRAND D.3.4 */
|
||||
.hero { position: relative; } /* content above the animated glow layer */
|
||||
.hero h1 { font-size: var(--fs-display); margin: 0 0 12px; }
|
||||
.hero p { color: var(--cherenkov); font-size: var(--fs-l); max-width: 62ch; margin: 0; } /* subtitle per BRAND D.3.4 */
|
||||
.eyebrow {
|
||||
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,
|
||||
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; }
|
||||
.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; }
|
||||
/* 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; }
|
||||
.hero .hero-teaser a { color: #fff; text-underline-offset: 3px; }
|
||||
.hero .hero-teaser a:hover { color: var(--cherenkov); }
|
||||
.btn--ghost:hover { background: rgba(255, 255, 255, 0.12); color: #fff; border-color: #fff; }
|
||||
|
||||
/* Section-landing head (news/search/proposals/dashboard/support) — white page,
|
||||
display-scale Galaxy h1 + standfirst; the navy band is reserved for / and /login. */
|
||||
.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-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;
|
||||
}
|
||||
.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 {
|
||||
position: absolute; top: calc(100% + 8px); left: 0; right: 0;
|
||||
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. */
|
||||
.suggest a {
|
||||
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;
|
||||
}
|
||||
.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 {
|
||||
content: "Press Enter for all results"; display: block; margin-top: 4px;
|
||||
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) */
|
||||
@@ -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);
|
||||
}
|
||||
.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: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; }
|
||||
}
|
||||
|
||||
/* News cards — image + category + clamped title/description + <time> meta;
|
||||
whole card clickable via stretched link (research: NN/g & card-UI guides 2025). */
|
||||
/* News cards — image + clamped title/description + <time> meta; whole card
|
||||
clickable via stretched link. Chrome is single-signal: hairline on white,
|
||||
borderless on Moon (white-on-moon separates); hover = one elevate gesture. */
|
||||
.card {
|
||||
position: relative; background: #fff; border: 1px solid var(--border);
|
||||
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) */
|
||||
.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 .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 {
|
||||
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;
|
||||
}
|
||||
.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:hover { color: var(--link); }
|
||||
.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;
|
||||
}
|
||||
.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) */
|
||||
@container (min-width: 640px) {
|
||||
.card--featured { grid-column: 1 / -1; display: grid; grid-template-columns: 3fr 2fr; }
|
||||
.card--featured .cover { height: 100%; aspect-ratio: auto; }
|
||||
.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; }
|
||||
}
|
||||
|
||||
/* Pagination (news archive) — prev/next + windowed page numbers; ≥44px targets.
|
||||
Pattern mined from AstroPaper/Astro blog templates, numbers added for 9 pages. */
|
||||
/* Pagination (news archive) — prev/next + windowed page numbers; ≥44px targets. */
|
||||
.pagination {
|
||||
display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
|
||||
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;
|
||||
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-step { font-weight: 600; }
|
||||
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); }
|
||||
|
||||
/* 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 .ico { width: 26px; height: 26px; stroke-width: 1.75; stroke-linejoin: round; color: var(--galaxy); }
|
||||
.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-layout { display: grid; grid-template-columns: minmax(0, 70ch); }
|
||||
.article { padding: var(--space-l) 0 var(--space-xl); max-width: 70ch; }
|
||||
.article .back { color: var(--muted); font-size: 0.9rem; }
|
||||
.article h1 {
|
||||
color: var(--galaxy); font-size: clamp(2rem, 5vw, 3rem);
|
||||
margin: 14px 0 10px; line-height: 1.12;
|
||||
}
|
||||
.article .back { color: var(--muted); font-size: var(--fs-s); }
|
||||
.article h1 { color: var(--galaxy); font-size: var(--fs-h1); margin: 14px 0 10px; }
|
||||
.article .meta {
|
||||
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
|
||||
(Guardian/BBC anatomy). Replaces the old first-prose-paragraph lead styling
|
||||
so an article never shows two competing leads. */
|
||||
.article .standfirst { margin: 0 0 var(--space-s); font-size: 1.12rem; line-height: 1.55; color: var(--muted); }
|
||||
/* Article TOC (MDN/Stripe pattern, rendered only when ≥3 h2/h3): sticky right rail
|
||||
on wide screens, collapsed <details> under the title otherwise. */
|
||||
.toc {
|
||||
background: var(--moon); border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: var(--space-s) var(--space-m); font-size: 0.92rem;
|
||||
}
|
||||
(Guardian/BBC anatomy); also the intro line under section-landing h1s. */
|
||||
.standfirst { margin: 0 0 var(--space-s); font-size: var(--fs-l); line-height: 1.55; color: var(--muted); }
|
||||
/* Article TOC (Stripe/Apple-docs pattern, rendered only when ≥3 h2/h3):
|
||||
quiet "On this page" — sticky hairline-rule rail on wide screens, a light
|
||||
disclosure row otherwise. Links are muted, Galaxy on hover; the scrollspy
|
||||
active state (Galaxy text + Cherenkov rule segment) is navigation state,
|
||||
not decoration — it sits outside the one-accent budget. */
|
||||
.toc { font-size: var(--fs-s); }
|
||||
.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);
|
||||
}
|
||||
.toc strong { display: block; margin-bottom: 4px; }
|
||||
.toc summary { cursor: pointer; padding: 6px 0; }
|
||||
.toc strong { display: block; margin-bottom: 10px; }
|
||||
.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 a { text-decoration: none; display: inline-flex; align-items: center; min-height: 24px; }
|
||||
.toc a:hover { text-decoration: underline; }
|
||||
.toc a {
|
||||
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 */
|
||||
.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; }
|
||||
@media (min-width: 1200px) {
|
||||
.article-layout { grid-template-columns: minmax(0, 70ch) 15rem; column-gap: var(--space-xl); justify-content: space-between; }
|
||||
.toc--inline { display: none; }
|
||||
.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 */
|
||||
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 */
|
||||
.prose { overflow-wrap: break-word; }
|
||||
.prose { line-height: 1.65; overflow-wrap: break-word; }
|
||||
.prose a { overflow-wrap: anywhere; }
|
||||
.prose h2 { color: var(--galaxy); margin: 34px 0 10px; font-size: clamp(1.25rem, 2.6vw, 1.45rem); }
|
||||
.prose h3 { color: var(--galaxy); margin: 26px 0 8px; font-size: 1.15rem; }
|
||||
.prose h2 { color: var(--galaxy); margin: 44px 0 12px; font-size: var(--fs-xl); }
|
||||
.prose h3 { color: var(--galaxy); margin: 30px 0 8px; font-size: var(--fs-l); }
|
||||
.prose p { margin: 0 0 16px; }
|
||||
.prose ul, .prose ol { margin: 0 0 16px; padding-left: 22px; }
|
||||
.prose li { margin: 6px 0; }
|
||||
.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);
|
||||
}
|
||||
.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; }
|
||||
/* 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, .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 */
|
||||
.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-next { grid-column: 2; text-align: right; }
|
||||
.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;
|
||||
}
|
||||
.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; }
|
||||
.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 {
|
||||
background: #fff; border: 1px solid var(--border); border-radius: calc(var(--radius) - 4px);
|
||||
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;
|
||||
}
|
||||
.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 li {
|
||||
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);
|
||||
}
|
||||
.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) */
|
||||
.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; }
|
||||
/* Date chips on results and suggestions */
|
||||
.search-results small, .suggest small {
|
||||
color: var(--muted); white-space: nowrap; font-size: 0.78rem;
|
||||
background: var(--moon); border-radius: var(--radius-pill); padding: 2px 10px;
|
||||
}
|
||||
.search-results small { margin-top: 3px; } /* optical align with the first title line */
|
||||
/* Dates on results and suggestions — plain muted text, no chip chrome */
|
||||
.search-results small, .suggest small { color: var(--muted); white-space: nowrap; font-size: var(--fs-xs); }
|
||||
.search-results small { margin-top: 5px; } /* optical align with the first title line */
|
||||
/* 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; }
|
||||
|
||||
/* Mock/status badges — every mock stays labelled (REQUIREMENTS.md §5), but the
|
||||
Demo phase banner is loud once, so per-element badges only whisper: quiet
|
||||
hairline pill, muted text (7.1:1 on Moon), sentence case, Galaxy dot on mocks. */
|
||||
/* Mock/status badges — whisper-quiet hairline pill, ONLY where a mock
|
||||
interaction actually happens (/proposals, /dashboard, /login, /support head);
|
||||
REQUIREMENTS §5 honesty lives at the interaction point, not on every tile. */
|
||||
.badge-mock, .badge-ext {
|
||||
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);
|
||||
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 */
|
||||
.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); } }
|
||||
.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 li {
|
||||
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; }
|
||||
.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-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 {
|
||||
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);
|
||||
}
|
||||
.pill--ok { color: var(--galaxy); background: var(--tint-cherenkov); }
|
||||
.pill--review { color: var(--galaxy); background: var(--moon); }
|
||||
.pill--ok { color: var(--galaxy); background: var(--moon); }
|
||||
.pill--review { color: var(--text); 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 {
|
||||
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);
|
||||
background: var(--nebula), var(--galaxy);
|
||||
}
|
||||
.auth-card {
|
||||
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);
|
||||
}
|
||||
.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; }
|
||||
.auth-card .muted { color: var(--muted); font-size: 0.95rem; }
|
||||
/* Whisper badge sits top-right, off the reading flow (back in flow ≤480px
|
||||
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); }
|
||||
|
||||
/* 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. */
|
||||
.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); }
|
||||
.foot-top { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-s) var(--space-m); }
|
||||
.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-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,
|
||||
opt-in per page via <Base ambient> (home only). Clearly visible pastel fields
|
||||
(alphas .16/.12/.10 — ceilings: links on the worst blob overlap stay ≥ 5.3:1);
|
||||
all reading surfaces (cards, panels, bands) are solid and cover it. Static
|
||||
when motion is reduced. */
|
||||
/* Whole-page ambient — a fixed brand-hue wash behind everything, opt-in per
|
||||
page via <Base ambient> (home only). Clearly visible pastel fields (alphas
|
||||
.16/.12/.10 — ceilings: links on the worst blob overlap stay ≥ 5.3:1); all
|
||||
reading surfaces (cards, panels, bands) are solid and cover it. Static when
|
||||
motion is reduced. */
|
||||
.has-ambient::before {
|
||||
content: ""; position: fixed; inset: -30%; z-index: -1; pointer-events: none;
|
||||
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
|
||||
reduce block above is a second safety net). Nebula translation of ctao.org:
|
||||
they drift looping videos under Galaxy-Blue vignettes; here an oversized
|
||||
gradient layer drifts via transform only (compositor-friendly, no repaints).
|
||||
Worst-case blob-over-text contrast verified ≥4.5:1 (see DESIGN.md).
|
||||
reduce block above is a second safety net). One hover language: every
|
||||
interactive element fades color/background/border/shadow on the SAME two
|
||||
tokens (--dur/--ease — ctao.org's quiet-fade feel); cards elevate as one
|
||||
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) {
|
||||
@view-transition { navigation: auto; }
|
||||
html { scroll-behavior: smooth; }
|
||||
/* Bands hand their glow to a slowly roaming layer inside the .band-bg clip
|
||||
frame (same --nebula stack) — the band itself never clips its overlays */
|
||||
.hero-band, .auth-band { background: var(--galaxy); }
|
||||
/* The auth band hands its glow to a roaming layer inside the .band-bg clip
|
||||
frame (same --nebula stack); the photo hero keeps its scrimmed photo and
|
||||
gets the lighter --nebula-glow drifting above it. */
|
||||
.auth-band { background: var(--galaxy); }
|
||||
.band-bg::before {
|
||||
content: ""; position: absolute; inset: -45% -30%;
|
||||
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 */
|
||||
.has-ambient::before { animation: ambient 60s ease-in-out infinite alternate; }
|
||||
.hero-band .band-bg::before { background: var(--nebula-glow); }
|
||||
/* 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 > * { 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(3) { animation-delay: 0.16s; }
|
||||
.hero > :nth-child(4) { animation-delay: 0.24s; }
|
||||
.hero > :nth-child(5) { animation-delay: 0.32s; }
|
||||
/* Hover feedback */
|
||||
.card, .search-results li { transition: border-color 0.15s ease, box-shadow 0.2s ease, transform 0.2s ease; }
|
||||
.card:hover { transform: translateY(-2px); }
|
||||
/* Editorial cover micro-zoom on card hover (AstroWind pattern; card clips it) */
|
||||
.card .cover { transition: transform 0.3s ease; }
|
||||
.card:hover .cover, .card:focus-within .cover { transform: scale(1.03); }
|
||||
.btn, .login-mock, .navlink { transition: background-color 0.15s ease, color 0.15s ease; }
|
||||
/* Unified hover feedback — single-property fades, shared tokens */
|
||||
a, .btn, .post-nav-title {
|
||||
transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease),
|
||||
border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
|
||||
}
|
||||
.card, .search-results li { transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease); }
|
||||
.card:hover { transform: translateY(-2px); } /* the one elevate gesture (no cover zoom) */
|
||||
}
|
||||
@keyframes nebula { /* inert unless applied inside the block above */
|
||||
from { transform: translate3d(-6%, -4%, 0) rotate(-5deg) scale(1); }
|
||||
to { transform: translate3d(6%, 5%, 0) rotate(5deg) scale(1.22); }
|
||||
from { transform: translate3d(-8%, -5%, 0) rotate(-6deg) scale(1); }
|
||||
to { transform: translate3d(8%, 6%, 0) rotate(6deg) scale(1.3); }
|
||||
}
|
||||
@keyframes ambient {
|
||||
from { transform: translate3d(-4%, -3%, 0) rotate(-9deg) scale(1); opacity: 0.85; }
|
||||
to { transform: translate3d(4%, 4%, 0) rotate(9deg) scale(1.14); opacity: 1; }
|
||||
from { transform: translate3d(-5%, -4%, 0) rotate(-10deg) scale(1); opacity: 0.85; }
|
||||
to { transform: translate3d(5%, 5%, 0) rotate(10deg) scale(1.16); opacity: 1; }
|
||||
}
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(14px); } }
|
||||
|
||||
Reference in New Issue
Block a user