Files
portal/src/styles/global.css
T
ctaoandClaude Fable 5 4fa35e5317 fix: mobile gutters, header search tiers, suggest wiring + empty-state, news pagination, hero CTAs, ghost button
- padding-block on .page/.page-head/.article: container gutter survived only >=1176px (shorthand padding zeroed it) — mobile content sat flush left
- restore <=1024px tiers: field hidden, magnifier + menu visible (header no longer overflows at 375px)
- re-wire hdr-q suggestions lost in the field restore; newsSearch inits null-safe
- focused-empty search state: recent queries (localStorage, cap 4) + latest articles (Baymard autocomplete guidance)
- suggest panel 480px (Baymard/DocSearch width convention)
- /news page 1 = 25 items (featured + 24 = even 3-col rows), later pages 24, custom slicing
- hero CTAs: primary Browse news (/news, spec-first real service), ghost Explore the data (external); mock Proposals out of hero
- .btn.btn--ghost compound selector — .btn base defined later was filling ghosts with Cherenkov

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

723 lines
40 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 */
--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 — 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 (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. */
--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 */
@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%;
}
/* 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%; 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
`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; }
.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: 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; }
.suggest li { display: flex; align-items: flex-start; gap: var(--space-s); padding: 0 12px; border-radius: calc(var(--radius) - 8px); }
.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; }
.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: 10px;
color: var(--text); font-weight: 500; font-size: var(--fs-s); text-decoration: none;
overflow-wrap: break-word;
}
.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: var(--fs-xs);
}
/* 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; }
.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-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; }
.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; }
.form-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.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;
}
.form-row input:focus, .form-row select:focus, .form-row textarea:focus { border-color: var(--azure); }
.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); }
.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) */
.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-radius: var(--radius);
padding: 12px 18px; 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) */
.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; }
/* 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: var(--space-l); max-width: 440px; width: 100%;
box-shadow: var(--shadow-lifted);
}
.auth-logo { height: 24px; width: auto; margin-bottom: var(--space-m); }
/* 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: 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) {
@view-transition { navigation: auto; }
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 16s 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 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); }
.card:hover { transform: translateY(-2px); } /* the one elevate gesture (no cover zoom) */
}
@keyframes nebula { /* inert unless applied inside the block above */
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(-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); } }