From cf35739a645aa19eb76651f0b0273d5a4c15667d Mon Sep 17 00:00:00 2001 From: ctao Date: Fri, 24 Jul 2026 23:00:07 +0200 Subject: [PATCH] feat: portal IA per spec (home entry point, /news, /proposals+redirect, /support), live search + suggestions, article TOC, mobile disclosure menu, CMS preview styles, modernization pass 1 --- astro.config.mjs | 1 + public/admin/preview.css | 24 ++++ public/brand/CTAO_Logo_login.svg | 143 +++------------------ public/search-client.js | 33 +++++ src/layouts/Base.astro | 35 ++++-- src/pages/admin/index.astro | 17 +++ src/pages/dashboard.astro | 74 +++++++++-- src/pages/index.astro | 63 ++++++++-- src/pages/login.astro | 17 +++ src/pages/news/[slug].astro | 21 +++- src/pages/news/index.astro | 39 ++++++ src/pages/proposals.astro | 49 ++++++++ src/pages/search.astro | 33 +++++ src/pages/search.json.js | 19 +++ src/pages/support.astro | 32 +++++ src/pages/telescopes.astro | 49 -------- src/styles/global.css | 209 +++++++++++++++++++++++++++---- 17 files changed, 627 insertions(+), 231 deletions(-) create mode 100644 public/admin/preview.css create mode 100644 public/search-client.js create mode 100644 src/pages/login.astro create mode 100644 src/pages/news/index.astro create mode 100644 src/pages/proposals.astro create mode 100644 src/pages/search.astro create mode 100644 src/pages/search.json.js create mode 100644 src/pages/support.astro delete mode 100644 src/pages/telescopes.astro diff --git a/astro.config.mjs b/astro.config.mjs index 2cfe1fb..066b53a 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -3,6 +3,7 @@ import { defineConfig } from 'astro/config'; // Static output (zero runtime) — the whole point of the git-based approach. export default defineConfig({ output: 'static', + redirects: { '/telescopes': '/proposals' }, server: { port: 4321, host: true }, vite: { server: { diff --git a/public/admin/preview.css b/public/admin/preview.css new file mode 100644 index 0000000..990e80c --- /dev/null +++ b/public/admin/preview.css @@ -0,0 +1,24 @@ +/* Entry-preview styles for the Sveltia editor (registered via the officially + supported CMS.registerPreviewStyle API). Mirrors the portal's article look + so editors see roughly what readers will see. Kept intentionally tiny. */ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=Space+Grotesk:wght@500..700&display=swap"); + +body { + font-family: "Inter", Arial, system-ui, sans-serif; + color: #101228; + line-height: 1.65; + max-width: 70ch; + margin: 0 auto; + padding: 24px; +} +h1, h2 { font-family: "Space Grotesk", "Inter", sans-serif; color: #00004a; } +h3, h4 { color: #00004a; } +a { color: #0057c2; } +img { max-width: 100%; height: auto; border-radius: 16px; } +blockquote { + margin: 22px 0; padding: 12px 20px; + border-left: 3px solid #00e4d8; background: #f5f5f5; +} +table { width: 100%; border-collapse: collapse; } +th, td { padding: 10px 12px; border-bottom: 1px solid #e2e5ee; text-align: left; } +thead th { border-bottom: 2px solid #00e4d8; color: #00004a; } diff --git a/public/brand/CTAO_Logo_login.svg b/public/brand/CTAO_Logo_login.svg index 02c0edd..5fbfc48 100644 --- a/public/brand/CTAO_Logo_login.svg +++ b/public/brand/CTAO_Logo_login.svg @@ -1,129 +1,18 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - \ No newline at end of file diff --git a/public/search-client.js b/public/search-client.js new file mode 100644 index 0000000..fb37325 --- /dev/null +++ b/public/search-client.js @@ -0,0 +1,33 @@ +// Shared client-side news search over /search.json (no dependencies, no modules). +// Used by the homepage hero (live suggestions) and /search (full results). +(function () { + window.newsSearch = function (opts) { + const input = document.getElementById(opts.input); + const list = document.getElementById(opts.list); + const status = document.getElementById(opts.status); + let index = null; + let timer; + async function run() { + const q = input.value.trim().toLowerCase(); + list.innerHTML = ''; + if (q.length < 2) { status.textContent = opts.hint || ''; return; } + if (!index) index = await (await fetch('/search.json')).json(); + const hits = index + .filter((p) => (p.title + ' ' + p.description + ' ' + p.category).toLowerCase().includes(q)) + .slice(0, opts.limit); + status.textContent = hits.length ? hits.length + ' ' + (opts.unit || 'result(s)') : 'No results.'; + for (const p of hits) { + const li = document.createElement('li'); + const a = document.createElement('a'); + a.href = '/news/' + encodeURIComponent(p.slug); + a.textContent = p.title; + const small = document.createElement('small'); + small.textContent = p.date; + li.append(a, small); + list.append(li); + } + } + input.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(run, 150); }); + return run; // callers may run() immediately (e.g. /search?q=…) + }; +})(); diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 6112c75..95282ad 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -6,9 +6,11 @@ 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. const links = [ - { href: '/', label: 'News' }, - { href: '/telescopes', label: 'Proposals' }, + { href: '/news', label: 'News' }, + { href: '/proposals', label: 'Proposals' }, { href: '/dashboard', label: 'Dashboard' }, + { href: 'https://padc-ctao-data-explorer.obspm.fr/', label: 'Data', ext: true }, + { href: '/support', label: 'Support' }, ]; --- @@ -18,6 +20,7 @@ const links = [ {title} + {/* Brand typography (BRAND D.3): Inter (body) + Space Grotesk (headlines). Google Fonts for the demo; self-host woff2 for production. */} @@ -32,15 +35,30 @@ const links = [ CTAO Science Portal - - ✎ Editor - + + Search + ✎ Editor + Sign in + {/* Mobile disclosure menu — native
/, no JS; desktop hides it via CSS */} +
@@ -50,11 +68,12 @@ const links = [ {/* Bottom menu required by SPEC §3.3.1: disclaimers, privacy, contact, settings, search. Placeholder links = without href (valid HTML placeholder-link semantics). */}
+ Disclaimer Privacy Contact Site settings - Search + Search Science Portal Core demo (Cyfronet / SUSS-PORT team). Sample content imported from ctao.org; footer links are placeholders. Statically generated from Markdown (git-based CMS). diff --git a/src/pages/admin/index.astro b/src/pages/admin/index.astro index 42aae28..572a1b1 100644 --- a/src/pages/admin/index.astro +++ b/src/pages/admin/index.astro @@ -10,8 +10,25 @@ CTAO — Content Editor + + + + + diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index 5c00352..64d051b 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -1,17 +1,71 @@ --- import Base from '../layouts/Base.astro'; + +// Sample data, clearly labelled as mock in the UI. Per SPEC §3.3.2 the dashboard +// aggregates: proposal statuses, scheduling info, data products, help-desk tickets. +const proposals = [ + { id: 'PROP-2026-0142', title: 'AGN flare monitoring with LST sub-array', status: 'In review', kind: 'review' }, + { id: 'PROP-2026-0089', title: 'Galactic PeVatron survey follow-up', status: 'Accepted', kind: 'ok' }, + { id: 'PROP-2025-0231', title: 'GRB afterglow ToO programme', status: 'Draft', kind: 'draft' }, +]; +const products = [ + { id: 'OBS-88213', desc: 'DL3 event lists — PKS 2155-304 (12 runs)', date: '2026-07-18' }, + { id: 'OBS-87990', desc: 'DL3 event lists — Crab Nebula calibration', date: '2026-07-02' }, +]; +const fmt = (iso) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium' }).format(new Date(iso)); +const tickets = [ + { id: '#4821', subject: 'Proposal form: co-I affiliation not saved', status: 'Answered', kind: 'ok' }, + { id: '#4711', subject: 'Data download quota question', status: 'Open', kind: 'review' }, +]; --- - + +
+
+
User area
+

Dashboard

+

Your proposals, data products and support tickets in one place

+
+
+
- Makieta — dane przykładowe -
-
-
📡
-

Dashboard użytkownika

-

Wg specyfikacji SUSS: status wniosków, harmonogramy, produkty danych i zgłoszenia - helpdesku użytkownika — zbierane z pozostałych serwisów.
- To część dynamiczna (nasz zakres, Release 5), utrzymywana osobno od - statycznych treści. Wszystko na tej stronie jest makietą.

+ Mock — sample data, no live services connected +
+
+

My proposals

+
    + {proposals.map((p) => ( +
  • + {p.id} + {p.title} + {p.status} +
  • + ))} +
+
+
+

Recent data products

+
    + {products.map((d) => ( +
  • + {d.id} + {d.desc} + +
  • + ))} +
+

Search and download in the Data Explorer (external, LUX team).

+
+
+

Help-desk tickets

+
    + {tickets.map((t) => ( +
  • + {t.id} + {t.subject} + {t.status} +
  • + ))} +
diff --git a/src/pages/index.astro b/src/pages/index.astro index 64364ab..f073756 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -2,29 +2,70 @@ import { getCollection } from 'astro:content'; import Base from '../layouts/Base.astro'; -const posts = (await getCollection('news', ({ data }) => !data.draft)).sort( - (a, b) => b.data.date.valueOf() - a.data.date.valueOf(), -); +const posts = (await getCollection('news', ({ data }) => !data.draft)) + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) + .slice(0, 3); const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d); const iso = (d) => d.toISOString().slice(0, 10); +// 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' }, + { title: 'Data Explorer', href: 'https://padc-ctao-data-explorer.obspm.fr/', ext: true, desc: 'Search and download CTAO science data products.', badge: 'ext' }, + { title: 'Dashboard', href: '/dashboard', desc: 'Your proposals, data products and support tickets in one place.', badge: 'mock' }, + { title: 'User Support', href: '/support', desc: 'Help desk, FAQ, user forum and mailing lists.', badge: 'mock' }, + { title: 'Software', desc: 'Science analysis tools and pipelines.', badge: 'tbd' }, + { title: 'Documentation', desc: 'User guides and technical documentation.', badge: 'tbd' }, +]; --- - +
Cherenkov Telescope Array Observatory
-

