Files
portal/src/styles/global.css
T
ctao/demoandClaude Fable 5 0bb378e0cc fix: typing-in-header scroll jump — global scroll-padding-top made Chrome's per-keystroke caret reveal walk the page to 0; offset moved to anchor targets (main [id] scroll-margin-top)
Chrome re-scrolls the focused editable into view on every keystroke and
honours the viewport's scroll-padding-top while ignoring the editable's
own scroll-margin (crbug.com/41492445) — so the sticky-header search
field, which sits above the 76px line and can never be scrolled below
it, dragged the page toward 0 one keystroke at a time, and the previous
negative-scroll-margin counter could not work. CDP harness bisection:
of all suspects (smooth scroll, sticky header, overflow clip,
animations, ambient layer, suggest siblings) only removing the global
scroll-padding stops the jump. TOC/skip anchor offset now lives as
scroll-margin-top on the in-content targets; harness confirms scrollY
constant while typing and anchors still land below the header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jWfn3RwPfFGTtBddm36Uy
2026-07-25 23:56:05 +02:00

787 lines
44 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ==========================================================================
CTAO Science Portal — design tokens per the official CTAO Brand Guidelines
(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.
========================================================================== */
:root {
color-scheme: light;
/* Brand palette — hex values straight from the brand book */
--galaxy: #00004a; /* Galaxy Blue — headings, header/footer bands */
--cherenkov: #00e4d8; /* Cherenkov Blue — primary CTA, flash, hero subtitle, logo cluster ONLY */
--moon: #f5f5f5; /* Moon Gray — surfaces */
--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 */
--muted: #46536a;
--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); /* <mark> highlight only */
--cherenkov-hover: color-mix(in oklab, var(--cherenkov), var(--galaxy) 12%); /* CTA hover state */
/* stroke 3: at ~0.72em rendering this matches the .ico family's ~1.5px optical weight */
--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="3"/%3E%3C/svg%3E');
/* 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", "Space Grotesk Fallback", 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 (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: 24px;
--space-l: clamp(24px, 2vw + 16px, 40px);
--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. */
/* Aurora rule (Gemini-style): gradient-only surfaces (login) get LARGE, soft,
slowly flowing color fields — one protagonist per surface: the photo hero
animates only the photo (Ken Burns), never glow flares on top of it. */
--nebula:
radial-gradient(60% 70% at 78% 78%, rgba(0, 228, 216, 0.42), transparent 70%),
radial-gradient(55% 62% at 10% 12%, rgba(0, 122, 255, 0.36), transparent 68%),
radial-gradient(80% 90% at 42% 55%, rgba(0, 0, 156, 0.8), transparent 74%);
/* 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, preload (Base.astro)
+ font-display: swap + metric-matched Arial fallbacks for BOTH families.
NOT `optional`: its ~100ms window loses to per-navigation revalidation
(dev/preview serve no-cache), so pageviews randomly committed to Arial or
the brand font — "fonts flash" between navigations. With swap every page
converges on the brand fonts, and the size-adjusted fallbacks make the
swap metric-neutral (near-zero shift). Override values computed from the
actual font metrics (weighted latin advance widths, capsize method). */
@font-face {
font-family: "Inter"; font-weight: 400 700; font-display: swap;
src: url("/fonts/inter-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Space Grotesk"; font-weight: 500 700; font-display: swap;
src: url("/fonts/space-grotesk-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter Fallback"; src: local("Arial");
size-adjust: 107%; ascent-override: 90.2%; descent-override: 22.48%; line-gap-override: 0%;
}
@font-face {
font-family: "Space Grotesk Fallback"; src: local("Arial");
size-adjust: 111.12%; ascent-override: 88.55%; descent-override: 26.28%; line-gap-override: 0%;
}
/* Screen-reader-only utility (visually hidden, still announced) */
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
/* Anchor jumps (TOC, skip link) must land below the 76px sticky header. That
offset used to be a global `html { scroll-padding-top: 76px }` — but Chrome
re-scrolls the focused editable into view on EVERY keystroke and honours
scroll-padding-top while doing it (crbug.com/41492445: the reveal also
ignores the editable's own scroll-margin, so a negative scroll-margin on
the header inputs could not cancel it). The header search fields sit above
the 76px line by definition and, being sticky, can never be scrolled below
it — so each typed character walked the page toward 0. Scoping the offset
to the in-content anchor targets fixes typing and keeps every jump
(TOC headings, skip link → #main) landing below the header. */
main, main [id] { scroll-margin-top: 76px; }
/* 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
`hidden`, creates no scroll container — sticky header keeps working. */
html, body { overflow-x: clip; }
body {
margin: 0;
font-family: var(--font-body);
background: #fff;
color: var(--text);
line-height: 1.5;
min-height: 100dvh;
}
/* 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; }
.prose p, .desc { text-wrap: pretty; }
/* 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 */
a:not([href]) { text-decoration: none; cursor: default; }
img { max-width: 100%; height: auto; display: block; }
/* Visible keyboard focus everywhere (WCAG 2.4.7) */
:focus-visible { outline: 2px solid var(--azure); outline-offset: 2px; }
/* Skip link — first tab stop, hidden until focused (WCAG 2.4.1) */
.skip {
position: absolute; left: 16px; top: -52px; z-index: 20;
background: var(--galaxy); color: #fff; text-decoration: none;
padding: 10px 16px; border-radius: 0 0 var(--radius) var(--radius);
}
.skip:focus { top: 0; }
/* Safety net: even third-party/UA leftovers never move for reduced-motion users */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; animation: none !important; }
}
.container { max-width: var(--maxw); margin: 0 auto; padding: 0 var(--gutter); }
/* 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
official logo, never a CSS approximation. Budget: one accent per view; static. */
.flash { width: 12px; height: 12px; color: var(--cherenkov); flex: none; }
/* 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; }
@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: everything keeps its natural size except the spacer
(links are single words, they can never wrap) */
.nav > * { flex: 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; text-decoration: none; }
.brand img { height: 26px; width: auto; }
.brand .brand-sub {
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: 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: #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: #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: 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;
}
/* 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; }
/* stroke 2.6 ≈ the family's 1.5px rendered weight at 14px (icons must not thin out when small) */
.nav-search .search-ico { left: 11px; width: 14px; height: 14px; color: rgba(255, 255, 255, 0.7); stroke-width: 2.6; }
.nav-search input {
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::placeholder { color: rgba(255, 255, 255, 0.65); } /* 6.9:1 */
.nav-search input:focus { border-color: #fff; }
/* Autocomplete panel: never narrower than the field, wide enough for a
typical title in ≤2 lines (Baymard autocomplete; DocSearch ~500px) */
.nav-search .suggest { left: auto; right: 0; width: min(480px, calc(100vw - 2 * var(--gutter))); top: calc(100% + 10px); }
/* ≤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; }
.nav-search { display: none; } /* field is desktop-only (NN/g tiers) */
.search-pop { display: block; } /* magnifier reveal takes over ≤1024px */
.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; white-space: nowrap;
}
.menu nav {
position: absolute; left: 0; right: 0; top: 100%;
display: flex; flex-direction: column; padding: 6px var(--gutter) 14px;
background: var(--galaxy); border-top: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: var(--shadow-lifted);
}
.menu nav a {
display: flex; align-items: center; min-height: 44px;
color: #fff; font-weight: 500; text-decoration: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
.menu nav a:last-child { border-bottom: 0; }
.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; }
.menu summary { min-width: 44px; justify-content: center; padding: 0; margin-left: 2px; }
}
@media (max-width: 360px) {
.nav { padding: 0 14px; gap: 2px; }
.brand { margin-right: 4px; }
/* 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 {
content: ""; display: inline-block; width: 0.72em; height: 0.72em; margin-left: 3px;
background: currentColor;
-webkit-mask: var(--ico-ext) center / contain no-repeat;
mask: var(--ico-ext) center / contain no-repeat;
}
/* 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(--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 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 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: 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-l); }
a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
/* Compound selector: outranks the later-defined .btn base (source-order tie
would otherwise fill the ghost with Cherenkov) */
.btn.btn--ghost { background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, 0.55); }
.btn.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-block: var(--space-band) 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-block-start: 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; }
.search-ico { position: absolute; left: 16px; top: 50%; translate: 0 -50%; color: var(--muted); pointer-events: none; }
.search-form input {
flex: 1; min-width: 0; font: inherit; color: var(--text);
background: #fff; border: 1px solid var(--border);
border-radius: var(--radius-pill); padding: 13px 18px 13px 44px;
}
.search-form input:focus { border-color: var(--azure); }
/* 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 */
list-style: none; margin: 0; padding: 8px; text-align: left;
background: #fff; border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow-lifted);
max-height: min(60vh, 480px); overflow: auto; /* long lists scroll on mobile */
}
.suggest:empty { display: none; }
/* Live-updating lists must never become the browser's scroll anchor —
Chrome otherwise "compensates" each keystroke's DOM swap with a scroll
adjustment (the page visibly jumps while typing). */
.suggest, .search-results, #search-status { overflow-anchor: none; }
.suggest li { display: flex; align-items: flex-start; gap: var(--space-s); padding: 0 12px; border-radius: calc(var(--radius) - 8px); }
.suggest li + li { margin-top: 2px; } /* rows breathe — hover fills read as discrete items */
.suggest li:hover, .suggest li:focus-within { background: var(--moon); }
/* Focused-empty state labels ("Recent" / "Latest news") — quiet, non-interactive */
li.suggest-label {
color: var(--muted); font-size: var(--fs-xs); font-weight: 600;
text-transform: uppercase; letter-spacing: 0.08em; padding-top: 8px;
}
.suggest li.suggest-label:hover { background: none; }
/* Each later group opens with a hairline — label + its rows read as one unit */
.suggest li.suggest-label:not(:first-child) {
margin-top: 6px; padding-top: 12px; border-top: 1px solid var(--border); border-radius: 0;
}
.search-results li.suggest-label { background: none; border: 0; box-shadow: none; padding: 8px 0 0; }
/* The link holds <mark>-split text nodes — it must lay out as flowing text,
never as a flex row (flex would turn every fragment into its own item and
stop titles from wrapping). ≥44px target comes from the block padding. */
.suggest a {
flex: 1; min-width: 0; display: block; padding-block: 12px; /* row ≥44px — real tap target */
color: var(--text); font-weight: 500; font-size: var(--fs-s); text-decoration: none;
overflow-wrap: break-word;
}
.suggest small { margin-top: 14px; } /* date tracks the first title line */
/* "Press Enter for all results" — a real list row (built by search-client.js,
aria-hidden like the group labels so screen-reader exposure is consistent) */
.suggest li.suggest-hint {
margin-top: 4px; padding: 8px 12px 2px; border-top: 1px solid var(--border);
border-radius: 0; color: var(--muted); font-size: var(--fs-xs);
}
.suggest li.suggest-hint:hover { background: none; }
/* Section header row (h2 + optional "All news →" link) */
.section-head {
display: flex; align-items: baseline; justify-content: space-between;
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: var(--fs-h2); }
.more { font-weight: 600; text-decoration: none; white-space: nowrap; }
.more:hover { text-decoration: underline; }
/* Responsive 1/2/3-column grid (news cards + service tiles); container for @container */
.grid { display: grid; gap: var(--space-m); grid-template-columns: 1fr; container-type: inline-size; }
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 940px) { .grid { grid-template-columns: repeat(3, 1fr); } }
/* Subgrid: cover/title/desc/meta rows align across each card row */
@supports (grid-template-rows: subgrid) {
.grid > .card:not(.card--featured, .tile) { display: grid; grid-template-rows: subgrid; grid-row: span 4; gap: 0; }
.grid > .card:not(.card--featured, .tile) .body { display: grid; grid-template-rows: subgrid; grid-row: span 3; gap: 10px; }
}
/* 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; line-height: 1.45;
}
.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; }
.card h3 {
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; }
/* Stretched link: the title's tap target covers the whole card (still one tab stop);
::before keeps ::after free for the external-link arrow */
.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: 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: 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: 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. */
.pagination {
display: flex; align-items: center; justify-content: center; flex-wrap: wrap;
gap: 4px var(--space-s); margin-top: var(--space-xl);
}
.pagination ol { display: flex; flex-wrap: wrap; justify-content: center; list-style: none; margin: 0; padding: 0; gap: 4px; }
.page-link {
display: inline-flex; align-items: center; justify-content: center;
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); }
.page-link[aria-current="page"] { background: var(--galaxy); color: #fff; }
.page-step { font-weight: 600; }
a.page-step { color: var(--link); }
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).
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; }
/* Planned tiles complete the grid rows but stay subdued (no link, muted) */
.tile--planned :is(h3, .ico) { color: var(--muted); }
/* Remaining planned services — one quiet line under the grid */
.planned-note { color: var(--muted); font-size: var(--fs-s); margin: var(--space-m) 0 0; }
/* 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-block: var(--space-l) var(--space-xl); max-width: 70ch; }
.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: 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; }
}
/* Standfirst — the description shown as the visible lead under the headline
(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: var(--fs-xs); text-transform: uppercase; font-weight: 600;
letter-spacing: 0.08em; color: var(--muted);
}
.toc strong { display: block; margin-bottom: 10px; }
.toc ol { list-style: none; margin: 0; padding: 0; }
.toc li { margin: 6px 0; }
.toc .d3 { padding-left: var(--space-s); }
.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; }
/* 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; stroke-width: 2.6; } /* optical weight match at 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: 80px; align-self: start;
margin-top: calc(var(--space-l) + 44px); /* aligns with the article h1 */
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 { line-height: 1.65; overflow-wrap: break-word; }
.prose a { overflow-wrap: anywhere; }
.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(--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: 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: 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(--galaxy); }
/* Prev/next article links — sequential nav from the date-sorted collection */
.post-nav {
display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-m);
border-top: 1px solid var(--border); margin-top: var(--space-xl); padding-top: var(--space-m);
}
.post-nav a { display: block; min-width: 0; text-decoration: none; }
.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: var(--fs-xs); font-weight: 600;
text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 4px;
}
.post-nav .post-nav-title {
color: var(--galaxy); font-weight: 500;
display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.post-nav a:hover .post-nav-title { color: var(--link); }
/* Generic page section — one rhythm token for all pages */
.page { padding-block: var(--space-xl); }
.panel {
background: #fff; border: 1px solid var(--border); border-radius: var(--radius);
padding: var(--space-m); box-shadow: var(--shadow-ambient);
}
.panel--narrow { max-width: 640px; }
/* Bulleted content inside a panel (/proposals integration landing) */
.panel-list { margin: 0 0 var(--space-m); padding-left: 1.25em; }
.panel-list li + li { margin-top: 6px; }
.btn {
background: var(--cherenkov); color: var(--galaxy); border: 0;
padding: 12px 24px; border-radius: var(--radius-pill);
font: inherit; font-weight: 600; cursor: pointer;
}
.btn:hover { background: var(--cherenkov-hover); }
/* Non-interactive placeholder CTA (planned integrations) — quiet Moon fill,
real disabled semantics; Cherenkov stays reserved for live primary actions */
.btn:disabled { background: var(--moon); color: var(--muted); cursor: not-allowed; }
.btn:disabled:hover { background: var(--moon); }
.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;
same single-signal chrome as cards (borderless on Moon, shadow hover).
Margins, not grid gap: idle-state group labels must hug their rows. */
.search-results { list-style: none; margin: var(--space-m) 0 0; padding: 0; max-width: 720px; }
.search-results li + li { margin-top: var(--space-m); } /* each article reads as its own card */
.search-results li.suggest-label + li { margin-top: var(--space-s); } /* rows hug their group label */
.search-results li {
display: flex; align-items: flex-start; gap: var(--space-s);
background: #fff; border-radius: var(--radius);
padding: var(--space-m); box-shadow: var(--shadow-ambient);
}
.search-results li:hover { box-shadow: var(--shadow-lifted); }
/* Title flows as inline text (never display:flex — see .suggest a note);
card-title treatment (fs-l Galaxy) so titles outweigh the muted meta */
.search-results a {
flex: 1; min-width: 0; overflow-wrap: break-word; text-decoration: none;
font-weight: 600; font-size: var(--fs-l); line-height: 1.3; color: var(--galaxy);
}
.search-results a:hover { color: var(--link); text-decoration: underline; }
/* 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) —
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 — 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: 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;
}
.badge-mock::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--galaxy); flex: none; }
/* 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: 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: var(--fs-s);
}
.rows li:last-child { border-bottom: 0; }
.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: var(--fs-xs); }
/* Status pills — neutral chrome; the label text carries the state */
.pill {
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(--moon); }
.pill--review { color: var(--text); background: #fff; }
.pill--draft { color: var(--muted); background: #fff; }
/* Auth (login mock) — nebula backdrop + floating card; the ONLY navy band
besides the home hero */
.auth-band {
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(28px, 4vw, 44px); max-width: 420px; width: 100%;
box-shadow: var(--shadow-lifted);
display: flex; flex-direction: column; gap: var(--space-s);
}
/* Logo ≥100px digital (BRAND B.1.3): 26px height ≈ 125px wordmark */
.auth-logo { height: 26px; width: auto; margin-bottom: var(--space-m); align-self: flex-start; }
/* 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; } }
/* Hierarchy per BRAND D.3.4: SG-medium Galaxy headline, Inter body */
.auth-card h1 { color: var(--galaxy); margin: 0; font-size: var(--fs-h2); }
.auth-card .muted { color: var(--muted); font-size: var(--fs-s); margin: 0; }
.auth-card .notice { margin: 0; }
.btn-wide { width: 100%; margin-top: var(--space-m); }
/* 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: 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; }
.foot-top p { margin: 0; max-width: 60ch; }
.foot-links { display: flex; flex-wrap: wrap; gap: 4px var(--space-m); }
.foot-links a { color: #fff; font-weight: 500; text-decoration: none; display: inline-flex; align-items: center; min-height: 24px; }
.foot-links a[href]:hover { text-decoration: underline; }
.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 — 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:
radial-gradient(44% 38% at 82% 10%, rgba(0, 228, 216, 0.16), transparent 70%),
radial-gradient(50% 44% at 6% 38%, rgba(0, 122, 255, 0.12), transparent 70%),
radial-gradient(46% 40% at 68% 90%, rgba(0, 0, 156, 0.1), transparent 70%);
}
/* ==========================================================================
Motion — ALL animation/transition lives in this block (WCAG 2.3.3; the
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) {
/* NO cross-document view transitions: the root crossfade snapshots the new
page at its first render opportunity — on slow networks that is a half-
parsed page, so every navigation reads as a full-page blink. Plain MPA
paint + prefetch is the intended feel (see DESIGN.md, Motion policy). */
html { scroll-behavior: smooth; }
/* 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 18s ease-in-out infinite alternate;
}
/* Photo hero, animated path: the photo itself moves (slow Ken Burns zoom —
the unmistakable "alive" signal) and carries its scrim so the AA zones
travel with it (≤ ~4% shift at scale 1.1 — left text zone stays ≥.76α).
The Cherenkov glow drifts AND breathes (opacity pulse) above the photo. */
.hero-band { background: var(--galaxy); }
.hero-band .band-bg::before {
inset: -6%;
background: var(--hero-scrim), url("/brand/hero.jpg") center / cover;
animation: kenburns 26s ease-in-out infinite alternate;
}
/* Whole-page wash — slow drift, clearly alive */
.has-ambient::before { animation: ambient 36s ease-in-out infinite alternate; }
/* Hero content rises in once, gently staggered */
.hero > * { 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; }
/* 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); }
.suggest li { transition: background-color var(--dur) var(--ease); } /* same quiet fade as every hover */
.card:hover, .search-results li:hover { transform: translateY(-2px); } /* the one elevate gesture (no cover zoom) */
}
@keyframes nebula { /* Gemini-style aurora: slow, ample, gentle breath */
from { transform: translate3d(-9%, -6%, 0) rotate(-5deg) scale(1); opacity: 0.85; }
to { transform: translate3d(9%, 7%, 0) rotate(5deg) scale(1.22); opacity: 1; }
}
@keyframes kenburns {
from { transform: scale(1) translate3d(0, 0, 0); }
to { transform: scale(1.1) translate3d(2%, -1.5%, 0); }
}
@keyframes ambient {
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); } }