feat: content-UX — header search, metadata de-noise, icon/microcopy coherence, article composition
This commit is contained in:
+62
-19
@@ -11,12 +11,15 @@ const ogImage = new URL(image || '/brand/og-card.jpg', site);
|
||||
const path = Astro.url.pathname;
|
||||
const isActive = (href) => (href === '/' ? path === '/' : path.startsWith(href));
|
||||
// Navigation = services from the Science Portal spec (REQUIREMENTS.md §2/§5).
|
||||
// External systems built by other teams are links out, clearly marked.
|
||||
// Order: serial-position effect (NN/g) — News (universal entry) first, Support
|
||||
// (help convention) last; Data/Proposals by task frequency; Dashboard is the
|
||||
// personal area, placed late next to the Sign-in cluster. External systems
|
||||
// built by other teams are links out, clearly marked.
|
||||
const links = [
|
||||
{ href: '/news', label: 'News' },
|
||||
{ href: 'https://padc-ctao-data-explorer.obspm.fr/', label: 'Data', ext: true },
|
||||
{ href: '/proposals', label: 'Proposals' },
|
||||
{ href: '/dashboard', label: 'Dashboard' },
|
||||
{ href: 'https://padc-ctao-data-explorer.obspm.fr/', label: 'Data', ext: true },
|
||||
{ href: '/support', label: 'Support' },
|
||||
];
|
||||
---
|
||||
@@ -45,6 +48,15 @@ const links = [
|
||||
</head>
|
||||
<body class={ambient ? 'has-ambient' : undefined}>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
{/* Demo disclosure — GOV.UK phase-banner pattern: one slim, honest banner
|
||||
instead of implementation talk scattered through the UI. Not sticky;
|
||||
it scrolls away and the header sticks below it. */}
|
||||
<div class="phase-banner">
|
||||
<div class="container">
|
||||
<strong class="phase-tag">Demo</strong>
|
||||
<span>This is a demonstration of the CTAO Science Portal — content is sample data.</span>
|
||||
</div>
|
||||
</div>
|
||||
<header class="site-header">
|
||||
<div class="container nav">
|
||||
<a class="brand" href="/" aria-label="CTAO Science Portal — home">
|
||||
@@ -59,17 +71,37 @@ const links = [
|
||||
))}
|
||||
</nav>
|
||||
<span class="spacer"></span>
|
||||
<a class="navlink" href="/search">Search</a>
|
||||
<a class="navlink navlink--edit" href="/admin" title="Content editor (demo CMS)">
|
||||
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><path d="M17 3l4 4L8 20l-5 1 1-5L17 3z" /></svg>
|
||||
Editor
|
||||
</a>
|
||||
{/* Site search lives in the header (desktop: compact field; ≤1024px: the
|
||||
magnifier <details> below). Plain GET form — works without JS; with JS
|
||||
the shared suggest dropdown appears (search-client.js). */}
|
||||
<form class="nav-search" role="search" aria-label="Site search" action="/search" method="get">
|
||||
<label class="sr-only" for="hdr-q">Search news</label>
|
||||
<svg class="ico search-ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
|
||||
<input id="hdr-q" name="q" type="search" placeholder="Search…" autocomplete="off" />
|
||||
<ul class="suggest" id="hdr-suggest"></ul>
|
||||
<p class="sr-only" id="hdr-suggest-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
<a class="login-mock" href="/login">Sign in</a>
|
||||
{/* ≤1024px: magnifier button reveals a full-width search row. name="header-panel"
|
||||
pairs it with the menu as a native exclusive group — opening one closes
|
||||
the other, no JS (exclusive-accordion behavior of <details name>). */}
|
||||
<details class="search-pop" name="header-panel">
|
||||
<summary aria-label="Search">
|
||||
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
|
||||
</summary>
|
||||
<form role="search" aria-label="Site search" action="/search" method="get">
|
||||
<label class="sr-only" for="pop-q">Search news</label>
|
||||
<input id="pop-q" name="q" type="search" placeholder="Search news…" autocomplete="off" />
|
||||
<button class="btn" type="submit">Search</button>
|
||||
<ul class="suggest" id="pop-suggest"></ul>
|
||||
<p class="sr-only" id="pop-suggest-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
</details>
|
||||
{/* Mobile disclosure menu — native <details>/<summary>, no JS; desktop hides it via CSS */}
|
||||
<details class="menu">
|
||||
<summary>
|
||||
<details class="menu" name="header-panel">
|
||||
<summary aria-label="Menu">
|
||||
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><path class="i-menu" d="M3 6h18M3 12h18M3 18h18" /><path class="i-close" d="M5 5l14 14M19 5 5 19" /></svg>
|
||||
Menu
|
||||
<span class="menu-label">Menu</span>
|
||||
</summary>
|
||||
<nav aria-label="Main navigation">
|
||||
{links.map((l) => l.ext ? (
|
||||
@@ -77,12 +109,21 @@ const links = [
|
||||
) : (
|
||||
<a href={l.href} aria-current={isActive(l.href) ? 'page' : undefined}>{l.label}</a>
|
||||
))}
|
||||
<a href="/search">Search</a>
|
||||
<a href="/admin">Editor</a>
|
||||
</nav>
|
||||
</details>
|
||||
</div>
|
||||
</header>
|
||||
<script is:inline src="/search-client.js"></script>
|
||||
<script is:inline>
|
||||
// Header search suggestions (desktop field + mobile reveal); the forms are
|
||||
// plain GET /search, so everything below is progressive enhancement.
|
||||
newsSearch({ input: 'hdr-q', list: 'hdr-suggest', status: 'hdr-suggest-status', limit: 6, unit: 'suggestion' });
|
||||
newsSearch({ input: 'pop-q', list: 'pop-suggest', status: 'pop-suggest-status', limit: 6, unit: 'suggestion' });
|
||||
var pop = document.querySelector('.search-pop');
|
||||
pop.addEventListener('toggle', function () {
|
||||
if (pop.open) document.getElementById('pop-q').focus();
|
||||
});
|
||||
</script>
|
||||
<main id="main">
|
||||
<slot />
|
||||
</main>
|
||||
@@ -92,23 +133,25 @@ const links = [
|
||||
<div class="container">
|
||||
<div class="foot-top">
|
||||
<img src="/brand/AAFF_CTAO_Logo_RGB_Monochrome-negative.svg" alt="CTAO" />
|
||||
<p>Science Portal — news, observation proposals, data and user services of the
|
||||
Cherenkov Telescope Array Observatory.</p>
|
||||
<p>CTAO Science Portal — demo by Cyfronet (SUSS-PORT team).</p>
|
||||
</div>
|
||||
{/* Order: action/utility first, legal middle, meta last (GOV.UK/NN-g
|
||||
footer anatomy). Spec §3.3.1 requires the five items, not an order. */}
|
||||
<nav class="foot-links" aria-label="Footer">
|
||||
<a href="/pages/contact">Contact</a>
|
||||
<a href="/search">Search</a>
|
||||
<a href="/pages/disclaimer">Disclaimer</a>
|
||||
<a href="/pages/privacy">Privacy</a>
|
||||
<a href="/pages/contact">Contact</a>
|
||||
<a>Site settings</a>
|
||||
<a href="/search">Search</a>
|
||||
{/* Internal tooling stays out of the main nav (NN/g: navigation reflects
|
||||
user tasks); the editor is a discreet utility link for the demo. */}
|
||||
<a href="/admin">Content editor</a>
|
||||
</nav>
|
||||
<div class="foot-note">
|
||||
{/* The real brand flash — path taken from the official logo SVG (BRAND D.4).
|
||||
Sole decorative accent in this view; static by design. */}
|
||||
<svg class="flash" viewBox="85.3 15 15 15" aria-hidden="true"><path fill="currentColor" d="M94.7,23.8c1-1.4,3.3-2.1,5.2-2.7c-2,0.1-4.4,0.3-5.8-0.6c-1.4-1-2.1-3.3-2.7-5.2c0.1,2,0.3,4.4-0.6,5.8c-1,1.4-3.3,2.1-5.2,2.7c2-0.1,4.4-0.3,5.8,0.6c1.4,1,2.1,3.3,2.7,5.2C93.9,27.6,93.7,25.2,94.7,23.8z"/></svg>
|
||||
<span>Science Portal Core demo (Cyfronet / SUSS-PORT team). Sample content
|
||||
imported from ctao.org; the Site settings link is a placeholder. Statically
|
||||
generated from Markdown (git-based CMS).</span>
|
||||
<span>© 2026 CTAO — demonstration, not an official service.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -58,7 +58,7 @@ const tickets = [
|
||||
<p class="notice">Search and download in the <a href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener" class="external">Data Explorer</a> (external, LUX team).</p>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2 class="panel-title">Help-desk tickets</h2>
|
||||
<h2 class="panel-title">Support tickets</h2>
|
||||
<ul class="rows">
|
||||
{tickets.map((t) => (
|
||||
<li>
|
||||
|
||||
+14
-17
@@ -7,6 +7,9 @@ const posts = (await getCollection('news', ({ data }) => !data.draft))
|
||||
.slice(0, 3);
|
||||
const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d);
|
||||
const iso = (d) => d.toISOString().slice(0, 10);
|
||||
// Reading time from the Markdown body (~220 wpm) — differentiating card metadata;
|
||||
// category/author are identical across all articles, so cards omit them (NN/g).
|
||||
const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220));
|
||||
// Services per the Science Portal spec; unbuilt ones are labelled, never faked.
|
||||
const services = [
|
||||
{ title: 'Proposals', href: '/proposals', desc: 'Submit observation proposals, including ToO / MWL requests.', badge: 'mock' },
|
||||
@@ -24,23 +27,18 @@ const services = [
|
||||
<div class="eyebrow">Cherenkov Telescope Array Observatory</div>
|
||||
<h1>CTAO Science Portal</h1>
|
||||
<p>Exploring the Universe at the Highest Energies</p>
|
||||
<form class="hero-search" role="search" action="/search" method="get">
|
||||
<label class="sr-only" for="home-q">Search news</label>
|
||||
<svg class="ico search-ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7" /><path d="m20 20-4.3-4.3" /></svg>
|
||||
<input id="home-q" name="q" type="search" placeholder="Search news and announcements…" autocomplete="off" />
|
||||
<button class="btn" type="submit">Search</button>
|
||||
{/* Live suggestions (plain links, Tab-accessible); Enter still submits to /search */}
|
||||
<ul class="suggest" id="home-suggest"></ul>
|
||||
<p class="sr-only" id="home-suggest-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
{/* Search lives in the header (one search location); the hero carries the
|
||||
two core scientist tasks per SPEC §3.1 instead — classic hero-CTA pair. */}
|
||||
<div class="hero-cta">
|
||||
<a class="btn external" href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener">Explore the data</a>
|
||||
<a class="btn btn--ghost" href="/proposals">Submit a proposal</a>
|
||||
</div>
|
||||
{posts[0] && (
|
||||
<p class="hero-teaser">Latest: <a href={`/news/${posts[0].id}`}>{posts[0].data.title}</a></p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script is:inline src="/search-client.js"></script>
|
||||
<script is:inline>
|
||||
newsSearch({ input: 'home-q', list: 'home-suggest', status: 'home-suggest-status', limit: 6, unit: 'suggestion' });
|
||||
</script>
|
||||
|
||||
{/* Surface rhythm (DESIGN.md): services on Moon Gray, editorial news on white */}
|
||||
<section class="band--moon">
|
||||
<div class="container page">
|
||||
@@ -56,7 +54,7 @@ const services = [
|
||||
<p class="desc">{s.desc}</p>
|
||||
{s.badge === 'mock' && <span class="badge-mock">Mock</span>}
|
||||
{s.badge === 'ext' && <span class="badge-ext">External — LUX team</span>}
|
||||
{s.badge === 'tbd' && <span class="badge-ext">Planned (TBD)</span>}
|
||||
{s.badge === 'tbd' && <span class="badge-ext">Planned — not yet available</span>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
@@ -73,10 +71,9 @@ const services = [
|
||||
<article class="card">
|
||||
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" loading="lazy" />}
|
||||
<div class="body">
|
||||
<span class="cat">{post.data.category}</span>
|
||||
<h3><a href={`/news/${post.id}`}>{post.data.title}</a></h3>
|
||||
<p class="desc">{post.data.description}</p>
|
||||
<div class="meta">{post.data.author} · <time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time></div>
|
||||
<div class="meta"><time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time> · {readMin(post)} min read</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
@@ -8,11 +8,10 @@ import Base from '../layouts/Base.astro';
|
||||
<img class="auth-logo" src="/brand/CTAO_Logo_positive.svg" alt="CTAO" />
|
||||
<span class="badge-mock">Mock — AAI integration pending</span>
|
||||
<h1>Sign in</h1>
|
||||
<p class="muted">One account for all CTAO services. Authentication is handled by the
|
||||
central <strong>CTAO AAI</strong> (single sign-on) — the portal never sees your password.</p>
|
||||
{/* Auth pages are scanned, not read — one line, no reassurance prose */}
|
||||
<p class="muted">Use your CTAO account — single sign-on via CTAO AAI.</p>
|
||||
<button class="btn btn-wide" type="button">Continue with your CTAO account</button>
|
||||
<p class="notice">No account yet? <a>Register via CTAO AAI</a> · Access level follows
|
||||
your CTAO role and permissions.</p>
|
||||
<p class="notice">No account? <a>Register via CTAO AAI</a></p>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
@@ -15,6 +15,9 @@ const { page } = Astro.props;
|
||||
const first = page.currentPage === 1;
|
||||
const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d);
|
||||
const iso = (d) => d.toISOString().slice(0, 10);
|
||||
// Reading time from the Markdown body (~220 wpm) — differentiating card metadata;
|
||||
// category/author are identical across all articles, so cards omit them (NN/g).
|
||||
const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220));
|
||||
// Windowed page list — 1 … n-1 n n+1 … last (0 marks an ellipsis)
|
||||
const nums = [];
|
||||
for (let n = 1; n <= page.lastPage; n++) {
|
||||
@@ -32,7 +35,7 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
<div class="container hero">
|
||||
<div class="eyebrow">Cherenkov Telescope Array Observatory</div>
|
||||
<h1>News & Announcements</h1>
|
||||
<p>Exploring the Universe at the Highest Energies</p>
|
||||
<p>Science highlights and updates from the observatory and its partners</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -45,10 +48,9 @@ const hrefFor = (n) => (n === 1 ? '/news' : `/news/${n}`);
|
||||
<img class="cover" src={encodeURI(post.data.cover)} alt="" loading={first && i === 0 ? 'eager' : 'lazy'} />
|
||||
)}
|
||||
<div class="body">
|
||||
<span class="cat">{post.data.category}</span>
|
||||
<h3><a href={`/news/${post.id}`}>{post.data.title}</a></h3>
|
||||
<p class="desc">{post.data.description}</p>
|
||||
<div class="meta">{post.data.author} · <time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time></div>
|
||||
<div class="meta"><time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time> · {readMin(post)} min read</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
|
||||
@@ -3,14 +3,22 @@ import { getCollection, render } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('news', ({ data }) => !data.draft);
|
||||
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
|
||||
// Date-sorted so each article knows its sequential neighbours (prev/next
|
||||
// pattern of every editorial site — keeps readers in the content flow).
|
||||
const posts = (await getCollection('news', ({ data }) => !data.draft)).sort(
|
||||
(a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
|
||||
);
|
||||
return posts.map((post, i) => ({
|
||||
params: { slug: post.id },
|
||||
props: { post, newer: posts[i - 1] ?? null, older: posts[i + 1] ?? null },
|
||||
}));
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { post, newer, older } = Astro.props;
|
||||
const { Content, headings } = await render(post);
|
||||
const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
|
||||
const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d);
|
||||
const readMin = Math.max(1, Math.round((post.body ?? '').split(/\s+/).length / 220));
|
||||
---
|
||||
<Base title={`${post.data.title} — CTAO`} description={post.data.description} type="article" image={post.data.cover && encodeURI(post.data.cover)}>
|
||||
{/* TOC pattern (MDN/Stripe/NN-g): sticky right rail ≥1200px, collapsed <details>
|
||||
@@ -19,7 +27,8 @@ const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).forma
|
||||
<article class="article">
|
||||
<a class="back" href="/news">← All news</a>
|
||||
<h1>{post.data.title}</h1>
|
||||
<div class="meta"><span class="cat">{post.data.category}</span><span>{post.data.author}</span>·<time datetime={post.data.date.toISOString().slice(0, 10)}>{fmt(post.data.date)}</time></div>
|
||||
{/* Meta order: category · author · date · reading time */}
|
||||
<div class="meta"><span class="cat">{post.data.category}</span><span>{post.data.author}</span>·<time datetime={post.data.date.toISOString().slice(0, 10)}>{fmt(post.data.date)}</time>·<span>{readMin} min read</span></div>
|
||||
{toc.length >= 3 && (
|
||||
<details class="toc toc--inline">
|
||||
<summary>On this page</summary>
|
||||
@@ -34,6 +43,22 @@ const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).forma
|
||||
<div class="prose">
|
||||
<Content />
|
||||
</div>
|
||||
{(newer || older) && (
|
||||
<nav class="post-nav" aria-label="More news">
|
||||
{newer && (
|
||||
<a class="post-nav-prev" rel="prev" href={`/news/${newer.id}`}>
|
||||
<small>← Newer</small>
|
||||
<span class="post-nav-title">{newer.data.title}</span>
|
||||
</a>
|
||||
)}
|
||||
{older && (
|
||||
<a class="post-nav-next" rel="next" href={`/news/${older.id}`}>
|
||||
<small>Older →</small>
|
||||
<span class="post-nav-title">{older.data.title}</span>
|
||||
</a>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</article>
|
||||
{toc.length >= 3 && (
|
||||
<nav class="toc toc--rail" aria-label="On this page">
|
||||
|
||||
@@ -7,14 +7,14 @@ import Base from '../layouts/Base.astro';
|
||||
<div class="container hero">
|
||||
<div class="eyebrow">Proposal Submission</div>
|
||||
<h1>Observation proposals</h1>
|
||||
<p>Submitting observation proposals, including ToO / MWL requests</p>
|
||||
<p>Submit observation proposals, including ToO and MWL requests</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="band--moon">
|
||||
<div class="container page">
|
||||
<span class="badge-mock">Mock — no submission logic</span>
|
||||
<span class="badge-ext">Target: Proposal Handling System (APC team) integrated into the portal</span>
|
||||
<span class="badge-ext">Planned integration: Proposal Handling System (APC team)</span>
|
||||
<div class="panel panel--narrow">
|
||||
<div class="form-row">
|
||||
<label for="f-title">Proposal title</label>
|
||||
@@ -42,7 +42,7 @@ import Base from '../layouts/Base.astro';
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="f-just">Scientific justification</label>
|
||||
<textarea id="f-just" rows="4" placeholder="Brief description of the observation goal..."></textarea>
|
||||
<textarea id="f-just" rows="4" placeholder="Brief description of the observation goal…"></textarea>
|
||||
</div>
|
||||
<button class="btn" type="button">Submit proposal</button>
|
||||
<p class="notice">Demonstration mock — the form does not store any data.</p>
|
||||
|
||||
@@ -26,7 +26,7 @@ import Base from '../layouts/Base.astro';
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<script is:inline src="/search-client.js"></script>
|
||||
{/* search-client.js is loaded once by the Base layout (header search) */}
|
||||
<script is:inline>
|
||||
const run = newsSearch({ input: 'q', list: 'search-results', status: 'search-status', limit: 20, hint: 'Type at least 2 characters.' });
|
||||
// Support /search?q=… deep links (hero search form, footer, bookmarks)
|
||||
|
||||
@@ -14,7 +14,7 @@ const channels = [
|
||||
<div class="container hero">
|
||||
<div class="eyebrow">User services</div>
|
||||
<h1>User Support</h1>
|
||||
<p>Help desk, FAQ / troubleshooting guide, user forum and mailing lists</p>
|
||||
<p>Help desk, FAQ, user forum and mailing lists</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
+159
-56
@@ -74,6 +74,11 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { -webkit-text-size-adjust: 100%; scroll-padding-top: 84px; /* 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);
|
||||
@@ -85,6 +90,8 @@ body {
|
||||
|
||||
h1, h2 { font-family: var(--font-display); letter-spacing: -0.01em; }
|
||||
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 */
|
||||
@@ -120,24 +127,72 @@ img { max-width: 100%; height: auto; display: block; }
|
||||
official logo, never a CSS approximation. Budget: one accent per view; static. */
|
||||
.flash { width: 12px; height: 12px; color: var(--cherenkov); flex: none; }
|
||||
|
||||
/* Demo disclosure — GOV.UK phase-banner pattern: slim Moon bar above the
|
||||
header (scrolls away; the header sticks below it). Galaxy-on-tint pairs
|
||||
computed ≥16:1 (same pairing as .cat chips). */
|
||||
.phase-banner { background: var(--moon); border-bottom: 1px solid var(--border); font-size: 0.8rem; }
|
||||
.phase-banner .container { display: flex; align-items: center; gap: 10px; padding-top: 6px; padding-bottom: 6px; }
|
||||
.phase-tag {
|
||||
flex: none; font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: var(--galaxy); background: var(--tint-cherenkov); border-radius: var(--radius-pill); padding: 2px 10px;
|
||||
}
|
||||
|
||||
/* Header — Galaxy Blue band, reverse (white) logo per BRAND B.1.2 */
|
||||
.site-header { background: var(--galaxy); position: sticky; top: 0; z-index: 10; }
|
||||
.nav { display: flex; align-items: center; gap: 4px; min-height: 64px; flex-wrap: nowrap; }
|
||||
.nav > nav { display: flex; align-items: center; gap: 4px; }
|
||||
/* Header tiers (one row, always): full nav >1024px; sub-brand only >1100px;
|
||||
disclosure menu (native <details>/<summary>, CSS-only) at ≤1024px */
|
||||
@media (max-width: 1100px) { .brand .brand-sub { display: none; } }
|
||||
@media (max-width: 360px) {
|
||||
.nav { padding: 0 14px; }
|
||||
.brand img { height: 20px; }
|
||||
.nav .login-mock { padding: 8px 14px; }
|
||||
}
|
||||
/* One-row guarantee: only the search field and the spacer flex — everything
|
||||
else keeps its natural size (links are single words, they can never wrap) */
|
||||
.nav > * { flex: none; }
|
||||
/* Header tiers (one row, always — width math in DESIGN.md):
|
||||
≥1100px full nav + inline search + sub-brand · 1025–1099px sub-brand hidden ·
|
||||
≤1024px disclosure menu + magnifier reveal (both native <details>, CSS-only) ·
|
||||
≤480px icon-only menu button, smaller logo · ≤360px compact paddings */
|
||||
@media (max-width: 1099px) { .brand .brand-sub { display: none; } }
|
||||
.menu { display: none; }
|
||||
.menu .i-close { display: none; }
|
||||
.menu[open] .i-menu { display: none; }
|
||||
.menu[open] .i-close { display: block; }
|
||||
.brand { display: flex; align-items: center; gap: 10px; margin-right: 10px; }
|
||||
.brand img { height: 26px; width: auto; }
|
||||
.brand .brand-sub {
|
||||
color: var(--cherenkov); font-size: 0.78rem; font-weight: 500;
|
||||
border-left: 1px solid rgba(255,255,255,0.25); padding-left: 10px;
|
||||
}
|
||||
.nav .spacer { flex: 1; }
|
||||
.nav a.navlink {
|
||||
color: var(--on-galaxy-muted); font-weight: 500; font-size: 0.95rem;
|
||||
padding: 10px 10px; text-decoration: none; border-bottom: 2px solid transparent;
|
||||
}
|
||||
.nav a.navlink:hover { color: #fff; }
|
||||
.nav a.navlink[aria-current="page"] { color: #fff; border-bottom-color: var(--cherenkov); }
|
||||
/* AAI sign-in entry — leads to the /login mock page */
|
||||
.nav .login-mock {
|
||||
color: var(--galaxy); background: var(--cherenkov); font: inherit; font-weight: 600;
|
||||
border: 0; border-radius: var(--radius-pill); padding: 8px 18px; margin-left: 6px;
|
||||
text-decoration: none; white-space: nowrap;
|
||||
}
|
||||
.nav .login-mock:hover { background: var(--cherenkov-hover); color: var(--galaxy); }
|
||||
/* Tap targets ≥44px (WCAG 2.5.5); inline-flex keeps the active bottom border */
|
||||
.nav a.navlink, .nav .login-mock {
|
||||
display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
|
||||
}
|
||||
/* Header search (desktop) — compact pill in the utility cluster; a plain GET
|
||||
form to /search (works without JS), suggestions reuse the shared .suggest
|
||||
dropdown. Flexes 130–180px so the row can never overflow (width math: DESIGN.md). */
|
||||
.nav-search { position: relative; display: flex; align-items: center; flex: 0 1 180px; min-width: 130px; margin-left: 6px; }
|
||||
.nav-search .search-ico { left: 12px; width: 15px; height: 15px; }
|
||||
.nav-search input {
|
||||
width: 100%; min-width: 0; font: inherit; font-size: 0.9rem; color: var(--text);
|
||||
background: #fff; border: 1px solid transparent; border-radius: var(--radius-pill);
|
||||
padding: 8px 14px 8px 34px;
|
||||
}
|
||||
.nav-search input:focus { border-color: var(--azure); }
|
||||
.nav-search .suggest { left: auto; right: 0; width: min(380px, 92vw); top: calc(100% + 10px); }
|
||||
/* ≤1024px replacement: magnifier <details> reveal — see the menu media block */
|
||||
.search-pop { display: none; }
|
||||
@media (max-width: 1024px) {
|
||||
.nav > nav, .nav > a.navlink { display: none; }
|
||||
.nav > nav { display: none; }
|
||||
.menu { display: block; }
|
||||
.menu summary {
|
||||
display: inline-flex; align-items: center; gap: 8px; min-height: 44px;
|
||||
@@ -158,37 +213,42 @@ img { max-width: 100%; height: auto; display: block; }
|
||||
}
|
||||
.menu nav a:last-child { border-bottom: 0; }
|
||||
.menu nav a[aria-current="page"] { color: var(--cherenkov); }
|
||||
/* Magnifier icon-button reveals a full-width search row under the header
|
||||
(visible search beats search buried in the menu — NN/g mobile-search).
|
||||
Same native-<details> panel pattern as the menu. */
|
||||
.nav-search { display: none; }
|
||||
.search-pop { display: block; }
|
||||
.search-pop summary {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 44px; min-height: 44px; color: #fff; cursor: pointer; list-style: none;
|
||||
}
|
||||
.search-pop summary::-webkit-details-marker { display: none; }
|
||||
.search-pop form {
|
||||
position: absolute; left: 0; right: 0; top: 100%;
|
||||
display: flex; gap: 8px; padding: 10px var(--gutter) 14px;
|
||||
background: var(--galaxy); border-top: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: var(--shadow-lifted);
|
||||
}
|
||||
.search-pop input {
|
||||
flex: 1; min-width: 0; font: inherit; color: var(--text);
|
||||
background: #fff; border: 1px solid var(--border); border-radius: var(--radius-pill);
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.search-pop input:focus { border-color: var(--azure); }
|
||||
.search-pop .btn { padding: 10px 18px; }
|
||||
.search-pop .suggest { left: 10px; right: 10px; top: calc(100% + 4px); }
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 10px; margin-right: 10px; }
|
||||
.brand img { height: 26px; width: auto; }
|
||||
.brand .brand-sub {
|
||||
color: var(--cherenkov); font-size: 0.78rem; font-weight: 500;
|
||||
border-left: 1px solid rgba(255,255,255,0.25); padding-left: 10px;
|
||||
@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; }
|
||||
}
|
||||
.nav .spacer { flex: 1; }
|
||||
.nav a.navlink {
|
||||
color: var(--on-galaxy-muted); font-weight: 500; font-size: 0.95rem;
|
||||
padding: 10px 10px; text-decoration: none; border-bottom: 2px solid transparent;
|
||||
@media (max-width: 360px) {
|
||||
.nav { padding: 0 14px; gap: 2px; }
|
||||
.brand { margin-right: 4px; }
|
||||
.brand img { height: 20px; }
|
||||
.nav .login-mock { padding: 8px 12px; }
|
||||
}
|
||||
.nav a.navlink:hover { color: #fff; }
|
||||
.nav a.navlink[aria-current="page"] { color: #fff; border-bottom-color: var(--cherenkov); }
|
||||
.nav a.navlink--edit {
|
||||
color: #fff; border: 1px solid rgba(255,255,255,0.35); border-radius: var(--radius-pill);
|
||||
padding: 7px 16px; margin-left: 6px;
|
||||
}
|
||||
.nav a.navlink--edit:hover { border-color: var(--cherenkov); color: var(--cherenkov); }
|
||||
/* AAI sign-in entry — leads to the /login mock page */
|
||||
.nav .login-mock {
|
||||
color: var(--galaxy); background: var(--cherenkov); font: inherit; font-weight: 600;
|
||||
border: 0; border-radius: var(--radius-pill); padding: 8px 18px; margin-left: 6px;
|
||||
text-decoration: none; white-space: nowrap;
|
||||
}
|
||||
.nav .login-mock:hover { background: var(--cherenkov-hover); color: var(--galaxy); }
|
||||
/* Tap targets ≥44px (WCAG 2.5.5); inline-flex keeps the active bottom border */
|
||||
.nav a.navlink, .nav .login-mock, .nav a.navlink--edit {
|
||||
display: inline-flex; align-items: center; gap: 6px; min-height: 44px;
|
||||
}
|
||||
.navlink--edit .ico { width: 15px; height: 15px; }
|
||||
/* External-link marker — SVG arrow via mask, inherits currentColor */
|
||||
.external::after {
|
||||
content: ""; display: inline-block; width: 0.72em; height: 0.72em; margin-left: 3px;
|
||||
@@ -215,16 +275,26 @@ img { max-width: 100%; height: auto; display: block; }
|
||||
letter-spacing: 0.14em; font-size: 0.72rem; margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Search fields — large pill input with inline magnifier; real GET form to /search */
|
||||
.hero-search, .search-form { position: relative; display: flex; gap: 8px; max-width: 560px; }
|
||||
.hero-search { margin-top: var(--space-m); }
|
||||
/* Hero CTAs — the two core scientist tasks (SPEC §3.1): one Cherenkov primary,
|
||||
one quiet ghost. Links styled as buttons because they navigate. */
|
||||
.hero-cta { display: flex; flex-wrap: wrap; gap: var(--space-s); margin-top: var(--space-m); }
|
||||
a.btn { display: inline-flex; align-items: center; gap: 6px; text-decoration: none; }
|
||||
.btn--ghost { background: transparent; color: #fff; border: 1px solid rgba(255, 255, 255, 0.55); }
|
||||
.btn--ghost:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
||||
/* One-line latest-news teaser under the CTAs (editorial touch, no widgets) */
|
||||
.hero .hero-teaser { margin-top: var(--space-m); font-size: 0.9rem; color: var(--on-galaxy-muted); max-width: none; }
|
||||
.hero .hero-teaser a { color: #fff; text-underline-offset: 3px; }
|
||||
.hero .hero-teaser a:hover { color: var(--cherenkov); }
|
||||
|
||||
/* 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; }
|
||||
.hero-search input, .search-form input {
|
||||
.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;
|
||||
}
|
||||
.hero-search input:focus, .search-form input:focus { border-color: var(--azure); }
|
||||
.search-form input:focus { border-color: var(--azure); }
|
||||
/* Live suggestions under the hero search — plain links, Tab-accessible */
|
||||
.suggest {
|
||||
position: absolute; top: calc(100% + 8px); left: 0; right: 0;
|
||||
@@ -235,14 +305,19 @@ img { max-width: 100%; height: auto; display: block; }
|
||||
max-height: min(60vh, 480px); overflow: auto; /* long lists scroll on mobile */
|
||||
}
|
||||
.suggest:empty { display: none; }
|
||||
.suggest li { display: flex; align-items: center; gap: var(--space-s); padding: 0 12px; border-radius: calc(var(--radius) - 8px); }
|
||||
.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); }
|
||||
/* 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; display: flex; align-items: center; min-height: 44px;
|
||||
flex: 1; min-width: 0; display: block; padding-block: 10px;
|
||||
color: var(--text); font-weight: 500; font-size: 0.94rem; text-decoration: none;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.suggest small { margin-top: 12px; } /* date chip tracks the first title line */
|
||||
.suggest::after {
|
||||
content: "↵ Enter — all results"; display: block; margin-top: 4px;
|
||||
content: "Press Enter for all results"; display: block; margin-top: 4px;
|
||||
padding: 8px 12px 2px; border-top: 1px solid var(--border);
|
||||
color: var(--muted); font-size: 0.78rem;
|
||||
}
|
||||
@@ -261,10 +336,10 @@ img { max-width: 100%; height: auto; display: block; }
|
||||
.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/cat/title/desc/meta rows align across each card row */
|
||||
/* 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 5; gap: 0; }
|
||||
.grid > .card:not(.card--featured, .tile) .body { display: grid; grid-template-rows: subgrid; grid-row: span 4; gap: 10px; }
|
||||
.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 + category + clamped title/description + <time> meta;
|
||||
@@ -380,6 +455,9 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
|
||||
}
|
||||
}
|
||||
|
||||
/* Long unbroken strings (URLs, identifiers) wrap instead of forcing page overflow */
|
||||
.prose { overflow-wrap: break-word; }
|
||||
.prose a { overflow-wrap: anywhere; }
|
||||
.prose h2 { color: var(--galaxy); margin: 34px 0 10px; font-size: clamp(1.25rem, 2.6vw, 1.45rem); }
|
||||
.prose h3 { color: var(--galaxy); margin: 26px 0 8px; font-size: 1.15rem; }
|
||||
.prose p { margin: 0 0 16px; }
|
||||
@@ -392,11 +470,30 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
|
||||
.prose img { border: 1px solid var(--border); border-radius: var(--radius); }
|
||||
.prose img + em, .prose p > em:only-child { color: var(--muted); font-size: 0.85rem; }
|
||||
.prose hr { border: 0; border-top: 1px solid var(--border); margin: 32px 0; }
|
||||
.prose table { width: 100%; border-collapse: collapse; margin: 0 0 16px; font-size: 0.94rem; }
|
||||
/* Wide tables scroll inside their own box, never the page (mobile guard) */
|
||||
.prose table { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; margin: 0 0 16px; font-size: 0.94rem; }
|
||||
.prose th { text-align: left; font-weight: 600; color: var(--galaxy); }
|
||||
.prose th, .prose td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||||
.prose thead th { border-bottom: 2px solid var(--cherenkov); }
|
||||
|
||||
/* 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: 0.72rem; 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: var(--space-xl) 0; }
|
||||
.panel {
|
||||
@@ -422,29 +519,34 @@ span.page-step { color: var(--muted); } /* disabled end stop — non-interactive
|
||||
/* Search results (list built by the inline script on /search) — card-like rows */
|
||||
.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; justify-content: space-between; align-items: center; gap: var(--space-s);
|
||||
display: flex; align-items: flex-start; gap: var(--space-s);
|
||||
background: #fff; border: 1px solid var(--border); border-radius: var(--radius);
|
||||
padding: 12px 18px; box-shadow: var(--shadow-ambient);
|
||||
}
|
||||
.search-results li:hover { border-color: var(--azure); box-shadow: var(--shadow-lifted); }
|
||||
.search-results a { text-decoration: none; font-weight: 500; }
|
||||
/* Title flows as inline text (never display:flex — see .suggest a note) */
|
||||
.search-results a { flex: 1; min-width: 0; overflow-wrap: break-word; text-decoration: none; font-weight: 500; }
|
||||
.search-results a:hover { text-decoration: underline; }
|
||||
/* Date chips on results and suggestions */
|
||||
.search-results small, .suggest small {
|
||||
color: var(--muted); white-space: nowrap; font-size: 0.78rem;
|
||||
background: var(--moon); border-radius: var(--radius-pill); padding: 2px 10px;
|
||||
}
|
||||
.search-results small { margin-top: 3px; } /* optical align with the first title line */
|
||||
/* Matched-term highlight in results/suggestions (Starlight search pattern) —
|
||||
brand tint only, no new hues */
|
||||
mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(var(--radius) - 12px); padding: 0 2px; }
|
||||
|
||||
/* Mock/status badges — every mock must be labelled (REQUIREMENTS.md §5) */
|
||||
/* Mock/status badges — every mock stays labelled (REQUIREMENTS.md §5), but the
|
||||
Demo phase banner is loud once, so per-element badges only whisper: quiet
|
||||
hairline pill, muted text (7.1:1 on Moon), sentence case, Galaxy dot on mocks. */
|
||||
.badge-mock, .badge-ext {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.06em;
|
||||
text-transform: uppercase; padding: 4px 10px; border-radius: var(--radius-pill); margin-bottom: 14px;
|
||||
display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
|
||||
font-size: 0.75rem; 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 { color: #7a3d00; background: #fff3e0; border: 1px solid #f0cf9b; }
|
||||
.badge-ext { color: var(--galaxy); background: var(--moon); border: 1px solid var(--border); text-transform: none; letter-spacing: 0; }
|
||||
.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); }
|
||||
@@ -534,6 +636,7 @@ mark { background: var(--tint-cherenkov); color: inherit; border-radius: calc(va
|
||||
.hero > :nth-child(2) { animation-delay: 0.08s; }
|
||||
.hero > :nth-child(3) { animation-delay: 0.16s; }
|
||||
.hero > :nth-child(4) { animation-delay: 0.24s; }
|
||||
.hero > :nth-child(5) { animation-delay: 0.32s; }
|
||||
/* Hover feedback */
|
||||
.card, .search-results li { transition: border-color 0.15s ease, box-shadow 0.2s ease, transform 0.2s ease; }
|
||||
.card:hover { transform: translateY(-2px); }
|
||||
|
||||
Reference in New Issue
Block a user