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
787 lines
44 KiB
CSS
787 lines
44 KiB
CSS
/* ==========================================================================
|
||
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); } }
|