News & Announcements

+

CTAO Science Portal

Exploring the Universe at the Highest Energies

+
-
+ + + +
+

Services

- {posts.map((post, i) => ( -
- {post.data.cover && ( - - )} + {services.map((s) => ( +
+

{s.href + ? (s.ext + ? {s.title} + : {s.title}) + : s.title}

+

{s.desc}

+ {s.badge === 'mock' && Mock} + {s.badge === 'ext' && External — LUX team} + {s.badge === 'tbd' && Planned (TBD)} +
+ ))} +
+ +
+

Latest news

+ All news → +
+
+ {posts.map((post) => ( +
+ {post.data.cover && }
{post.data.category}

{post.data.title}

diff --git a/src/pages/login.astro b/src/pages/login.astro new file mode 100644 index 0000000..6256e51 --- /dev/null +++ b/src/pages/login.astro @@ -0,0 +1,17 @@ +--- +import Base from '../layouts/Base.astro'; +--- + +
+
+ + Mock — AAI integration pending +

Sign in

+

One account for all CTAO services. Authentication is handled by the + central CTAO AAI (single sign-on) — the portal never sees your password.

+ +

No account yet? Register via CTAO AAI · Access level follows + your CTAO role and permissions.

+
+
+ diff --git a/src/pages/news/[slug].astro b/src/pages/news/[slug].astro index fa2154d..f3090bf 100644 --- a/src/pages/news/[slug].astro +++ b/src/pages/news/[slug].astro @@ -8,15 +8,26 @@ export async function getStaticPaths() { } const { post } = Astro.props; -const { Content } = await render(post); -const fmt = (d) => new Intl.DateTimeFormat('pl-PL', { dateStyle: 'long' }).format(d); +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); ---
- ← Wszystkie newsy + ← All news

{post.data.title}

-
{post.data.category} · {post.data.author} · {fmt(post.data.date)}
- {post.data.cover && } +
{post.data.category} · {post.data.author} ·
+ {post.data.cover && } + {toc.length >= 3 && ( + + )}
diff --git a/src/pages/news/index.astro b/src/pages/news/index.astro new file mode 100644 index 0000000..63a32b3 --- /dev/null +++ b/src/pages/news/index.astro @@ -0,0 +1,39 @@ +--- +import { getCollection } from 'astro:content'; +import Base from '../../layouts/Base.astro'; + +const posts = (await getCollection('news', ({ data }) => !data.draft)).sort( + (a, b) => b.data.date.valueOf() - a.data.date.valueOf(), +); +const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d); +const iso = (d) => d.toISOString().slice(0, 10); +--- + +
+ +
+
Cherenkov Telescope Array Observatory
+

News & Announcements

+

Exploring the Universe at the Highest Energies

+
+
+ +
+

All news

+
+ {posts.map((post, i) => ( +
+ {post.data.cover && ( + + )} +
+ {post.data.category} +

{post.data.title}

+

{post.data.description}

+
{post.data.author} ·
+
+
+ ))} +
+
+ diff --git a/src/pages/proposals.astro b/src/pages/proposals.astro new file mode 100644 index 0000000..b40570c --- /dev/null +++ b/src/pages/proposals.astro @@ -0,0 +1,49 @@ +--- +import Base from '../layouts/Base.astro'; +--- + +
+
+
Proposal Submission
+

Observation proposals

+

Submitting observation proposals, including ToO / MWL requests

+
+
+ +
+ Mock — no submission logic + Target: Proposal Handling System (APC team) integrated into the portal +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Demonstration mock — the form does not store any data.

+
+
+ diff --git a/src/pages/search.astro b/src/pages/search.astro new file mode 100644 index 0000000..454dae9 --- /dev/null +++ b/src/pages/search.astro @@ -0,0 +1,33 @@ +--- +import Base from '../layouts/Base.astro'; +--- + +
+
+
Science Portal
+

Search

+

Find news and announcements

+
+
+ +
+ +

Type to search the news archive.

+
    + +
    + + + + diff --git a/src/pages/search.json.js b/src/pages/search.json.js new file mode 100644 index 0000000..1dce58e --- /dev/null +++ b/src/pages/search.json.js @@ -0,0 +1,19 @@ +// Build-time search index (Astro static endpoint — no server, no dependencies). +// Kept lean: title/description/category/date/slug only (~60 KB for 200+ articles). +import { getCollection } from 'astro:content'; + +export async function GET() { + const posts = await getCollection('news', ({ data }) => !data.draft); + const index = posts + .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) + .map((p) => ({ + slug: p.id, + title: p.data.title, + description: p.data.description, + category: p.data.category, + date: p.data.date.toISOString().slice(0, 10), + })); + return new Response(JSON.stringify(index), { + headers: { 'Content-Type': 'application/json; charset=utf-8' }, + }); +} diff --git a/src/pages/support.astro b/src/pages/support.astro new file mode 100644 index 0000000..ba3001b --- /dev/null +++ b/src/pages/support.astro @@ -0,0 +1,32 @@ +--- +import Base from '../layouts/Base.astro'; +// User support channels per the Science Portal spec — all mock for now. +const channels = [ + { title: 'Help desk', desc: 'Submit and track support tickets.' }, + { title: 'FAQ & troubleshooting', desc: 'Common questions and step-by-step guides.' }, + { title: 'User forum', desc: 'Discuss with the CTAO user community.' }, + { title: 'Mailing lists', desc: 'Announcements and topical mailing lists.' }, +]; +--- + +
    +
    +
    User services
    +

    User Support

    +

    Help desk, FAQ / troubleshooting guide, user forum and mailing lists

    +
    +
    + +
    + Mock — support services not yet connected +
    + {channels.map((c) => ( +
    +

    {c.title}

    +

    {c.desc}

    + Mock +
    + ))} +
    +
    + diff --git a/src/pages/telescopes.astro b/src/pages/telescopes.astro deleted file mode 100644 index a9dac0c..0000000 --- a/src/pages/telescopes.astro +++ /dev/null @@ -1,49 +0,0 @@ ---- -import Base from '../layouts/Base.astro'; ---- - -
    -
    -
    Proposal Submission
    -

    Zgłoszenie obserwacji

    -

    Składanie wniosków obserwacyjnych (w tym ToO / MWL)

    -
    -
    - -
    - Makieta — bez logiki wysyłki - Docelowo: Proposal Handling System (team APC) zintegrowany w portalu -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - -

    Makieta demonstracyjna — formularz nie zapisuje danych.

    -
    -
    - diff --git a/src/styles/global.css b/src/styles/global.css index 3b42d11..e0fb379 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -25,10 +25,21 @@ --radius: 16px; --radius-pill: 999px; --maxw: 1120px; + /* Spacing scale (4px base) — use these instead of ad-hoc paddings */ + --space-s: 12px; + --space-m: 20px; + --space-l: 32px; + --space-xl: 56px; +} + +/* 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%; } +html { -webkit-text-size-adjust: 100%; scroll-padding-top: 84px; /* sticky header + anchor jumps */ } body { margin: 0; font-family: var(--font-body); @@ -42,6 +53,8 @@ h1, h2 { font-family: var(--font-display); letter-spacing: -0.01em; } a { color: var(--link); text-decoration: underline; text-underline-offset: 3px; } a:hover { color: var(--indigo); } +/* Placeholder links ( 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) */ @@ -68,8 +81,39 @@ img { max-width: 100%; height: auto; display: block; } } /* Header — Galaxy Blue band, reverse (white) logo per BRAND B.1.2 */ -.site-header { background: var(--galaxy); } +.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: wrap; } +.nav > nav { display: flex; align-items: center; gap: 4px; } +/* Mobile: disclosure menu (native
    / — CSS-only, keyboard accessible) */ +.menu { display: none; } +@media (max-width: 720px) { + .nav { flex-wrap: nowrap; } + .nav > nav, .nav > a.navlink { display: none; } + .menu { display: block; } + .menu summary { + display: inline-flex; align-items: center; gap: 8px; min-height: 44px; + padding: 0 10px; margin-left: 6px; color: #fff; font-weight: 500; + cursor: pointer; list-style: none; white-space: nowrap; + } + .menu summary::-webkit-details-marker { display: none; } + .menu .menu-icon::before { content: "☰"; } + .menu[open] .menu-icon::before { content: "✕"; } + .menu nav { + position: absolute; left: 0; right: 0; top: 100%; + display: flex; flex-direction: column; padding: 6px 20px 14px; + background: var(--galaxy); border-top: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0 20px 32px rgba(0, 0, 74, 0.35); + } + .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"] { color: var(--cherenkov); } +} +/* Very narrow screens: drop the decorative sub-brand so logo/Sign in/Menu always fit */ +@media (max-width: 460px) { .brand .brand-sub { display: none; } } .brand { display: flex; align-items: center; gap: 10px; margin-right: 10px; } .brand img { height: 26px; width: auto; } .brand .brand-sub { @@ -88,10 +132,16 @@ img { max-width: 100%; height: auto; display: block; } padding: 7px 16px; margin-left: 6px; } .nav a.navlink--edit:hover { border-color: var(--cherenkov); color: var(--cherenkov); } -/* AAI login placeholder — visibly disabled (mock) */ +/* 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; cursor: not-allowed; + border: 0; border-radius: var(--radius-pill); padding: 8px 18px; margin-left: 6px; + text-decoration: none; white-space: nowrap; +} +.nav .login-mock:hover { filter: brightness(0.92); 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; min-height: 44px; } .external::after { content: " ↗"; font-size: 0.85em; } @@ -101,7 +151,7 @@ img { max-width: 100%; height: auto; display: block; } radial-gradient(700px 340px at 85% 115%, rgba(0,228,216,0.28), transparent 65%), radial-gradient(500px 260px at -5% -40%, rgba(0,0,156,0.55), transparent 60%), var(--galaxy); - color: #fff; padding: 56px 0 48px; position: relative; overflow: hidden; + color: #fff; padding: var(--space-xl) 0; position: relative; overflow: hidden; } .hero-band .flash { position: absolute; right: 8%; top: 22%; width: 22px; height: 22px; } .hero h1 { font-size: clamp(1.9rem, 5vw, 2.9rem); margin: 0 0 10px; line-height: 1.12; font-weight: 700; } @@ -111,11 +161,44 @@ img { max-width: 100%; height: auto; display: block; } letter-spacing: 0.14em; font-size: 0.72rem; margin-bottom: 8px; } -/* Section headings on light background */ -.section-title { color: var(--galaxy); font-size: 1.4rem; margin: 36px 0 4px; } +/* Hero search — real GET form to /search (works without JS) */ +.hero-search, .search-form { display: flex; gap: 8px; max-width: 560px; } +.hero-search { margin-top: var(--space-m); } +.hero-search 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: 11px 18px; +} +.hero-search input:focus, .search-form input:focus { border-color: var(--azure); } +/* Live suggestions under the hero search — plain links, Tab-accessible */ +.hero-search { position: relative; } +.suggest { + position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 5; + list-style: none; margin: 0; padding: 6px; text-align: left; + background: #fff; border: 1px solid var(--border); border-radius: var(--radius); + box-shadow: 0 16px 40px rgba(0, 0, 74, 0.25); +} +.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:hover { background: var(--moon); } +.suggest a { + flex: 1; display: flex; align-items: center; min-height: 44px; + color: var(--text); font-weight: 500; font-size: 0.94rem; text-decoration: none; +} +.suggest small { color: var(--muted); font-size: 0.8rem; white-space: nowrap; } -/* News grid */ -.grid { display: grid; gap: 16px; grid-template-columns: 1fr; padding: 28px 0 64px; } +/* 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: clamp(1.3rem, 2.5vw, 1.6rem); } +.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) */ +.grid { display: grid; gap: var(--space-m); grid-template-columns: 1fr; } @media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 940px) { .grid { grid-template-columns: repeat(3, 1fr); } } @@ -125,11 +208,18 @@ img { max-width: 100%; height: auto; display: block; } position: relative; background: #fff; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; - transition: border-color 0.15s ease; + transition: border-color 0.15s ease, box-shadow 0.2s ease; +} +.card:hover, .card:focus-within { + border-color: var(--azure); + box-shadow: 0 2px 6px rgba(0, 0, 74, 0.06), 0 12px 28px rgba(0, 0, 74, 0.10); +} +/* Visible focus ring for the whole-card stretched link (WCAG 2.4.7) */ +.card:focus-within { + box-shadow: 0 0 0 2px var(--azure), 0 2px 6px rgba(0, 0, 74, 0.06), 0 12px 28px rgba(0, 0, 74, 0.10); } -.card:hover, .card:focus-within { border-color: var(--azure); } .card .cover { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; } -.card .body { padding: 18px 20px 20px; display: flex; flex-direction: column; gap: 10px; flex: 1; } +.card .body { padding: var(--space-m); display: flex; flex-direction: column; gap: 10px; flex: 1; } .card .cat { align-self: flex-start; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600; @@ -141,8 +231,9 @@ img { max-width: 100%; height: auto; display: block; } display: -webkit-box; -webkit-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) */ -.card h3 a::after { content: ""; position: absolute; inset: 0; } +/* 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: 0.94rem; @@ -157,9 +248,20 @@ img { max-width: 100%; height: auto; display: block; } .card--featured h3 { font-size: 1.6rem; font-family: var(--font-display); -webkit-line-clamp: 3; } .card--featured .desc { -webkit-line-clamp: 3; } } +/* Hover lift + smooth anchor scroll — motion only when the user allows it */ +@media (prefers-reduced-motion: no-preference) { + html { scroll-behavior: smooth; } + .card { transition: border-color 0.15s ease, box-shadow 0.2s ease, transform 0.2s ease; } + .card:hover { transform: translateY(-2px); } +} + +/* Service tiles — card tokens, text-first */ +.tile { padding: var(--space-m); gap: 8px; } +.tile h3 { margin: 0; } +.tile .badge-mock, .tile .badge-ext { margin: auto 0 0; align-self: flex-start; } /* Article */ -.article { padding: 40px 0 72px; max-width: 720px; } +.article { padding: 40px 0 72px; max-width: 70ch; } .article .back { color: var(--muted); font-size: 0.9rem; } .article h1 { color: var(--galaxy); font-size: clamp(1.7rem, 4.5vw, 2.4rem); @@ -167,7 +269,22 @@ img { max-width: 100%; height: auto; display: block; } } .article .meta { color: var(--muted); font-size: 0.9rem; margin-bottom: 28px; } .article .cover { border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 28px; } -.prose h2 { color: var(--galaxy); margin: 34px 0 10px; font-size: 1.45rem; } +/* Article TOC — boxed "On this page" list (rendered only when ≥3 h2/h3) */ +.toc { + background: var(--moon); border: 1px solid var(--border); border-radius: var(--radius); + padding: var(--space-s) var(--space-m); margin-bottom: var(--space-l); font-size: 0.92rem; +} +.toc strong { + display: block; font-size: 0.72rem; text-transform: uppercase; + letter-spacing: 0.08em; color: var(--muted); margin-bottom: 4px; +} +.toc ol { list-style: none; margin: 0; padding: 0; } +.toc li { margin: 6px 0; } +.toc .d3 { padding-left: var(--space-s); } +.toc a { text-decoration: none; } +.toc a:hover { text-decoration: underline; } + +.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; } .prose ul, .prose ol { margin: 0 0 16px; padding-left: 22px; } @@ -186,7 +303,8 @@ img { max-width: 100%; height: auto; display: block; } /* Generic page / mockup */ .page { padding: 40px 0 72px; } -.panel { background: #fff; border: 1px solid var(--border); border-radius: var(--radius); padding: 26px; } +.panel { background: #fff; border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-m); } +.panel--narrow { max-width: 640px; } .form-row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; } .form-row label { font-size: 0.88rem; font-weight: 500; } .form-row input, .form-row select, .form-row textarea { @@ -202,19 +320,68 @@ img { max-width: 100%; height: auto; display: block; } .btn:hover { filter: brightness(0.92); } .notice { color: var(--muted); font-size: 0.85rem; margin-top: 10px; } +/* Search results (list built by the inline script on /search) */ +.search-results { list-style: none; margin: var(--space-m) 0 0; padding: 0; max-width: 720px; } +.search-results li { + display: flex; justify-content: space-between; align-items: baseline; + gap: var(--space-s); padding: 12px 0; border-bottom: 1px solid var(--border); +} +.search-results a { text-decoration: none; font-weight: 500; } +.search-results a:hover { text-decoration: underline; } +.search-results small { color: var(--muted); white-space: nowrap; } + /* Mock/status badges — every mock must be labelled (REQUIREMENTS.md §5) */ .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: 999px; 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); } +.badge-ext { color: var(--galaxy); background: var(--moon); border: 1px solid var(--border); text-transform: none; letter-spacing: 0; } -.placeholder { display: grid; place-items: center; min-height: 32vh; text-align: center; color: var(--muted); } -.placeholder .big { font-size: 3rem; margin-bottom: 8px; } +/* Dashboard mock — aggregation panels with status pills */ +.dash-grid { display: grid; gap: var(--space-m); grid-template-columns: 1fr; margin-top: var(--space-s); } +@media (min-width: 940px) { .dash-grid { grid-template-columns: repeat(2, 1fr); } } +.panel-title { color: var(--galaxy); font-size: 1.1rem; margin: 0 0 var(--space-s); font-family: var(--font-body), sans-serif; font-weight: 600; } +.rows { list-style: none; margin: 0; padding: 0; } +.rows li { + display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; + padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 0.92rem; +} +.rows li:last-child { border-bottom: 0; } +.row-id { color: var(--muted); font-size: 0.8rem; min-width: 7.5em; } +.row-main { flex: 1; min-width: 12em; } +.row-date { color: var(--muted); font-size: 0.8rem; } +.pill { + font-size: 0.72rem; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; + padding: 3px 10px; border-radius: var(--radius-pill); border: 1px solid var(--border); +} +.pill--ok { color: var(--galaxy); background: var(--tint-cherenkov); } +.pill--review { color: var(--galaxy); background: var(--moon); } +.pill--draft { color: var(--muted); background: #fff; } + +/* Auth (login mock) — nebula backdrop + floating card, modern auth pattern */ +.auth-band { + min-height: calc(100dvh - 64px); + display: grid; place-items: center; padding: var(--space-xl) 20px; + background: + radial-gradient(900px 500px at 80% 110%, rgba(0,228,216,0.35), transparent 60%), + radial-gradient(700px 420px at 10% -20%, rgba(0,0,156,0.6), transparent 60%), + var(--galaxy); +} +.auth-card { + background: #fff; border-radius: calc(var(--radius) + 8px); + padding: 40px 36px; max-width: 440px; width: 100%; + box-shadow: 0 24px 64px rgba(0, 0, 74, 0.35); +} +.auth-logo { height: 24px; width: auto; margin-bottom: var(--space-m); } +.auth-card h1 { color: var(--galaxy); margin: 6px 0 10px; font-size: 1.8rem; } +.auth-card .muted { color: var(--muted); font-size: 0.95rem; } +.btn-wide { width: 100%; margin-top: var(--space-s); } /* Footer — bottom menu per SPEC §3.3.1: disclaimers, privacy, contact, settings, search */ .footer { background: var(--galaxy); color: var(--on-galaxy-muted); font-size: 0.85rem; margin-top: 24px; } -.footer .inner { display: flex; flex-wrap: wrap; gap: 8px 22px; align-items: center; padding: 26px 0; } +.footer .inner { display: flex; flex-wrap: wrap; gap: 8px 22px; align-items: center; padding: var(--space-l) 0; } +.footer .flash { width: 10px; height: 10px; } .footer a { color: #fff; } +.footer a:not([href]) { color: var(--on-galaxy-muted); } .footer .note { width: 100%; margin-top: 6px; color: var(--on-galaxy-muted); }