Compare commits

..
80 Commits
Author SHA1 Message Date
ctao ab9e4857a4 handoff review fixes: deploy log removed from the repo (internal infra notes do not belong in the handover), diagrams moved to the content repo, pages glob non-recursive, dead badge/pill css and stale design-system pointers removed, node engines declared, README states access and node prerequisites, demo-only dev server settings dropped, audit clean 2026-09-06 15:36:06 +02:00
ctao bd95bacfc5 astro check restored: tsconfig excludes public/ (the 2 MB vendored CMS bundle was what exhausted the heap) and extends tsconfigs/strict; type annotations in template scripts, z from astro/zod; exact version pins; compressHTML true (lossless whitespace, no words glued to links) and explicit trailingSlash/build.format; README: upgrading section 2026-09-06 15:12:56 +02:00
ctao e8b47ac888 runbook: manual retry of a failed release (rm state/last-failed) 2026-09-04 09:08:20 +02:00
ctao 6ed5d5859a build.sh: recreate pruned parent dirs before the content overlay (checkout of the content-free tree removes empty src/content) 2026-09-03 19:50:43 +02:00
ctao 751fd0ec79 em dashes out of skeleton copy (titles use ' - ', prose reworded); split hardening after review: poll errors name the failing repo, failed release is not retried until a new commit, link-content checks all three dirs, editor write-access note, two-repo unit descriptions and comments 2026-09-03 19:41:56 +02:00
ctao 13f945fbf3 content split + copy audit + css polish: content (news/pages/uploads) moves to the ctao/content repo (build overlays it — two-repo build.sh, link-content.sh for local dev); portal copy trimmed to source-documents-only (dashboard empty states, minimal privacy/disclaimer, no mock badges); css: inverted selection on navy bands, featured-card text-track floor, 44px toc target, mobile search dates on own line, footer link row gap; DESIGN.md rewritten as a short handoff 2026-09-03 19:35:22 +02:00
ctao 2e6d374b4b article headlines: --fs-h2 instead of --fs-h1 (long news titles filled the viewport at 56px; editorial 32-40px convention) 2026-09-03 15:14:44 +02:00
ctao c4d21b59be review round 2: deterministic checks (npm run check: build + zero-dep link checker; tsconfig with docs-recommended include/exclude, astro check dropped — OOMs at 12GB on a 200-entry collection), sitemap static pages derived from route files, non-recursive content glob (id/slug trap), --header-h token, honest CDN comments (sveltia UI fonts), preview.css drift (prose a wrap, img block, h1 size) 2026-09-03 15:11:43 +02:00
ctao cec8018c18 handoff pass: vendored sveltia bundle, astro pinned ^7.2.10, root README + refreshed deploy runbook, review fixes (coverless card grid, shared NewsCard/lib, real site origin, nginx security headers, linger step, dead config removed, preview.css sync, tsconfig) 2026-09-03 14:56:53 +02:00
ctao c64528a1d3 cors: full-origin ALLOW_DOMAIN (no SCHEME key in 1.27) + Authorization in allowed headers 2026-07-29 10:47:02 +02:00
ctao 746b015e85 vhost switch: Sveltia backend + Gitea ROOT_URL to https ingress domains, CORS locked to the portal origin 2026-07-29 10:35:26 +02:00
ctao a74eef0e47 ui iteration: nav underline moved to ::before (restores external arrow on Data), card reveal tuned (entry 75%, 28px), reading progress 3px, permanent lifted header shadow; article honesty pass: app-in-path class line (Grav/Tina clarified), 'is Gitea a second CMS' section, no-VPN claim removed 2026-07-29 07:55:22 +02:00
ctao c6de06f017 fix: esbuild css minifier — lightningcss folds animation-timeline into the animation shorthand (invalid), silently killing scroll-driven effects in production 2026-07-28 21:25:59 +02:00
ctao bbbb090cce ui polish pass: scroll-driven effects (card reveal, header shadow, reading progress), nav underline-grow, card meta icons, og:image svg fallback, apple-touch-icon+favicon, hero 472->180KB + preload, article cover CLS guard, ::selection, per-article lang, robots noindex (demo); article: db-cms comparison diagram, 6-year pros/cons, reuse section 2026-07-28 20:57:51 +02:00
ctao 54de9d3ee9 content: demo article tone pass (neutral wording) 2026-07-28 20:08:36 +02:00
ctao f6340a89b3 article meta: inline icons (tag/author/date/read-time) in the existing .ico stroke family 2026-07-28 20:06:49 +02:00
ctao 93712a1481 content: demo article — custom SVG diagrams (machine architecture, publish flow, attack surface) + cover, dated to sit second on the list 2026-07-28 20:03:44 +02:00
ctao 71b9808a11 content: demo presentation article (architecture, links, static-vs-database comparison) 2026-07-28 19:53:25 +02:00
ctao c0edb2033d cms: 1:1 article preview (mirror .prose tokens/rules), webp upload transformations to keep repo lean 2026-07-28 19:49:37 +02:00
ctao 145490fe60 Update Article “the-ctao-eric-council-visits-ctao-north-in-la-palma” 2026-07-28 17:37:51 +00:00
ctao 48fa17c351 Update Article “the-ctao-eric-council-visits-ctao-north-in-la-palma” 2026-07-28 17:36:58 +00:00
ctao ba629e2a96 cms: oauth-only sign-in (documented auth_methods), note on localhost-only local-repo button 2026-07-28 19:33:51 +02:00
ctao d06c2cf0c0 cms: CTAO branding on sign-in (app_title, logo.src instead of deprecated logo_url) 2026-07-28 19:29:04 +02:00
ctao c0d06f4a17 fix: /admin redirect loses port behind tunnel/proxy (absolute_redirect off, trailing slash in footer link) 2026-07-28 19:24:34 +02:00
ctao 99f5601456 deploy log: OAuth app via API, Sveltia pointed at machine Gitea, editor-cycle latency measured (12s/21s) 2026-07-28 18:35:41 +02:00
ctao 29385a5ca7 content: remove latency-test article 2026-07-28 16:31:58 +00:00
ctao 5744c16a1c content: simulate a Sveltia editor save (test) 2026-07-28 16:30:34 +00:00
ctao bc0201252d cms: point Sveltia at the machine Gitea (localhost:3000, machine OAuth app_id) 2026-07-28 18:24:40 +02:00
ctao 46ec5e2e3c deploy: npm ci --ignore-scripts (kills postinstall vector + esbuild ETXTBSY race); persistent npm cache; deployment log 2026-07-28 15:39:00 +02:00
ctao 80d886c0cb deploy: join gitea netns instead of bridge (machine kernel lacks ip_tables for rootless netavark); pin image digests; push-to-create repo 2026-07-28 14:07:44 +02:00
ctaoandClaude Fable 5 ed7a9c01e7 deploy: machine deployment — quadlets (gitea, nginx, isolated build network), poll+build pipeline, runbook
Reviewed (Opus security review, all findings verified) and tested end-to-end
locally: cold build 23s, no-op poll 45ms, incremental rebuild 4s, prune,
gitea-down grace, real bridge network with container DNS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jWfn3RwPfFGTtBddm36Uy
2026-07-28 13:44:46 +02:00
ctao 4102974927 test: trigger deploy pipeline (empty commit) 2026-07-28 13:33:05 +02:00
ctao beb585e5dc fix: eyebrow/UI font jump — Inter inlined too; zero font swaps site-wide 2026-07-26 00:18:49 +02:00
ctao 47902f7278 fix: hero headline font jump on load — Space Grotesk inlined as data URI in the stylesheet (renders correctly from first paint; no swap-triggered balance reflow) 2026-07-26 00:12:28 +02:00
ctao/demoandClaude Fable 5 0bb378e0cc fix: typing-in-header scroll jump — global scroll-padding-top made Chrome's per-keystroke caret reveal walk the page to 0; offset moved to anchor targets (main [id] scroll-margin-top)
Chrome re-scrolls the focused editable into view on every keystroke and
honours the viewport's scroll-padding-top while ignoring the editable's
own scroll-margin (crbug.com/41492445) — so the sticky-header search
field, which sits above the 76px line and can never be scrolled below
it, dragged the page toward 0 one keystroke at a time, and the previous
negative-scroll-margin counter could not work. CDP harness bisection:
of all suspects (smooth scroll, sticky header, overflow clip,
animations, ambient layer, suggest siblings) only removing the global
scroll-padding stops the jump. TOC/skip anchor offset now lives as
scroll-margin-top on the in-content targets; harness confirms scrollY
constant while typing and anchors still land below the header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jWfn3RwPfFGTtBddm36Uy
2026-07-25 23:56:05 +02:00
ctao 694b7d2003 fix: page jumped to top while typing in header search — caret-visibility scrolling honours scroll-padding-top; negative scroll-margin on header inputs cancels it (suggestions re-enabled) 2026-07-25 18:59:57 +02:00
ctao 61f5d8444f fix: scroll jumps while typing — overflow-anchor:none on live search lists (Chrome scroll anchoring) 2026-07-25 18:50:54 +02:00
ctao affa3262ef fix: search scroll jump (atomic list swap, no clear-before-await); services grid back to 6 tiles (2 subdued planned) + planned line for the rest 2026-07-25 18:33:06 +02:00
ctao 1ef2cab89e fix: navigation blink root-caused — defer search-client.js (was parser-blocking mid-body), npm run demo serves the production build through the tunnel; DESIGN.md documents the VT removal + font-swap decision 2026-07-25 17:48:48 +02:00
ctao c9161af655 style: login card refined per guidebook — SG-medium h2 headline, D.3.4 hierarchy, logo min-size, token rhythm 2026-07-25 17:35:28 +02:00
ctao d0eeedb62b Revert "feat: login rewritten — split-screen auth (white form column + living aurora pane, tagline), 2026 convention"
This reverts commit 185380ffa5.
2026-07-25 17:25:42 +02:00
ctao 185380ffa5 feat: login rewritten — split-screen auth (white form column + living aurora pane, tagline), 2026 convention 2026-07-25 17:03:40 +02:00
ctao e7c804c9da fix: zero font jumping on navigation — preload both brand fonts + font-display: optional (no late swaps; brand-approved Arial if cold) 2026-07-25 15:36:31 +02:00
ctao 075d44dce3 feat: hero photo — LST-1 under the Milky Way (full-res original fetched from ctao.org) 2026-07-25 15:17:33 +02:00
ctao 6e3f8e23b6 style: one protagonist per surface — photo hero keeps Ken Burns only (flares removed), login gets Gemini-style large soft aurora; services trimmed to 4 real tiles + planned line 2026-07-25 14:55:01 +02:00
ctao 95f8f7b1a6 fix: lava-lamp blobs — defined 30% cores, alpha .5-.6, screen blend, 9s cycle, wide travel (motion objectively 12x stronger: 0.6%->7.6% px delta) 2026-07-25 14:44:23 +02:00
ctao 4082be0c37 fix: hero motion clearly perceptible — Ken Burns on the photo layer, glow opacity pulse, faster cycles (12s/26s) 2026-07-25 14:21:47 +02:00
ctao 97d1a0aa43 content: unpublish test article (will be recreated live during the demo) 2026-07-25 13:45:10 +02:00
ctao 3362b214c3 fix: final-review findings — own-lead descriptions, PHS integration landing, planned tiles, 404, sitemap math, cleanups 2026-07-25 13:13:27 +02:00
ctao ffe54bc766 fix: light dismiss for header menu/search overlays (outside tap closes, never activates) 2026-07-25 12:33:09 +02:00
ctaoandClaude Fable 5 4fa35e5317 fix: mobile gutters, header search tiers, suggest wiring + empty-state, news pagination, hero CTAs, ghost button
- padding-block on .page/.page-head/.article: container gutter survived only >=1176px (shorthand padding zeroed it) — mobile content sat flush left
- restore <=1024px tiers: field hidden, magnifier + menu visible (header no longer overflows at 375px)
- re-wire hdr-q suggestions lost in the field restore; newsSearch inits null-safe
- focused-empty search state: recent queries (localStorage, cap 4) + latest articles (Baymard autocomplete guidance)
- suggest panel 480px (Baymard/DocSearch width convention)
- /news page 1 = 25 items (featured + 24 = even 3-col rows), later pages 24, custom slicing
- hero CTAs: primary Browse news (/news, spec-first real service), ghost Explore the data (external); mock Proposals out of hero
- .btn.btn--ghost compound selector — .btn base defined later was filling ghosts with Cherenkov

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jWfn3RwPfFGTtBddm36Uy
2026-07-25 12:16:41 +02:00
eaf511a976 feat: Apple-grade pass — scale, type, photographic hero, de-noise, single-signal chrome, lighter header
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012jWfn3RwPfFGTtBddm36Uy
2026-07-25 11:47:30 +02:00
ctao 3202b853a7 Update Article “the-ctao-eric-council-visits-ctao-north-in-la-palma” 2026-07-25 09:39:48 +00:00
ctao d564f840e8 Update Article “the-ctao-eric-council-visits-ctao-north-in-la-palma” 2026-07-25 09:39:25 +00:00
ctao 0516cae5b2 content: Opus editorial workflow — headings for 132 articles, markdown hygiene for 150 (206 reviewed); disclaimer content credit 2026-07-25 07:54:14 +02:00
ctao d014445248 feat: page composition per IA research — section order, tile icons, cross-page coherence 2026-07-25 07:53:07 +02:00
ctao 419e9a2dd1 feat: content-UX — header search, metadata de-noise, icon/microcopy coherence, article composition 2026-07-25 07:32:11 +02:00
ctao a8833ce736 content: manual editorial pass over all news articles (headings, quotes, captions) 2026-07-25 07:02:35 +02:00
ctao 4ace039233 feat: astro built-ins — news pagination, prefetch, RSS, OG/SEO meta, TOC scrollspy (Starlight-pattern) 2026-07-25 06:57:58 +02:00
ctao 2de0cf148e fix: unclip search-suggest dropdown — nebula clip moved to dedicated .band-bg layer; suggest above sticky header 2026-07-24 23:44:54 +02:00
ctao 3b736af25e feat: design system — ambient motion, spacing/elevation/surface tokens, right-rail TOC, footer links, DESIGN.md 2026-07-24 23:39:17 +02:00
ctao ea4ebd2d70 feat: round 2 — responsive header (width-math tiers), brand footer, editorial article, modern search; 2026 upgrades (view transitions, text-wrap, self-hosted fonts, inline SVG icons, color-mix, subgrid, container queries) 2026-07-24 23:14:09 +02:00
ctao a8c0e802e2 feat: pages collection (privacy/disclaimer/contact) editable in CMS 2026-07-24 23:11:57 +02:00
ctao cf35739a64 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 2026-07-24 23:00:07 +02:00
ctao 11e7eb484f content: import remaining ctao.org news (206 articles total, stress test) 2026-07-24 22:37:52 +02:00
ctao 009a6ae52a feat: CTAO-branded CMS sign-in (logo_url plaque asset), EN editor title 2026-07-24 22:19:40 +02:00
ctao 7c949bbb3c style: ctao.org-inspired pill buttons + larger radius; reorder: ERIC Council featured, test article second 2026-07-24 22:14:20 +02:00
ctao 02f30af010 feat: CTAO brand redesign (official tokens, Inter+Space Grotesk, logo), spec-based nav/footer, EN UI, card covers + featured lead, mock badges, Sveltia logo; remove fake demo articles 2026-07-24 22:09:13 +02:00
ctao 778ab25063 config: switch Sveltia backend to localhost (tunnels closed) 2026-07-24 21:18:44 +02:00
ctao 7286121f71 content: restore inline images lost in import conversion 2026-07-24 21:11:02 +02:00
ctao 366c95aa86 content: import 29 news articles with images from ctao.org 2026-07-24 21:05:16 +02:00
ctao f8181396b1 Update Artykuł “testowy-artykuł-tytuł” +2 2026-07-24 18:41:28 +00:00
ctao 757752d91f demo: move test-article source note to the top 2026-07-24 20:40:19 +02:00
ctao 45c5e3c407 demo: richer Markdown formatting in test article (headings, table, lists, quote) + prose styles 2026-07-24 20:37:49 +02:00
ctao 228ad5b60b fix: cover as public-path string (Sveltia uploads) + render cover in article 2026-07-24 20:34:22 +02:00
ctao ed45ff6d54 Create Artykuł “testowy-artykuł-tytuł” +2 2026-07-24 18:31:33 +00:00
ctao 8a2bf69576 restyle: flat Cherenkov-blue design, WCAG (skip link, focus, labels), auto dark mode 2026-07-24 20:20:29 +02:00
ctao 4180882065 fix: new Gitea tunnel URL in CMS config 2026-07-24 20:14:02 +02:00
ctao c2016e723f fix: gitea backend + allowedHosts + admin route 2026-07-24 19:59:45 +02:00
ctao a1c85d73c7 CTAO portal demo (Astro + Sveltia) 2026-07-24 18:09:00 +02:00
50 changed files with 2103 additions and 2384 deletions
-4
View File
@@ -1,4 +0,0 @@
*.sh text eol=lf
*.service text eol=lf
*.timer text eol=lf
*.container text eol=lf
+1 -1
View File
@@ -3,7 +3,7 @@ dist/
.astro/ .astro/
.DS_Store .DS_Store
# Content lives in the ctao/content repo (deploy copies it in; npm run content:link links ../content locally) # Content lives in the ctao/content repo (build overlays it; scripts/link-content.sh locally)
src/content/news/ src/content/news/
src/content/pages/ src/content/pages/
public/uploads/ public/uploads/
+38
View File
@@ -0,0 +1,38 @@
# Design handoff
All styling lives in **`src/styles/global.css`** — design tokens in `:root`
(colors, type scale, spacing, radius, shadows, `--header-h`) + plain-CSS
components below them. No frameworks, no preprocessors, no runtime JS for
presentation. Pages are `.astro` templates (plain HTML).
## Brand (official: CTAO Brand Guidelines, April 2024 — PDF in the project docs)
- Colors: Galaxy Blue `#00004A`, Cherenkov Blue `#00E4D8`, Moon Gray
`#F5F5F5`, Cosmic Azure `#007AFF` (focus rings only; links use the
darkened `--link` so they pass AA on white), Interstellar Indigo `#00009C`.
Cherenkov is an ACCENT (brand book rations it — currently: primary CTA,
hero subtitle, logo flash); it fails AA as text on white.
- Comments in the code cite `SPEC §…`, `BRAND D.x` and `REQUIREMENTS`: those
are the CTAO specification, the brand guidelines PDF and the team's
consolidated requirements, kept in the project's document folder at
Cyfronet, not in this repo.
- Type: **Inter** for everything, **Space Grotesk** (weight 500) only for
communicative headlines (h1/h2). Sizes come from the `--fs-*` scale.
- Logo files in `public/brand/`; always "CTAO", min 100px wide, no effects.
## Practical notes (the non-obvious ones)
- **`public/admin/preview.css` mirrors the `.prose` styles** so the CMS
editor preview matches articles 1:1 — when you touch `.prose` or its
tokens, update the mirror (both files carry a KEEP IN SYNC comment).
- Fonts ship **inlined as data: URIs** in global.css (no font requests, no
swap flash). The woff2 files in `public/fonts/` look unused but feed the
CMS preview — keep them.
- All `animation`/`transition` rules sit in the single
`prefers-reduced-motion: no-preference` block at the end of global.css.
- Scroll-driven effects need `vite.build.cssMinify: 'esbuild'`
(astro.config.mjs) — the default minifier breaks `animation-timeline`.
- `--header-h` drives the sticky-header offsets (anchors, TOC rail, reading
progress); change the header height only through the token.
- Deterministic gate before pushing: `npm run check` (types + build + link
check, a few seconds).
+87 -41
View File
@@ -1,54 +1,100 @@
# CTAO Science Portal # CTAO Science Portal — static site + git-based CMS
Static Astro site with a git-based CMS (Sveltia). Code lives here; editorial Public portal for CTAO news, built as a fully static site. Editors get a
content lives in the separate [`ctao/content`](https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content) browser WYSIWYG editor (Sveltia CMS) that commits Markdown to a Gitea repo;
repository. The Home page and four Example pages are editable; the rest are placeholders. a build job turns commits into static HTML. No application or database runs
on the public path.
- Live demo: https://astro.isl-dev.grid.cyfronet.pl Live demo: <https://astro.isl-dev.grid.cyfronet.pl> ·
- Editor: https://astro.isl-dev.grid.cyfronet.pl/admin/ (Gitea sign-in) Gitea on the demo machine: <https://astro-git.isl-dev.grid.cyfronet.pl>
## Run locally ## Repository layout & deployment
Node 22.19 or newer. Clone the content repository next to this one: Code and content are SEPARATE repositories, so editorial commits never mix
with development:
```sh | Repo | Holds | Source of truth | Who commits |
git clone https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git ../content |---|---|---|---|
| `ctao-portal` (this one, Bitbucket) | Templates, CSS, deploy | development | the team |
| `ctao/content` (Gitea on the demo machine) | `news/`, `pages/`, `uploads/` | editorial | Sveltia CMS / editors |
The demo machine polls ITS Gitea (`ctao/portal` mirror + `ctao/content`) and
republishes on a push to either — so a push to Bitbucket does NOT deploy;
deploying code = pushing it to the machine's Gitea (a deliberate step). The
build overlays content onto code (`deploy/build.sh`); the content paths are
gitignored here.
## Stack
- **Astro** (static output). Pages are `.astro` templates (plain HTML with a
JS frontmatter block). No React/Vue/Svelte components, no client-side
framework.
- **Plain CSS** — everything lives in `src/styles/global.css` as design
tokens (custom properties) + rules. No Tailwind, no preprocessor.
The design system (tokens, type scale, spacing, motion policy) is
documented in `DESIGN.md` — read it before touching styles.
- **Sveltia CMS** — a single prebuilt JS bundle, vendored in
`public/vendor/sveltia-cms.js` and loaded on `/admin/`
(`src/pages/admin/index.astro`), configured by `public/admin/config.yml`.
It runs entirely in the editor's browser and talks to the Gitea API
(OAuth PKCE). There is no CMS server. To update it:
`curl -sL https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js -o public/vendor/sveltia-cms.js`
- Runtime dependencies: none beyond Astro. This is deliberate — keep it
that way.
## Develop
Prerequisites: Node 22.19 or newer (the machine builds on Node 24), and
network access to the demo machine's Gitea (Cyfronet network / VPN) for the
content clone below. Without content the site still builds, but with no
articles and `npm run check` reports the footer's page links as broken.
```
npm ci npm ci
git clone https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git ../ctao-content
./scripts/link-content.sh # copies news/pages/uploads into the dev tree
npm run dev # http://localhost:4321 npm run dev # http://localhost:4321
npm run build # static output in dist/
npm run check # deterministic gate: astro check (types) + build + internal-link check
``` ```
The site reads `../content` directly through links, so pull that repository `tsconfig.json` excludes `public/` on purpose: the vendored CMS bundle there is
for the latest content. To use a checkout elsewhere: 2 MB of minified JS and would make `astro check` run out of memory.
`npm run content:link -- <path>`.
Before pushing code, run `npm run check` (types, build, internal links). ## Layout
## Edit content locally | Path | What |
With `npm run dev` running, open `http://localhost:4321/admin/` in Chrome or
Edge, choose **Work with Local Repository** and select the `../content` folder.
Saves write the Markdown files and show up on the local site on reload. They
are not committed: commit and push in `../content` to publish.
## Publishing
Pushing to `main` of `ctao/portal` (code) or `ctao/content` (content) on
Gitea rebuilds and publishes the live demo automatically; the online editor
commits to `ctao/content` directly. Pushing to Bitbucket does not deploy.
Server setup is in [deploy/README.md](deploy/README.md).
## Where things live
| Path | Purpose |
|---|---| |---|---|
| `src/layouts/Base.astro` | Header, navigation, footer, page shell | | `src/pages/` | Routes (`.astro` templates), incl. `news/`, `search`, RSS/sitemap |
| `src/layouts/ContentPage.astro` | Layout of the editable pages (Home and Examples) | | `src/layouts/Base.astro` | HTML shell: head, header/nav, footer |
| `src/data/examples.json` | Example pages: menu labels, order, slugs | | `src/content/news/*.md` | Articles (from the content repo — gitignored here; schema in `src/content.config.ts`) |
| `src/data/placeholders.json` | Placeholder pages | | `src/styles/global.css` | All CSS: tokens + components + prose |
| `src/styles/global.css` | All site styles | | `DESIGN.md` | Design handoff: where the tokens live, brand basics, practical notes |
| `public/admin/config.yml` | CMS fields and Gitea connection | | `public/admin/` | CMS config (`config.yml`) + editor preview styles (`preview.css`) |
| `scripts/preview-styles.mjs` | Builds the CMS preview styles from `global.css` | | `public/uploads/` | Editor-uploaded media (from the content repo — gitignored here) |
| `scripts/content.mjs` | Links the content checkout into the site | | `deploy/` | Runbook + container/systemd units for the demo machine — see `deploy/README.md` |
To add an Example page, add it to `src/data/examples.json` and ## Editing content
`public/admin/config.yml`, and commit its Markdown file to `ctao/content`.
Editors use `/admin/` (link in the footer) and sign in with a Gitea account.
Saving commits to `main`; the machine polls and republishes automatically
(seconds). Full history/rollback = git history in Gitea. Articles with
`draft: true` are excluded from the build.
Note for styling work: `public/admin/preview.css` mirrors the `.prose` rules
from `global.css` so the editor preview matches the site 1:1 — keep them in
sync (both files carry a KEEP IN SYNC comment).
## Upgrading
Versions are pinned exactly; an upgrade is a deliberate change, never a side
effect of `npm install`. Astro ships security fixes only for the current and
one previous major, so plan one upgrade per major rather than skipping several.
```
npx @astrojs/upgrade # moves astro and official integrations together
npm run check # types + build + links must pass
npm run preview # verify the real dist/ output, not the dev server
```
No experimental flags in `astro.config.mjs`: they can change in minor releases.
+23 -1
View File
@@ -1,9 +1,31 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from 'astro/config';
// Static output (zero runtime) — the whole point of the git-based approach.
export default defineConfig({ export default defineConfig({
output: 'static', output: 'static',
// Public origin — used to build absolute URLs (canonical, og:*, RSS,
// sitemap). Change when the portal moves to its production domain.
site: 'https://astro.isl-dev.grid.cyfronet.pl', site: 'https://astro.isl-dev.grid.cyfronet.pl',
trailingSlash: 'always', // URL contract, pinned explicitly (these are the defaults): changing either
// later rewrites every public URL. nginx serves /path/ via try_files $uri/.
trailingSlash: 'ignore',
build: { format: 'directory' }, build: { format: 'directory' },
// Lossless whitespace handling instead of the v7 default 'jsx', which drops
// line breaks around inline elements and silently glues words to links when
// a template line wraps before <a>. Templates render as written.
compressHTML: true, compressHTML: true,
// Built-in prefetch on every internal link (no per-link attributes needed).
// Default 'hover' strategy: near-instant navigation without the bandwidth
// cost of 'viewport' on a 24-card grid; auto-falls back to 'tap' on
// data-saver / slow connections (Astro handles that).
prefetch: { prefetchAll: true },
vite: {
build: {
// Lightning CSS (the default minifier) illegally folds animation-timeline
// INTO the `animation` shorthand (`animation: ... view()`), which browsers
// reject — silently killing every scroll-driven effect in production.
// esbuild minifies without property merging.
cssMinify: 'esbuild',
},
},
}); });
+10 -46
View File
@@ -1,10 +1,10 @@
# Deploy: CTAO portal demo on strapi-experimental.cyfronet # Deploy — CTAO portal demo on strapi-experimental.cyfronet
Everything runs rootless as user `strapi`. Footprint on the machine: Everything runs rootless as user `strapi`. Footprint on the machine:
ONE directory (`~/ctao-portal-demo/`) + 4 unit files in `~/.config/`. ONE directory (`~/ctao-portal-demo/`) + 4 unit files in `~/.config/`.
Uninstall restores the machine exactly (see bottom). No secrets in any file. Uninstall restores the machine exactly (see bottom). No secrets in any file.
Two Gitea repos by design: `ctao/portal` (code, developed by the team) and Two Gitea repos by design — `ctao/portal` (code, developed by the team) and
`ctao/content` (Markdown + uploads, committed by the CMS). The build overlays `ctao/content` (Markdown + uploads, committed by the CMS). The build overlays
content onto code; a push to EITHER republishes the site. content onto code; a push to EITHER republishes the site.
@@ -29,44 +29,9 @@ missing). Everything else runs inside containers.
| 3000 | Gitea | https://astro-git.isl-dev.grid.cyfronet.pl (ingress vhost) | | 3000 | Gitea | https://astro-git.isl-dev.grid.cyfronet.pl (ingress vhost) |
| 8080 | portal (nginx, static) | https://astro.isl-dev.grid.cyfronet.pl (ingress vhost) | | 8080 | portal (nginx, static) | https://astro.isl-dev.grid.cyfronet.pl (ingress vhost) |
## Publish the simplified portal from `main`
1. Check that `ctao/content:main` contains the four Example files in `pages/`
(listed in `src/data/examples.json`) and the uploads they reference. They
are already committed there (`a3452d7`, "demo lorem ipsum pages"). News and
other page files can remain in the content repository; this portal loads
only the four Example files.
2. Push the content branch to Gitea as `ctao/content:main`, and the code
branch as `ctao/portal:main`. These are separate pushes; Bitbucket is
not the deployment remote.
3. Copy the updated `build.sh` to `~/ctao-portal-demo/bin/build.sh` and the
updated `ctao-portal-build.service` to `~/.config/systemd/user/`. Reload
systemd with `systemctl --user daemon-reload`. The service explicitly sets
`CODE_BRANCH=main` and `CONTENT_BRANCH=main`; the script defaults agree.
4. Start the build service or let its timer run. It overlays `pages/` and
`uploads/`, clears the previous news overlay, and runs `npm run check`
before the atomic release switch. Missing Example pages fail the build.
5. Verify the four `/example/…/` pages and empty `/news/`. Sign in at `/admin/`,
save an Example title, and check that a commit appears on
`ctao/content:main` and the rebuilt page reflects it.
The OAuth client and public portal origin stay the same. Sveltia's
`backend.branch` in `public/admin/config.yml` must always match
`CONTENT_BRANCH`. Branches are selected independently; a single `BRANCH`
environment variable is no longer used. For a separate preview hostname,
update the site origin, OAuth redirect, and Gitea CORS together.
To undo the simplification in shared code history, use
`git revert -m 1 <simplification-merge-commit>` on `main` and publish the
resulting commit. The previous portal code is also preserved at `0772462`.
For an immediate deployment rollback, restore the previous release and worker
script/service configuration. Content has its own history: reverting a code
commit does not revert CMS edits. Keep the old content files available if
the previous portal may be restored.
## Install (each step reviewed before running; [W] = writes to the machine) ## Install (each step reviewed before running; [W] = writes to the machine)
1. **[W]** `loginctl enable-linger $USER`. Without lingering every user unit 1. **[W]** `loginctl enable-linger $USER` — without lingering every user unit
dies at logout and nothing starts after a reboot. Verify: dies at logout and nothing starts after a reboot. Verify:
`loginctl show-user $USER -p Linger` → `Linger=yes`. `loginctl show-user $USER -p Linger` → `Linger=yes`.
2. **[W]** `mkdir -p ~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config} ~/.config/containers/systemd ~/.config/systemd/user` 2. **[W]** `mkdir -p ~/ctao-portal-demo/{gitea-data,gitea-config,releases,state,bin,config} ~/.config/containers/systemd ~/.config/systemd/user`
@@ -84,16 +49,15 @@ the previous portal may be restored.
(Upgrading later = pick new digests deliberately, update the pins in the (Upgrading later = pick new digests deliberately, update the pins in the
`.container` files / `Containerfile.build`, re-pull, re-build.) `.container` files / `Containerfile.build`, re-pull, re-build.)
5. **[W]** `systemctl --user daemon-reload && systemctl --user start ctao-demo-gitea` 5. **[W]** `systemctl --user daemon-reload && systemctl --user start ctao-demo-gitea`
6. **[W]** Create the Gitea admin. Run it interactively in a terminal so the 6. **[W]** Create the Gitea admin — run interactively in a terminal so the
password never lands in a file or shell history: password never lands in a file or shell history:
`podman exec -it ctao-demo-gitea gitea admin user create --admin --username <you> --email <you@…> --random-password` `podman exec -it ctao-demo-gitea gitea admin user create --admin --username <you> --email <you@…> --random-password`
7. **[W]** In the Gitea UI: create org `ctao` with repos `portal` and 7. **[W]** In the Gitea UI: create org `ctao` with repos `portal` and
`content` (both public read). The content `main` branch needs the four `content` (both public read). Then, from your workstation, push both over
Example pages described above. Then, from your workstation, push both over
the vhost with a repo-scoped token: the vhost with a repo-scoped token:
`git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/portal.git main` `git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/portal.git main`
`git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git main` `git push https://<user>:<token>@astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git main`
8. **[W]** `systemctl --user enable --now ctao-portal-build.timer`. The first run 8. **[W]** `systemctl --user enable --now ctao-portal-build.timer` — first run
clones + `npm ci` + builds (minutes); later runs are seconds. Wait until clones + `npm ci` + builds (minutes); later runs are seconds. Wait until
`journalctl --user -u ctao-portal-build -n 5` shows `published <sha>` `journalctl --user -u ctao-portal-build -n 5` shows `published <sha>`
(starting nginx earlier just serves 404s until the first build lands). (starting nginx earlier just serves 404s until the first build lands).
@@ -102,7 +66,7 @@ the previous portal may be restored.
and `journalctl --user -u ctao-portal-build -n 20` (shows measured build times). and `journalctl --user -u ctao-portal-build -n 20` (shows measured build times).
Editor accounts need WRITE access to `ctao/content` (Gitea → repo → Editor accounts need WRITE access to `ctao/content` (Gitea → repo →
Collaborators, or a team); public read alone lets them sign in but every Collaborators, or a team) — public read alone lets them sign in but every
save fails. save fails.
## CMS sign-in (Sveltia ↔ Gitea OAuth) ## CMS sign-in (Sveltia ↔ Gitea OAuth)
@@ -152,12 +116,12 @@ loginctl disable-linger $USER # only if nothing else of yours should survive l
lockfile changes. Routine rebuilds are fully offline. lockfile changes. Routine rebuilds are fully offline.
- Memory caps (`MemoryHigh`) keep us polite next to Outline + Strapi; - Memory caps (`MemoryHigh`) keep us polite next to Outline + Strapi;
the build container is capped at 1 GB via `podman run --memory`. Watch the the build container is capped at 1 GB via `podman run --memory`. Watch the
first `npm ci` + build in the journal; if it OOMs inside its cgroup first `npm ci` + build in the journal — if it OOMs inside its cgroup
(contained, just retries), raise the cap. (contained, just retries), raise the cap.
- Build isolation: build containers join the Gitea container's network - Build isolation: build containers join the Gitea container's network
namespace (`--network=container:ctao-demo-gitea`): repo/npm code sees namespace (`--network=container:ctao-demo-gitea`) — repo/npm code sees
Gitea on localhost:3000 but cannot reach host loopback services. (Rootless Gitea on localhost:3000 but cannot reach host loopback services. (Rootless
netavark bridges don't work here: no `ip_tables` kernel module, no sudo.) netavark bridges don't work here: no `ip_tables` kernel module, no sudo.)
- Secrets inventory: Gitea admin password (typed interactively, lives only - Secrets inventory: Gitea admin password (typed interactively, lives only
in Gitea's DB) and repo-scoped push tokens (managed in Gitea); that's the in Gitea's DB) and repo-scoped push tokens (managed in Gitea) — that's the
complete list. Build/poll/serve use none. complete list. Build/poll/serve use none.
+16 -18
View File
@@ -7,7 +7,7 @@
# #
# Two repositories by design: code (templates/CSS, developed by the team) and # Two repositories by design: code (templates/CSS, developed by the team) and
# content (Markdown + uploads, committed by the CMS). The build overlays # content (Markdown + uploads, committed by the CMS). The build overlays
# content onto code, so an editor publishing a page and a developer # content onto code, so an editor publishing an article and a developer
# shipping CSS never mix histories — either change republishes the site. # shipping CSS never mix histories — either change republishes the site.
set -euo pipefail set -euo pipefail
@@ -17,8 +17,7 @@ BASE="${BASE:-$HOME/ctao-portal-demo}"
GITEA_URL="${GITEA_URL:-http://localhost:3000}" # published by ctao-demo-gitea GITEA_URL="${GITEA_URL:-http://localhost:3000}" # published by ctao-demo-gitea
CODE_REPO="${CODE_REPO:-ctao/portal}" # owner/repo in Gitea CODE_REPO="${CODE_REPO:-ctao/portal}" # owner/repo in Gitea
CONTENT_REPO="${CONTENT_REPO:-ctao/content}" CONTENT_REPO="${CONTENT_REPO:-ctao/content}"
CODE_BRANCH="${CODE_BRANCH:-main}" BRANCH="${BRANCH:-main}"
CONTENT_BRANCH="${CONTENT_BRANCH:-main}"
BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}" BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}"
# The build joins the Gitea container's network namespace: localhost inside # The build joins the Gitea container's network namespace: localhost inside
# the build = Gitea's loopback (port 3000), host loopback stays unreachable. # the build = Gitea's loopback (port 3000), host loopback stays unreachable.
@@ -27,7 +26,7 @@ BUILD_IMAGE="${BUILD_IMAGE:-localhost/ctao-portal-build:1}"
BUILD_NETNS="${BUILD_NETNS:-container:ctao-demo-gitea}" BUILD_NETNS="${BUILD_NETNS:-container:ctao-demo-gitea}"
CODE_URL="${CODE_URL:-http://localhost:3000/$CODE_REPO.git}" CODE_URL="${CODE_URL:-http://localhost:3000/$CODE_REPO.git}"
CONTENT_URL="${CONTENT_URL:-http://localhost:3000/$CONTENT_REPO.git}" CONTENT_URL="${CONTENT_URL:-http://localhost:3000/$CONTENT_REPO.git}"
KEEP="${KEEP:-3}" # previous releases kept besides `current` (rollback targets) KEEP="${KEEP:-3}" # released builds to retain
mkdir -p "$BASE/repo" "$BASE/content" "$BASE/releases" "$BASE/state" "$BASE/npm-cache" mkdir -p "$BASE/repo" "$BASE/content" "$BASE/releases" "$BASE/state" "$BASE/npm-cache"
# Host prerequisites (everything else runs inside containers). Fail loud — # Host prerequisites (everything else runs inside containers). Fail loud —
@@ -38,16 +37,16 @@ done
# --- 1. Cheap poll: both branch heads via the local Gitea API --- # --- 1. Cheap poll: both branch heads via the local Gitea API ---
head_of() { head_of() {
curl -fsS --max-time 5 "$GITEA_URL/api/v1/repos/$1/branches/$2" \ curl -fsS --max-time 5 "$GITEA_URL/api/v1/repos/$1/branches/$BRANCH" \
| jq -r '.commit.id' || true | jq -r '.commit.id' || true
} }
code_sha=$(head_of "$CODE_REPO" "$CODE_BRANCH") code_sha=$(head_of "$CODE_REPO")
content_sha=$(head_of "$CONTENT_REPO" "$CONTENT_BRANCH") content_sha=$(head_of "$CONTENT_REPO")
# Gitea down/unreachable is a transient, not a unit failure — exit 0 quietly # Gitea down/unreachable is a transient, not a unit failure — exit 0 quietly
# instead of painting the journal red every 10 s. Name the repo: a 404 here # instead of painting the journal red every 10 s. Name the repo: a 404 here
# also means "repo/branch missing or renamed", not just "Gitea down". # also means "repo/branch missing or renamed", not just "Gitea down".
[[ "$code_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "poll failed for $CODE_REPO@$CODE_BRANCH (gitea down, or repo/branch missing) — skipping"; exit 0; } [[ "$code_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "poll failed for $CODE_REPO@$BRANCH (gitea down, or repo/branch missing) — skipping"; exit 0; }
[[ "$content_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "poll failed for $CONTENT_REPO@$CONTENT_BRANCH (gitea down, or repo/branch missing) — skipping"; exit 0; } [[ "$content_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "poll failed for $CONTENT_REPO@$BRANCH (gitea down, or repo/branch missing) — skipping"; exit 0; }
release="${code_sha:0:12}-${content_sha:0:12}" # code+content pin the release release="${code_sha:0:12}-${content_sha:0:12}" # code+content pin the release
# Skip only if this pair is both recorded AND still present in releases/ # Skip only if this pair is both recorded AND still present in releases/
# (a deleted release dir must trigger a rebuild, not an eternal skip). # (a deleted release dir must trigger a rebuild, not an eternal skip).
@@ -74,7 +73,7 @@ podman run --rm --network="$BUILD_NETNS" --memory=1g \
-e ASTRO_TELEMETRY_DISABLED=1 \ -e ASTRO_TELEMETRY_DISABLED=1 \
-e CODE_SHA="$code_sha" -e CODE_URL="$CODE_URL" \ -e CODE_SHA="$code_sha" -e CODE_URL="$CODE_URL" \
-e CONTENT_SHA="$content_sha" -e CONTENT_URL="$CONTENT_URL" \ -e CONTENT_SHA="$content_sha" -e CONTENT_URL="$CONTENT_URL" \
-e RELEASE="$release" -e CODE_BRANCH="$CODE_BRANCH" -e CONTENT_BRANCH="$CONTENT_BRANCH" \ -e RELEASE="$release" -e BRANCH="$BRANCH" \
-v "$BASE/repo:/work/repo:z" \ -v "$BASE/repo:/work/repo:z" \
-v "$BASE/content:/work/content:z" \ -v "$BASE/content:/work/content:z" \
-v "$BASE/releases:/work/releases:z" \ -v "$BASE/releases:/work/releases:z" \
@@ -82,20 +81,21 @@ podman run --rm --network="$BUILD_NETNS" --memory=1g \
-w /work "$BUILD_IMAGE" sh -ec ' -w /work "$BUILD_IMAGE" sh -ec '
git config --global safe.directory "/work/repo" git config --global safe.directory "/work/repo"
git config --global --add safe.directory "/work/content" git config --global --add safe.directory "/work/content"
sync_clone() { # $1 dir $2 url $3 sha $4 branch sync_clone() { # $1 dir $2 url $3 sha
[ -d "$1/.git" ] || git clone --branch "$4" "$2" "$1" [ -d "$1/.git" ] || git clone --branch "$BRANCH" "$2" "$1"
git -C "$1" remote set-url origin "$2" # self-heal if the URL changes git -C "$1" remote set-url origin "$2" # self-heal if the URL changes
git -C "$1" fetch --quiet origin "$4" git -C "$1" fetch --quiet origin "$BRANCH"
# --force: the working copy is disposable; a stray tracked-file edit # --force: the working copy is disposable; a stray tracked-file edit
# must not wedge every future build. # must not wedge every future build.
git -C "$1" checkout --quiet --force "$3" git -C "$1" checkout --quiet --force "$3"
} }
sync_clone repo "$CODE_URL" "$CODE_SHA" "$CODE_BRANCH" sync_clone repo "$CODE_URL" "$CODE_SHA"
sync_clone content "$CONTENT_URL" "$CONTENT_SHA" "$CONTENT_BRANCH" sync_clone content "$CONTENT_URL" "$CONTENT_SHA"
# Overlay content onto code (these paths are gitignored in the code repo). # Overlay content onto code (these paths are gitignored in the code repo).
# mkdir -p: checkout prunes the emptied parent dirs, cp needs them back. # mkdir -p: checkout prunes the emptied parent dirs, cp needs them back.
rm -rf repo/src/content/news repo/src/content/pages repo/public/uploads rm -rf repo/src/content/news repo/src/content/pages repo/public/uploads
mkdir -p repo/src/content repo/public mkdir -p repo/src/content repo/public
cp -a content/news repo/src/content/news
cp -a content/pages repo/src/content/pages cp -a content/pages repo/src/content/pages
cp -a content/uploads repo/public/uploads cp -a content/uploads repo/public/uploads
cd repo cd repo
@@ -109,9 +109,7 @@ podman run --rm --network="$BUILD_NETNS" --memory=1g \
npm ci --ignore-scripts --no-audit --no-fund npm ci --ignore-scripts --no-audit --no-fund
echo "$lock" > .deps-hash echo "$lock" > .deps-hash
fi fi
# Validate the overlay and links before publishing; never seed demo content npm run build
# in place of an incomplete content branch.
npm run check
rm -rf "../releases/$RELEASE" rm -rf "../releases/$RELEASE"
cp -a dist "../releases/$RELEASE" cp -a dist "../releases/$RELEASE"
' || { echo "$release" > "$BASE/state/last-failed"; echo "BUILD FAILED for $release (see above) — will not retry until a new commit"; exit 1; } ' || { echo "$release" > "$BASE/state/last-failed"; echo "BUILD FAILED for $release (see above) — will not retry until a new commit"; exit 1; }
-2
View File
@@ -8,8 +8,6 @@ Description=CTAO portal demo — rebuild if the code or content repo has new com
[Service] [Service]
Type=oneshot Type=oneshot
Environment=CODE_BRANCH=main
Environment=CONTENT_BRANCH=main
ExecStart=%h/ctao-portal-demo/bin/build.sh ExecStart=%h/ctao-portal-demo/bin/build.sh
# Polite neighbour on the shared VM: # Polite neighbour on the shared VM:
Nice=10 Nice=10
-3
View File
@@ -13,9 +13,6 @@
"devDependencies": { "devDependencies": {
"@astrojs/check": "0.9.10", "@astrojs/check": "0.9.10",
"typescript": "6.0.3" "typescript": "6.0.3"
},
"engines": {
"node": ">=22.19.0"
} }
}, },
"node_modules/@astrojs/check": { "node_modules/@astrojs/check": {
-4
View File
@@ -4,13 +4,9 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"content:link": "node scripts/content.mjs --relink",
"predev": "node scripts/content.mjs && node scripts/preview-styles.mjs",
"dev": "astro dev", "dev": "astro dev",
"prebuild": "node scripts/content.mjs && node scripts/preview-styles.mjs",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"precheck": "node scripts/content.mjs && node scripts/preview-styles.mjs",
"check": "astro check && astro build && node scripts/check-links.mjs" "check": "astro check && astro build && node scripts/check-links.mjs"
}, },
"dependencies": { "dependencies": {
+35 -80
View File
@@ -4,7 +4,8 @@
# these origins in deploy/ctao-demo-gitea.container. # these origins in deploy/ctao-demo-gitea.container.
backend: backend:
name: gitea name: gitea
# Keep this branch in sync with CONTENT_BRANCH in deploy/build.sh. # Content lives in its OWN repo (editorial history separate from code —
# the build overlays it onto the portal code, see deploy/build.sh).
repo: ctao/content repo: ctao/content
branch: main branch: main
base_url: https://astro-git.isl-dev.grid.cyfronet.pl base_url: https://astro-git.isl-dev.grid.cyfronet.pl
@@ -44,82 +45,36 @@ media_libraries:
optimize: true optimize: true
collections: collections:
- name: home - name: news
label: "Home page" label: "News"
files: label_singular: "Article"
- name: home folder: "news"
label: "Home page" create: true
file: "pages/home.md" slug: "{{slug}}"
format: frontmatter extension: md
fields: format: frontmatter
- { name: title, label: "Title", widget: string } summary: "{{title}} · {{date}}"
- { name: introduction, label: "Introduction", widget: text, required: false } fields:
- name: images - { name: title, label: "Title", widget: string }
label: "Images" - { name: description, label: "Short description", widget: text }
label_singular: "image" - { name: date, label: "Date", widget: datetime, date_format: "YYYY-MM-DD", time_format: false }
widget: list - { name: category, label: "Category", widget: string, default: "news" }
required: false - { name: author, label: "Author", widget: string, default: "CTAO" }
max: 2 - { name: cover, label: "Cover image", widget: image, required: false }
hint: "Up to two images, shown side by side." - { name: lang, label: "Language (if not English, e.g. pl)", widget: string, required: false }
fields: - { name: draft, label: "Draft (unpublished)", widget: boolean, default: false }
- { name: image, label: "Image", widget: image } - { name: body, label: "Body", widget: markdown }
- { name: alt, label: "Alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
- { name: caption, label: "Caption", widget: string, required: false } - name: pages
- { name: body, label: "Body", widget: markdown } label: "Pages"
- { name: buttonLabel, label: "Button label", widget: string, required: false } label_singular: "Page"
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." } folder: "pages"
- name: examples create: true
label: "Example pages" slug: "{{slug}}"
files: extension: md
- name: lorem-ipsum format: frontmatter
label: "Lorem ipsum" summary: "{{title}}"
file: "pages/lorem-ipsum.md" fields:
format: frontmatter - { name: title, label: "Title", widget: string }
fields: - { name: description, label: "Description", widget: text, required: false }
- { name: title, label: "Title", widget: string } - { name: body, label: "Body", widget: markdown }
- { name: introduction, label: "Introduction", widget: text, required: false }
- { name: image, label: "Image", widget: image, required: false }
- { name: imageAlt, label: "Image alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
- { name: imageCaption, label: "Image caption", widget: string, required: false }
- { name: body, label: "Body", widget: markdown }
- { name: buttonLabel, label: "Button label", widget: string, required: false }
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
- name: dolor-sit-amet
label: "Dolor sit amet"
file: "pages/dolor-sit-amet.md"
format: frontmatter
fields:
- { name: title, label: "Title", widget: string }
- { name: introduction, label: "Introduction", widget: text, required: false }
- { name: image, label: "Image", widget: image, required: false }
- { name: imageAlt, label: "Image alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
- { name: imageCaption, label: "Image caption", widget: string, required: false }
- { name: body, label: "Body", widget: markdown }
- { name: buttonLabel, label: "Button label", widget: string, required: false }
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
- name: consectetur-adipiscing
label: "Consectetur adipiscing"
file: "pages/consectetur-adipiscing.md"
format: frontmatter
fields:
- { name: title, label: "Title", widget: string }
- { name: introduction, label: "Introduction", widget: text, required: false }
- { name: image, label: "Image", widget: image, required: false }
- { name: imageAlt, label: "Image alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
- { name: imageCaption, label: "Image caption", widget: string, required: false }
- { name: body, label: "Body", widget: markdown }
- { name: buttonLabel, label: "Button label", widget: string, required: false }
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
- name: sed-do-eiusmod
label: "Sed do eiusmod"
file: "pages/sed-do-eiusmod.md"
format: frontmatter
fields:
- { name: title, label: "Title", widget: string }
- { name: introduction, label: "Introduction", widget: text, required: false }
- { name: image, label: "Image", widget: image, required: false }
- { name: imageAlt, label: "Image alternative text", widget: string, required: false, hint: "Describe the image; leave empty only for decorative images." }
- { name: imageCaption, label: "Image caption", widget: string, required: false }
- { name: body, label: "Body", widget: markdown }
- { name: buttonLabel, label: "Button label", widget: string, required: false }
- { name: buttonUrl, label: "Button destination", widget: string, required: false, hint: "A site path such as /example/lorem-ipsum/, or an https:// URL." }
+56 -455
View File
@@ -1,470 +1,71 @@
/* Generated by scripts/preview-styles.mjs; edit src/styles/global.css. */ /* Entry-preview styles for the Sveltia editor, registered via the documented
CMS.registerPreviewStyle API (bare element selectors, per the official docs
/* ========================================================================== example). This file MIRRORS the article styles 1:1 — the design tokens from
CTAO Science Portal: content page mockup :root and the `.prose` block in src/styles/global.css — with `.prose`
Colours and type follow the CTAO Brand Guidelines (April 2024). dropped from selectors (the preview pane has no such wrapper).
The editor preview (public/admin/preview.css) is generated from the KEEP IN SYNC: when you touch `.prose` or the tokens in global.css, update
hero, prose and button sections below by scripts/preview-styles.mjs. the corresponding value here (a matching reminder sits next to `.prose`).
========================================================================== */ Fonts: same self-hosted woff2 files the site uses, same origin. (The
Sveltia bundle still loads its own UI fonts from jsDelivr — only the
/* Fonts PREVIEW pane is CDN-free.) */
========================================================================== */
@font-face { @font-face {
font-family: "Inter"; font-family: "Inter"; font-weight: 400 700; font-display: swap;
font-weight: 400 700; src: url("/fonts/inter-latin.woff2") format("woff2");
font-display: swap;
src: url("/assets/fonts/inter-latin.woff2") format("woff2");
} }
@font-face { @font-face {
font-family: "Space Grotesk"; font-family: "Space Grotesk"; font-weight: 500 700; font-display: swap;
font-weight: 500 700; src: url("/fonts/space-grotesk-latin.woff2") format("woff2");
font-display: swap;
src: url("/assets/fonts/space-grotesk-latin.woff2") format("woff2");
} }
/* Tokens
========================================================================== */
:root { :root {
/* Brand main colours (D.2.1) */ /* tokens copied from global.css :root */
--galaxy-blue: #00004a; --galaxy: #00004a;
--cherenkov-blue: #00e4d8; --moon: #f5f5f5;
--moon-gray: #f5f5f5; --text: #101228;
--muted: #46536a;
/* Brand secondary colours (D.2.2) */ --border: #e2e5ee;
--white: #ffffff; --link: #0057c2;
--cosmic-azure: #007aff; --font-body: "Inter", Arial, system-ui, sans-serif;
--interstellar-indigo: #00009c; --font-display: "Space Grotesk", var(--font-body);
--fs-s: 0.9rem;
/* Text */ --fs-l: 1.125rem;
--text: #111111; --fs-xl: 1.5rem;
--text-muted: #555555; --fs-h2: clamp(1.75rem, 3.5vw, 2.5rem);
/* Cosmic Azure at 84% brightness: 5.4:1 on white, 3.5:1 against body text */ --radius: 16px;
--link: #0066d6;
/* Type: Inter for everything, Space Grotesk for headlines (D.3) */
--font-body: "Inter", Arial, sans-serif;
--font-head: "Space Grotesk", Arial, sans-serif;
/* Layout */
--content-width: 85rem;
--gutter: clamp(1rem, 4vw, 2.5rem);
--header-height: 7rem;
--header-height-scrolled: 5rem;
/* Large decorative corners (hero, header, search panel): grows with the
viewport instead of switching at breakpoints */
--radius-lg: min(15vw, 180px);
--radius-sm: 15px;
/* How far photos are pulled up into the hero */
--hero-overlap: clamp(6rem, 11.25vw, 9rem);
/* Dropdown submenus (Example, language) */
--radius-menu: 8px;
--transition: 0.3s ease-in-out;
/* Film grain tile over the hero background, also hides gradient banding */
--grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.05'/%3E%3C/svg%3E");
}
/* Base
========================================================================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-padding-top: var(--header-height-scrolled);
} }
/* preview shell ≈ article column */
body { body {
margin: 0;
font-family: var(--font-body); font-family: var(--font-body);
font-size: 1rem;
line-height: 1.5;
color: var(--text); color: var(--text);
background: var(--white);
-webkit-font-smoothing: antialiased;
}
body.no-scroll {
overflow: hidden;
}
img {
display: block;
max-width: 100%;
height: auto;
}
a {
color: var(--link);
text-decoration: none;
}
a:hover,
a:focus-visible {
text-decoration: underline;
text-underline-offset: 0.15em;
}
button {
padding: 0;
font: inherit;
color: inherit;
background: none;
border: 0;
cursor: pointer;
}
/* Focus ring: Link blue on light backgrounds, Cherenkov Blue on dark ones */
:focus-visible {
outline: 2px solid var(--link);
outline-offset: 3px;
}
.site-header :focus-visible,
.page-hero :focus-visible,
.site-footer :focus-visible {
outline-color: var(--cherenkov-blue);
}
/* Utilities
========================================================================== */
.container {
width: min(100% - 2 * var(--gutter), var(--content-width));
margin-inline: auto;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
.skip-link {
position: absolute;
top: -4rem;
left: 1rem;
z-index: 100;
padding: 0.5rem 1rem;
color: var(--galaxy-blue);
background: var(--white);
border-radius: var(--radius-sm);
}
.skip-link:focus {
top: 1rem;
}
.arrow {
display: inline-block;
transition: transform 0.25s ease;
}
a:hover .arrow,
button:hover .arrow {
transform: translateX(4px);
}
@keyframes panel-in {
from {
opacity: 0;
transform: translateY(-6px);
}
to {
opacity: 1;
transform: none;
}
}
/* Page hero
========================================================================== */
.page-hero {
margin-top: calc(-1 * var(--header-height));
padding: calc(var(--header-height) + 3rem) 0 calc(var(--hero-overlap) + 3rem);
color: var(--white);
background: var(--grain), var(--galaxy-blue);
border-bottom-right-radius: var(--radius-lg);
}
/* Pages without a photo: no room needed for the figure to overlap */
.page-hero--compact {
padding-bottom: 4rem;
}
.breadcrumb ol {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0 0 1rem;
padding: 0;
font-size: 0.875rem;
list-style: none;
}
.breadcrumb li + li::before {
content: "›";
margin-right: 0.5rem;
color: rgba(255, 255, 255, 0.5);
}
.breadcrumb a {
color: rgba(255, 255, 255, 0.6);
}
.breadcrumb a:hover {
color: var(--white);
}
.breadcrumb [aria-current] {
font-weight: 500;
color: var(--white);
}
/* Headline: Space Grotesk medium, white on dark (D.3.4) */
.page-hero__title {
max-width: 24ch;
margin: 0 0 1.25rem;
font-family: var(--font-head);
font-size: clamp(2rem, 5vw, 3.75rem);
font-weight: 500;
line-height: 1.05;
letter-spacing: -0.02em;
text-wrap: balance;
overflow-wrap: anywhere;
}
.page-hero__lead {
max-width: 70ch; max-width: 70ch;
margin: 0; margin: 0 auto;
font-size: 1.0625rem; padding: 24px;
font-weight: 500;
line-height: 1.4;
} }
/* Photo pulled up over the hero; flat, no shadow */ /* = global.css base heading/link rules = */
.page-figure { h1, h2 { font-family: var(--font-display); font-weight: 500; letter-spacing: -0.025em; line-height: 1.1; }
margin: calc(-1 * var(--hero-overlap)) 0 4rem; h3 { font-weight: 600; }
} h1, h2, h3 { overflow-wrap: break-word; text-wrap: balance; }
a { color: var(--link); text-decoration: underline; text-underline-offset: 3px; }
.page-figure img {
width: 100%; /* = global.css `.prose` rules, selectors unwrapped = */
border-radius: var(--radius-sm); body { line-height: 1.65; overflow-wrap: break-word; }
} a { overflow-wrap: anywhere; }
h1 { color: var(--galaxy); font-size: var(--fs-h2); } /* mirrors `.article h1` */
.page-figure figcaption { h2 { color: var(--galaxy); margin: 44px 0 12px; font-size: var(--fs-xl); }
margin-top: 0.75rem; h3 { color: var(--galaxy); margin: 30px 0 8px; font-size: var(--fs-l); }
font-size: 0.8125rem; p { margin: 0 0 16px; text-wrap: pretty; }
color: var(--text-muted); ul, ol { margin: 0 0 16px; padding-left: 22px; }
} li { margin: 6px 0; }
blockquote {
/* Two photos side by side (Home), pulled up over the hero together; they margin: 22px 0; padding: 12px 20px; border-left: 3px solid var(--galaxy);
stack by themselves once each would be narrower than 18rem */ background: var(--moon); color: var(--text);
.page-figures {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: 1.5rem;
margin: calc(-1 * var(--hero-overlap)) 0 4rem;
}
.page-figures .page-figure {
margin: 0;
}
/* Same shape for both photos, whatever the uploads' proportions */
.page-figures img {
aspect-ratio: 3 / 2;
object-fit: cover;
}
/* Prose
========================================================================== */
.prose {
padding-bottom: 5rem;
overflow-wrap: anywhere;
}
.prose--no-image {
padding-top: 4rem;
}
.prose > * {
max-width: 64rem;
}
/* Galaxy Blue headline on light background (D.3.4) */
.prose h2 {
margin: 3rem 0 1rem;
font-family: var(--font-head);
font-size: clamp(1.75rem, 3vw, 2.375rem);
font-weight: 500;
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--galaxy-blue);
}
.prose h2:first-child {
margin-top: 0;
}
.prose p {
margin: 0 0 1.25rem;
font-size: 1.0625rem;
line-height: 1.6;
}
/* Text links: colour only at rest, underline on hover and focus */
.prose a:not(.btn):hover,
.prose a:not(.btn):focus-visible {
text-decoration: underline;
text-underline-offset: 0.15em;
}
/* Cherenkov Blue fails contrast as text on white, so it is the accent bar */
.prose .highlight,
.prose blockquote {
margin: 2.5rem 0;
padding-left: 1.5rem;
font-size: 1.375rem;
font-weight: 500;
line-height: 1.35;
color: var(--galaxy-blue);
border-left: 4px solid var(--cherenkov-blue);
}
.prose blockquote p {
font-size: inherit;
line-height: inherit;
}
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
font-family: var(--font-head);
color: var(--galaxy-blue);
}
.prose li {
margin-bottom: 0.5rem;
}
.prose img {
border-radius: var(--radius-sm);
}
.prose pre {
padding: 1rem;
overflow-x: auto;
background: var(--moon-gray);
}
.prose code {
overflow-wrap: normal;
}
.prose table {
display: block;
overflow-x: auto;
border-collapse: collapse;
}
.prose th,
.prose td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid #ddd;
}
.prose hr {
margin: 2rem 0;
border: 0;
border-top: 1px solid #ddd;
}
/* Placeholder pages: empty body until their content exists */
.placeholder-body {
min-height: 12rem;
}
/* Buttons
========================================================================== */
.btn {
display: inline-flex;
align-items: center;
gap: 0.75rem;
height: 2.75rem;
padding: 0 1.5rem;
font-size: 0.9375rem;
font-weight: 700;
border-radius: 999px;
}
/* Underline the label only: a flex container would pass it on to the arrow */
.btn:hover,
.btn:focus-visible {
text-decoration: none;
}
.btn:hover .btn__label,
.btn:focus-visible .btn__label {
text-decoration: underline;
text-underline-offset: 0.15em;
}
.btn--primary {
color: var(--white);
background: linear-gradient(
90deg,
var(--galaxy-blue) 0%,
var(--cosmic-azure) 70%,
var(--cherenkov-blue) 100%
);
}
.btn--primary:hover {
filter: brightness(1.1);
}
/* Preferences
========================================================================== */
/* Windows High Contrast: drop the decorative hero background behind system text */
@media (forced-colors: active) {
.page-hero {
background-image: none;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
}
}
/* Preview frame
========================================================================== */
/* There is no site header above the hero */
.page-hero {
margin-top: 0;
padding-top: 3rem;
} }
img { display: block; max-width: 100%; height: auto; border: 1px solid var(--border); border-radius: var(--radius); }
img + em, p > em:only-child { color: var(--muted); font-size: var(--fs-s); }
hr { border: 0; border-top: 1px solid var(--border); margin: 32px 0; }
table { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; margin: 0 0 16px; font-size: var(--fs-s); }
th { text-align: left; font-weight: 600; color: var(--galaxy); }
th, td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
thead th { border-bottom: 2px solid var(--galaxy); }
Binary file not shown.
Binary file not shown.
-102
View File
@@ -1,102 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 153 32" style="enable-background:new 0 0 153 32;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;}
.st2{display:inline;clip-path:url(#SVGID_00000049184525615463858320000015264053225314800771_);}
.st3{fill:#FFFFFF;}
.st4{fill:#00E5D9;}
.st5{fill:#00E4D8;}
</style>
<g class="st0">
<defs>
<rect id="SVGID_1_" x="0.9" width="151.4" height="32"/>
</defs>
<clipPath id="SVGID_00000109021271204444604050000000952066022526769593_" class="st1">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="display:inline;clip-path:url(#SVGID_00000109021271204444604050000000952066022526769593_);">
<path class="st3" d="M19.3,0c10.9,0,17.1,4.8,18,12.6h-9.1c-0.6-2.8-3.4-5.1-8.8-5.1c-6.3,0-9.4,3.2-9.4,8.5
c0,5.3,3.1,8.6,9.4,8.6c5.5,0,8.3-2.4,8.8-5.2h9.1c-0.9,7.9-7,12.7-18,12.7C7.4,32,0.9,26.4,0.9,16C0.9,5.6,7.4,0,19.3,0z"/>
<path class="st3" d="M62.3,8.4c0.2-0.1,0.4-0.1,0.6-0.1c4,0,8,0,12.1,0c0.2,0,0.3,0,0.5,0c0.1,0,0.2,0,0.3-0.1c0-0.1,0-0.2,0-0.3
c0-2.3,0-4.6,0-6.9c0-0.1,0-0.2,0-0.3c-0.2,0-0.5,0-0.7,0c-11.4,0-22.8,0-34.3,0c-0.2,0-0.3,0-0.4,0c-0.1,0-0.2,0-0.3,0.1
c0,0.2,0,0.4,0,0.5c0,0.8,0,1.6,0,2.4c0,1.3,0,2.6,0,3.9c0,0.2,0,0.4,0.1,0.5c0.2,0.1,0.4,0.1,0.6,0.1c4,0,8.1,0,12.1,0
c0.2,0,0.3,0,0.5,0c0.1,0,0.2,0,0.3,0.1c0,0.2,0,0.5,0,0.7c0,6.6,0,13.1,0,19.7c0,0.2,0,0.4,0,0.5c0,0.1,0,0.2,0,0.3
c0.1,0,0.1,0,0.1,0c2.7-0.5,5.4-1.1,8.1-1.6c0.1,0,0.2-0.1,0.3-0.1c0-0.2,0-0.4,0-0.6c0-6,0-12,0-18.1c0-0.2,0-0.4,0-0.5
C62.2,8.6,62.3,8.5,62.3,8.4L62.3,8.4z M53.7,30.2c0,0.3-0.1,0.6,0,0.8c0.4,0.1,6.6,0.1,8.2,0c0.1,0,0.2,0,0.3,0
c0-0.1,0.1-0.1,0.1-0.2c0-0.6,0-1.3,0-1.9c0,0,0-0.1-0.1-0.2C59.4,29.2,56.5,29.7,53.7,30.2L53.7,30.2z"/>
<path class="st3" d="M151.5,11.4c0.1,0,0.2,0,0.2,0c0.1,0.1,0.1,0.2,0.1,0.3c0.1,0.6,0.2,1.3,0.3,1.9c0.2,1.3,0.2,2.6,0.1,3.8
c-0.1,2-0.6,4-1.5,5.9c-1,2.2-2.6,3.9-4.5,5.3c-1.2,0.8-2.5,1.5-3.9,2c-1.1,0.4-2.3,0.7-3.4,0.9c-0.5,0.1-1.1,0.2-1.6,0.3
c-0.5,0.1-0.9,0.1-1.4,0.1c-0.1,0-0.1,0-0.2,0c-0.7,0.1-1.5,0.1-2.2,0.1c-0.7,0-1.5,0-2.2,0c-0.3,0-0.5,0-0.8-0.1
c-0.1,0-0.3,0-0.4,0c-2.2-0.2-4.3-0.6-6.4-1.3c-1.4-0.5-2.7-1.2-4-2c-2.7-1.9-4.5-4.5-5.4-7.7c-0.1-0.5-0.3-1.1-0.3-1.6
c0-0.1,0-0.1,0-0.2c0.1-0.1,0.2-0.1,0.4-0.2c0.5-0.1,1.1-0.2,1.6-0.3c0.9-0.2,1.8-0.4,2.7-0.5c0.9-0.2,1.8-0.4,2.7-0.6
c0.3-0.1,0.5-0.1,0.8-0.2c0.2,0,0.3-0.1,0.5-0.1c0.1,0,0.1,0.1,0.2,0.2c0,0.4,0.1,0.8,0.2,1.2c0.2,0.9,0.6,1.8,1.2,2.6
c0.6,0.8,1.3,1.4,2.2,1.9c0.7,0.4,1.5,0.7,2.4,0.9c0.6,0.1,1.2,0.3,1.8,0.3c0.8,0.1,1.7,0.2,2.5,0.2c0.4,0,0.7,0,1.1,0
c0.1,0,0.3,0,0.4,0c1.3-0.1,2.5-0.3,3.7-0.7c0.5-0.2,1-0.4,1.5-0.7c1.6-0.9,2.7-2.3,3.2-4.1c0.2-0.6,0.3-1.2,0.3-1.8
c0-0.5,0.1-1,0.1-1.4c0-0.7-0.1-1.4-0.2-2.1c0-0.1,0-0.2,0-0.3c0,0,0-0.1,0-0.1c0,0,0.1,0,0.1-0.1c0.1,0,0.3-0.1,0.4-0.1
c1.5-0.3,2.9-0.6,4.4-0.9c0.7-0.1,1.5-0.3,2.2-0.4c0.2,0,0.4-0.1,0.6-0.1c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0-0.1
C151.2,11.3,151.3,11.3,151.5,11.4C151.5,11.4,151.5,11.4,151.5,11.4L151.5,11.4z"/>
<path class="st3" d="M151.1,11.5c-0.4,0-0.7,0.1-1.1,0.1c-0.7,0.1-1.4,0.2-2.1,0.4c-0.7,0.1-1.3,0.2-2,0.4c-0.7,0.1-1.3,0.2-2,0.3
c-0.1,0-0.3,0.1-0.4,0.1c-0.1,0-0.3,0-0.4,0c-0.1-0.2-0.2-0.3-0.2-0.5c-0.5-1.4-1.4-2.4-2.6-3.2c-0.5-0.3-1-0.6-1.5-0.8
c-0.7-0.3-1.5-0.5-2.3-0.6c-1.3-0.2-2.6-0.3-3.8-0.3c-0.7,0-1.4,0.1-2.1,0.2c-0.8,0.1-1.6,0.3-2.4,0.5c-0.5,0.2-1,0.4-1.4,0.6
c-1.8,1-3,2.5-3.5,4.5c-0.2,0.7-0.3,1.5-0.3,2.2c0,0.2,0,0.4,0,0.7c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.3,0.1-0.4,0.1
c-0.6,0.1-1.3,0.2-1.9,0.3c-0.6,0.1-1.3,0.2-1.9,0.3c-0.7,0.1-1.4,0.2-2.1,0.4c-0.7,0.1-1.3,0.2-2,0.4c-0.1,0-0.3,0.1-0.4,0
c-0.1,0-0.1-0.1-0.1-0.2c0-0.1,0-0.3,0-0.4c0-0.9-0.1-1.8,0-2.7c0.1-2.3,0.7-4.5,1.8-6.6c1.1-2,2.6-3.6,4.4-4.8
c1.3-0.9,2.7-1.5,4.2-2c1.1-0.4,2.3-0.6,3.4-0.8c0.6-0.1,1.3-0.2,1.9-0.3c0.6-0.1,1.3-0.1,1.9-0.1c1.2,0,2.4,0,3.6,0
c0.2,0,0.4,0,0.5,0c0.1,0,0.2,0,0.3,0c0.8,0,1.6,0.2,2.5,0.3c1.3,0.2,2.5,0.5,3.7,0.9c1.4,0.5,2.8,1.1,4.1,2
c1.9,1.3,3.4,2.9,4.5,5c0.4,0.8,0.8,1.7,1,2.6c0,0.1,0,0.2,0.1,0.2c0,0,0,0.1,0,0.1c-0.1,0-0.1,0.1-0.2,0.1l0,0
C151.4,11.5,151.2,11.4,151.1,11.5L151.1,11.5L151.1,11.5z"/>
<path class="st3" d="M151,11.5c0.1-0.1,0.1-0.1,0.2-0.1c0.1,0,0.2,0,0.3,0C151.4,11.5,151.2,11.5,151,11.5L151,11.5z"/>
<path class="st3" d="M84.5,23.5C84.5,23.4,84.6,23.4,84.5,23.5c2.7-5.4,5.3-10.8,8-16.1c0,0,0-0.1,0.1-0.1c0,0,0-0.1,0.1-0.1
c0,0,0,0,0,0c0,0,0,0,0.1,0c0.1,0.1,0.2,0.3,0.2,0.4c0.9,1.8,1.7,3.5,2.6,5.3c1.2,2.4,2.4,4.8,3.6,7.2c0.1,0.2,0.1,0.3,0.3,0.5
c0.6-0.1,1.2-0.2,1.8-0.3c1.1-0.2,2.1-0.4,3.2-0.5c1-0.2,2-0.3,3-0.5c0.1,0,0.2-0.1,0.3-0.1c-0.1-0.2-0.2-0.3-0.2-0.5
c-1.4-2.8-2.8-5.6-4.2-8.4c-1.5-2.9-2.9-5.8-4.4-8.7c-0.1-0.2-0.1-0.4-0.3-0.5c-0.1,0-0.1,0-0.2,0c-3.8,0-7.6,0-11.4,0
c-0.1,0-0.1,0-0.2,0c0,0,0,0-0.1,0c0,0,0,0-0.1,0C82.6,9,78.5,17.2,74.4,25.3c0,0,0,0.1,0,0.1c0,0,0.1,0.1,0.1,0
C77.8,24.8,81.2,24.1,84.5,23.5z M100,21.8c0.3,0.7,4.4,9,4.6,9.3c0.1,0,0.2,0,0.3,0c2.9,0,5.8,0,8.7,0c0,0,0,0,0.1,0
c0,0,0,0,0.1-0.1c0-0.1-0.1-0.2-0.1-0.2c-0.1-0.2-0.3-0.5-0.4-0.8c-1.6-3.1-3.2-6.3-4.7-9.4c-0.1-0.2-0.1-0.3-0.3-0.5
C105.5,20.7,102.8,21.2,100,21.8L100,21.8z M73.7,26.8c-0.7,1.4-1.4,2.7-2,4.1c0,0.1,0,0.2-0.1,0.3h9.1c0.2-0.3,2.6-5,2.9-5.8
c0-0.1,0-0.1,0.1-0.2c-0.1,0-0.2,0-0.3,0C80.2,25.6,76.9,26.2,73.7,26.8L73.7,26.8z M93.8,29C93.8,29,93.8,29,93.8,29
c0.1-0.5,0.1-1,0-1.5c0-0.5-0.1-1.1,0-1.6c0-0.3,0.1-0.7,0.2-1c0.3-1.2,1.1-2,2.1-2.4c0.5-0.2,1.1-0.4,1.6-0.6
c0.4-0.2,0.9-0.3,1.3-0.5c-0.1-0.1-0.2-0.1-0.3-0.1c-1,0-1.9,0-2.9,0c-0.9,0-1.7-0.4-2.4-1c-0.4-0.4-0.8-0.9-1-1.5
c-0.3-0.8-0.6-1.6-0.8-2.4c0-0.1,0-0.1-0.2-0.1c0,0.2,0,0.3,0,0.5c0,0.8,0,1.7,0,2.5c0,0.8-0.2,1.5-0.7,2.1
c-0.5,0.7-1.1,1.1-1.8,1.3c-0.8,0.3-1.6,0.6-2.4,0.9c-0.1,0-0.1,0.1-0.2,0.1c0.1,0.1,0.3,0.1,0.4,0.1c0.9,0,1.8,0,2.8,0
c0.2,0,0.4,0,0.6,0.1c1.3,0.3,2.1,1.1,2.6,2.3c0.1,0.2,0.2,0.5,0.3,0.7C93.3,27.6,93.5,28.3,93.8,29L93.8,29z"/>
<path class="st4" d="M93.8,29C93.8,29,93.8,29,93.8,29c0.1-0.5,0.1-1,0-1.5c0-0.5-0.1-1.1,0-1.6c0-0.3,0.1-0.7,0.2-1
c0.3-1.2,1.1-2,2.1-2.4c0.5-0.2,1.1-0.4,1.6-0.6c0.4-0.2,0.8-0.3,1.3-0.5c-0.1-0.1-0.2-0.1-0.3-0.1c-1,0-1.9,0-2.9,0
c-0.9,0-1.7-0.4-2.4-1c-0.4-0.4-0.8-0.9-1-1.5c-0.3-0.8-0.6-1.6-0.8-2.4c0-0.1,0-0.1-0.2-0.1c0,0.2,0,0.3,0,0.5c0,0.8,0,1.7,0,2.5
c0,0.8-0.2,1.5-0.7,2.1c-0.5,0.7-1.1,1.1-1.8,1.3c-0.8,0.3-1.6,0.6-2.4,0.9c-0.1,0-0.1,0.1-0.2,0.1c0.1,0.1,0.3,0.1,0.4,0.1
c0.9,0,1.8,0,2.8,0c0.2,0,0.4,0,0.6,0.1c1.3,0.3,2.1,1.1,2.6,2.3c0.1,0.2,0.2,0.5,0.3,0.7C93.3,27.6,93.5,28.3,93.8,29L93.8,29z"
/>
</g>
</g>
<g id="Logotype">
<g>
<g>
<polygon class="st3" points="100.4,22.3 104.7,30.8 113.9,30.8 108.6,20.6 "/>
<g>
<path class="st3" d="M122.9,16.1c0,0,0-0.1,0-0.1c0-5.2,2.9-8.3,10.2-8.3c5.7,0,8.8,1.9,9.8,5.2l8.9-1.4
c-1.9-7-8.3-11.1-18.7-11.1c-12.5,0-19.3,5.8-19.3,15.6c0,0.5,0,1.1,0.1,1.6L122.9,16.1L122.9,16.1z"/>
<path class="st3" d="M143.1,13.3c0.2,0.8,0.3,1.7,0.3,2.6c0,5.2-3,8.4-10.3,8.4c-6.5,0-9.6-2.5-10.1-6.8l-8.8,1.8
c1.4,7.8,8,12.2,18.9,12.2c12.4,0,19.3-5.9,19.3-15.7c0-1.6-0.2-3.1-0.5-4.5L143.1,13.3L143.1,13.3z"/>
</g>
<g>
<polygon class="st3" points="73.5,27 71.5,30.8 80.8,30.8 83.5,25.4 "/>
<polygon class="st3" points="98.6,1.2 86.8,1.2 74.6,24.7 84.9,22.6 92.7,7.2 99.2,19.9 107.6,18.6 "/>
</g>
<g>
<g>
<polygon class="st3" points="53.6,30.2 53.6,30.8 62.2,30.8 62.2,28.8 "/>
<polygon class="st3" points="53.6,29.1 62.2,27.3 62.2,8.5 75.7,8.5 75.7,1.2 40.1,1.2 40.1,8.5 53.6,8.5 "/>
</g>
<path class="st3" d="M19.3,0.4c10.9,0,17.1,4.7,18,12.3h-9.1c-0.6-2.7-3.4-5-8.8-5c-6.3,0-9.4,3.1-9.4,8.3
c0,5.2,3.1,8.4,9.4,8.4c5.5,0,8.3-2.3,8.8-5.1h9.1c-0.9,7.7-7.1,12.4-18,12.4C7.4,31.6,0.9,26.1,0.9,16S7.4,0.4,19.3,0.4
L19.3,0.4z"/>
</g>
</g>
<path class="st5" 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.8
c-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"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.1 KiB

-129
View File
@@ -1,129 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 153 32" style="enable-background:new 0 0 153 32;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;}
.st2{display:inline;clip-path:url(#SVGID_00000021077498389242353310000002629454631782585475_);}
.st3{fill:#00004A;}
.st4{fill:#00E5D9;}
.st5{fill:#00E4D8;}
</style>
<g class="st0">
<defs>
<rect id="SVGID_1_" x="0.9" width="151.4" height="32"/>
</defs>
<clipPath id="SVGID_00000068637063279286702420000005043763006986931103_" class="st1">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="display:inline;clip-path:url(#SVGID_00000068637063279286702420000005043763006986931103_);">
<path class="st3" d="M19.3,0c10.9,0,17.1,4.8,18,12.6h-9.1c-0.6-2.8-3.4-5.1-8.8-5.1c-6.3,0-9.4,3.2-9.4,8.5
c0,5.3,3.1,8.6,9.4,8.6c5.5,0,8.3-2.4,8.8-5.2h9.1c-0.9,7.9-7,12.7-18,12.7C7.4,32,0.9,26.4,0.9,16C0.9,5.6,7.4,0,19.3,0z"/>
<path class="st3" d="M62.3,8.4c0.2-0.1,0.4-0.1,0.6-0.1c4,0,8,0,12.1,0c0.2,0,0.3,0,0.5,0c0.1,0,0.2,0,0.3-0.1c0-0.1,0-0.2,0-0.3
c0-2.3,0-4.6,0-6.9c0-0.1,0-0.2,0-0.3c-0.2,0-0.5,0-0.7,0c-11.4,0-22.8,0-34.3,0c-0.2,0-0.3,0-0.4,0c-0.1,0-0.2,0-0.3,0.1
c0,0.2,0,0.4,0,0.5c0,0.8,0,1.6,0,2.4c0,1.3,0,2.6,0,3.9c0,0.2,0,0.4,0.1,0.5c0.2,0.1,0.4,0.1,0.6,0.1c4,0,8.1,0,12.1,0
c0.2,0,0.3,0,0.5,0c0.1,0,0.2,0,0.3,0.1c0,0.2,0,0.5,0,0.7c0,6.6,0,13.1,0,19.7c0,0.2,0,0.4,0,0.5c0,0.1,0,0.2,0,0.3
c0.1,0,0.1,0,0.1,0c2.7-0.5,5.4-1.1,8.1-1.6c0.1,0,0.2-0.1,0.3-0.1c0-0.2,0-0.4,0-0.6c0-6,0-12,0-18.1c0-0.2,0-0.4,0-0.5
C62.2,8.6,62.3,8.5,62.3,8.4L62.3,8.4z M53.7,30.2c0,0.3-0.1,0.6,0,0.8c0.4,0.1,6.6,0.1,8.2,0c0.1,0,0.2,0,0.3,0
c0-0.1,0.1-0.1,0.1-0.2c0-0.6,0-1.3,0-1.9c0,0,0-0.1-0.1-0.2C59.4,29.2,56.5,29.7,53.7,30.2L53.7,30.2z"/>
<path class="st3" d="M151.5,11.4c0.1,0,0.2,0,0.2,0c0.1,0.1,0.1,0.2,0.1,0.3c0.1,0.6,0.2,1.3,0.3,1.9c0.2,1.3,0.2,2.6,0.1,3.8
c-0.1,2-0.6,4-1.5,5.9c-1,2.2-2.6,3.9-4.5,5.3c-1.2,0.8-2.5,1.5-3.9,2c-1.1,0.4-2.3,0.7-3.4,0.9c-0.5,0.1-1.1,0.2-1.6,0.3
c-0.5,0.1-0.9,0.1-1.4,0.1c-0.1,0-0.1,0-0.2,0c-0.7,0.1-1.5,0.1-2.2,0.1c-0.7,0-1.5,0-2.2,0c-0.3,0-0.5,0-0.8-0.1
c-0.1,0-0.3,0-0.4,0c-2.2-0.2-4.3-0.6-6.4-1.3c-1.4-0.5-2.7-1.2-4-2c-2.7-1.9-4.5-4.5-5.4-7.7c-0.1-0.5-0.3-1.1-0.3-1.6
c0-0.1,0-0.1,0-0.2c0.1-0.1,0.2-0.1,0.4-0.2c0.5-0.1,1.1-0.2,1.6-0.3c0.9-0.2,1.8-0.4,2.7-0.5c0.9-0.2,1.8-0.4,2.7-0.6
c0.3-0.1,0.5-0.1,0.8-0.2c0.2,0,0.3-0.1,0.5-0.1c0.1,0,0.1,0.1,0.2,0.2c0,0.4,0.1,0.8,0.2,1.2c0.2,0.9,0.6,1.8,1.2,2.6
c0.6,0.8,1.3,1.4,2.2,1.9c0.7,0.4,1.5,0.7,2.4,0.9c0.6,0.1,1.2,0.3,1.8,0.3c0.8,0.1,1.7,0.2,2.5,0.2c0.4,0,0.7,0,1.1,0
c0.1,0,0.3,0,0.4,0c1.3-0.1,2.5-0.3,3.7-0.7c0.5-0.2,1-0.4,1.5-0.7c1.6-0.9,2.7-2.3,3.2-4.1c0.2-0.6,0.3-1.2,0.3-1.8
c0-0.5,0.1-1,0.1-1.4c0-0.7-0.1-1.4-0.2-2.1c0-0.1,0-0.2,0-0.3c0,0,0-0.1,0-0.1c0,0,0.1,0,0.1-0.1c0.1,0,0.3-0.1,0.4-0.1
c1.5-0.3,2.9-0.6,4.4-0.9c0.7-0.1,1.5-0.3,2.2-0.4c0.2,0,0.4-0.1,0.6-0.1c0.1,0,0.1-0.1,0.2-0.1c0,0,0,0,0-0.1
C151.2,11.3,151.3,11.3,151.5,11.4C151.5,11.4,151.5,11.4,151.5,11.4L151.5,11.4z"/>
<path class="st3" d="M151.1,11.5c-0.4,0-0.7,0.1-1.1,0.1c-0.7,0.1-1.4,0.2-2.1,0.4c-0.7,0.1-1.3,0.2-2,0.4c-0.7,0.1-1.3,0.2-2,0.3
c-0.1,0-0.3,0.1-0.4,0.1c-0.1,0-0.3,0-0.4,0c-0.1-0.2-0.2-0.3-0.2-0.5c-0.5-1.4-1.4-2.4-2.6-3.2c-0.5-0.3-1-0.6-1.5-0.8
c-0.7-0.3-1.5-0.5-2.3-0.6c-1.3-0.2-2.6-0.3-3.8-0.3c-0.7,0-1.4,0.1-2.1,0.2c-0.8,0.1-1.6,0.3-2.4,0.5c-0.5,0.2-1,0.4-1.4,0.6
c-1.8,1-3,2.5-3.5,4.5c-0.2,0.7-0.3,1.5-0.3,2.2c0,0.2,0,0.4,0,0.7c0,0.1,0,0.2-0.1,0.2c-0.1,0.1-0.3,0.1-0.4,0.1
c-0.6,0.1-1.3,0.2-1.9,0.3c-0.6,0.1-1.3,0.2-1.9,0.3c-0.7,0.1-1.4,0.2-2.1,0.4c-0.7,0.1-1.3,0.2-2,0.4c-0.1,0-0.3,0.1-0.4,0
c-0.1,0-0.1-0.1-0.1-0.2c0-0.1,0-0.3,0-0.4c0-0.9-0.1-1.8,0-2.7c0.1-2.3,0.7-4.5,1.8-6.6c1.1-2,2.6-3.6,4.4-4.8
c1.3-0.9,2.7-1.5,4.2-2c1.1-0.4,2.3-0.6,3.4-0.8c0.6-0.1,1.3-0.2,1.9-0.3c0.6-0.1,1.3-0.1,1.9-0.1c1.2,0,2.4,0,3.6,0
c0.2,0,0.4,0,0.5,0c0.1,0,0.2,0,0.3,0c0.8,0,1.6,0.2,2.5,0.3c1.3,0.2,2.5,0.5,3.7,0.9c1.4,0.5,2.8,1.1,4.1,2
c1.9,1.3,3.4,2.9,4.5,5c0.4,0.8,0.8,1.7,1,2.6c0,0.1,0,0.2,0.1,0.2c0,0,0,0.1,0,0.1c-0.1,0-0.1,0.1-0.2,0.1l0,0
C151.4,11.5,151.2,11.4,151.1,11.5L151.1,11.5L151.1,11.5z"/>
<path class="st3" d="M151,11.5c0.1-0.1,0.1-0.1,0.2-0.1c0.1,0,0.2,0,0.3,0C151.4,11.5,151.2,11.5,151,11.5L151,11.5z"/>
<path class="st3" d="M84.5,23.5C84.5,23.4,84.6,23.4,84.5,23.5c2.7-5.4,5.3-10.8,8-16.1c0,0,0-0.1,0.1-0.1c0,0,0-0.1,0.1-0.1
c0,0,0,0,0,0c0,0,0,0,0.1,0c0.1,0.1,0.2,0.3,0.2,0.4c0.9,1.8,1.7,3.5,2.6,5.3c1.2,2.4,2.4,4.8,3.6,7.2c0.1,0.2,0.1,0.3,0.3,0.5
c0.6-0.1,1.2-0.2,1.8-0.3c1.1-0.2,2.1-0.4,3.2-0.5c1-0.2,2-0.3,3-0.5c0.1,0,0.2-0.1,0.3-0.1c-0.1-0.2-0.2-0.3-0.2-0.5
c-1.4-2.8-2.8-5.6-4.2-8.4c-1.5-2.9-2.9-5.8-4.4-8.7c-0.1-0.2-0.1-0.4-0.3-0.5c-0.1,0-0.1,0-0.2,0c-3.8,0-7.6,0-11.4,0
c-0.1,0-0.1,0-0.2,0c0,0,0,0-0.1,0c0,0,0,0-0.1,0C82.6,9,78.5,17.2,74.4,25.3c0,0,0,0.1,0,0.1c0,0,0.1,0.1,0.1,0
C77.8,24.8,81.2,24.1,84.5,23.5z M100,21.8c0.3,0.7,4.4,9,4.6,9.3c0.1,0,0.2,0,0.3,0c2.9,0,5.8,0,8.7,0c0,0,0,0,0.1,0
c0,0,0,0,0.1-0.1c0-0.1-0.1-0.2-0.1-0.2c-0.1-0.2-0.3-0.5-0.4-0.8c-1.6-3.1-3.2-6.3-4.7-9.4c-0.1-0.2-0.1-0.3-0.3-0.5
C105.5,20.7,102.8,21.2,100,21.8L100,21.8z M73.7,26.8c-0.7,1.4-1.4,2.7-2,4.1c0,0.1,0,0.2-0.1,0.3h9.1c0.2-0.3,2.6-5,2.9-5.8
c0-0.1,0-0.1,0.1-0.2c-0.1,0-0.2,0-0.3,0C80.2,25.6,76.9,26.2,73.7,26.8L73.7,26.8z M93.8,29C93.8,29,93.8,29,93.8,29
c0.1-0.5,0.1-1,0-1.5c0-0.5-0.1-1.1,0-1.6c0-0.3,0.1-0.7,0.2-1c0.3-1.2,1.1-2,2.1-2.4c0.5-0.2,1.1-0.4,1.6-0.6
c0.4-0.2,0.9-0.3,1.3-0.5c-0.1-0.1-0.2-0.1-0.3-0.1c-1,0-1.9,0-2.9,0c-0.9,0-1.7-0.4-2.4-1c-0.4-0.4-0.8-0.9-1-1.5
c-0.3-0.8-0.6-1.6-0.8-2.4c0-0.1,0-0.1-0.2-0.1c0,0.2,0,0.3,0,0.5c0,0.8,0,1.7,0,2.5c0,0.8-0.2,1.5-0.7,2.1
c-0.5,0.7-1.1,1.1-1.8,1.3c-0.8,0.3-1.6,0.6-2.4,0.9c-0.1,0-0.1,0.1-0.2,0.1c0.1,0.1,0.3,0.1,0.4,0.1c0.9,0,1.8,0,2.8,0
c0.2,0,0.4,0,0.6,0.1c1.3,0.3,2.1,1.1,2.6,2.3c0.1,0.2,0.2,0.5,0.3,0.7C93.3,27.6,93.5,28.3,93.8,29L93.8,29z"/>
<path class="st4" d="M93.8,29C93.8,29,93.8,29,93.8,29c0.1-0.5,0.1-1,0-1.5c0-0.5-0.1-1.1,0-1.6c0-0.3,0.1-0.7,0.2-1
c0.3-1.2,1.1-2,2.1-2.4c0.5-0.2,1.1-0.4,1.6-0.6c0.4-0.2,0.8-0.3,1.3-0.5c-0.1-0.1-0.2-0.1-0.3-0.1c-1,0-1.9,0-2.9,0
c-0.9,0-1.7-0.4-2.4-1c-0.4-0.4-0.8-0.9-1-1.5c-0.3-0.8-0.6-1.6-0.8-2.4c0-0.1,0-0.1-0.2-0.1c0,0.2,0,0.3,0,0.5c0,0.8,0,1.7,0,2.5
c0,0.8-0.2,1.5-0.7,2.1c-0.5,0.7-1.1,1.1-1.8,1.3c-0.8,0.3-1.6,0.6-2.4,0.9c-0.1,0-0.1,0.1-0.2,0.1c0.1,0.1,0.3,0.1,0.4,0.1
c0.9,0,1.8,0,2.8,0c0.2,0,0.4,0,0.6,0.1c1.3,0.3,2.1,1.1,2.6,2.3c0.1,0.2,0.2,0.5,0.3,0.7C93.3,27.6,93.5,28.3,93.8,29L93.8,29z"
/>
</g>
</g>
<g id="Logotype" class="st0">
<g class="st1">
<g>
<polygon class="st3" points="100.4,22.3 104.7,30.8 113.9,30.8 108.6,20.6 "/>
<g>
<path class="st3" d="M122.9,16.1c0,0,0-0.1,0-0.1c0-5.2,2.9-8.3,10.2-8.3c5.7,0,8.8,1.9,9.8,5.2l8.9-1.4
c-1.9-7-8.3-11.1-18.7-11.1c-12.5,0-19.3,5.8-19.3,15.6c0,0.5,0,1.1,0.1,1.6L122.9,16.1L122.9,16.1z"/>
<path class="st3" d="M143.1,13.3c0.2,0.8,0.3,1.7,0.3,2.6c0,5.2-3,8.4-10.3,8.4c-6.5,0-9.6-2.5-10.1-6.8l-8.8,1.8
c1.4,7.8,8,12.2,18.9,12.2c12.4,0,19.3-5.9,19.3-15.7c0-1.6-0.2-3.1-0.5-4.5L143.1,13.3L143.1,13.3z"/>
</g>
<g>
<polygon class="st3" points="73.5,27 71.5,30.8 80.8,30.8 83.5,25.4 "/>
<polygon class="st3" points="98.6,1.2 86.8,1.2 74.6,24.7 84.9,22.6 92.7,7.2 99.2,19.9 107.6,18.6 "/>
</g>
<g>
<g>
<polygon class="st3" points="53.6,30.2 53.6,30.8 62.2,30.8 62.2,28.8 "/>
<polygon class="st3" points="53.6,29.1 62.2,27.3 62.2,8.5 75.7,8.5 75.7,1.2 40.1,1.2 40.1,8.5 53.6,8.5 "/>
</g>
<path class="st3" d="M19.3,0.4c10.9,0,17.1,4.7,18,12.3h-9.1c-0.6-2.7-3.4-5-8.8-5c-6.3,0-9.4,3.1-9.4,8.3
c0,5.2,3.1,8.4,9.4,8.4c5.5,0,8.3-2.3,8.8-5.1h9.1c-0.9,7.7-7.1,12.4-18,12.4C7.4,31.6,0.9,26.1,0.9,16S7.4,0.4,19.3,0.4
L19.3,0.4z"/>
</g>
</g>
<path class="st5" 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.8
c-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"/>
</g>
</g>
<g id="Logotype_00000134236662009793237430000016451412871672361145_">
<g>
<g>
<polygon class="st3" points="100,22.3 104.3,30.7 113.5,30.7 108.2,20.5 "/>
<g>
<path class="st3" d="M122.4,16.1c0,0,0-0.1,0-0.1c0-5.2,2.9-8.2,10.2-8.2c5.7,0,8.7,1.9,9.8,5.2l8.8-1.4c-1.8-7-8.2-11-18.6-11
c-12.4,0-19.1,5.7-19.1,15.5c0,0.5,0,1.1,0.1,1.6L122.4,16.1L122.4,16.1z"/>
<path class="st3" d="M142.5,13.4c0.2,0.8,0.3,1.7,0.3,2.6c0,5.1-3,8.3-10.2,8.3c-6.4,0-9.5-2.5-10.1-6.7l-8.7,1.8
c1.4,7.7,7.9,12.2,18.8,12.2c12.4,0,19.1-5.8,19.1-15.6c0-1.6-0.2-3.1-0.5-4.4L142.5,13.4L142.5,13.4z"/>
</g>
<g>
<polygon class="st3" points="73.4,26.9 71.4,30.7 80.6,30.7 83.3,25.3 "/>
<polygon class="st3" points="98.4,1.3 86.6,1.3 74.5,24.7 84.7,22.5 92.5,7.3 98.9,19.9 107.2,18.6 "/>
</g>
<g>
<g>
<polygon class="st3" points="53.7,30.1 53.7,30.7 62.2,30.7 62.2,28.7 "/>
<polygon class="st3" points="53.7,29 62.2,27.3 62.2,8.5 75.6,8.5 75.6,1.3 40.2,1.3 40.2,8.5 53.7,8.5 "/>
</g>
<path class="st3" d="M19.6,0.5c10.8,0,17,4.7,17.8,12.2h-9.1c-0.6-2.7-3.3-5-8.8-5c-6.3,0-9.4,3.1-9.4,8.2s3.1,8.3,9.4,8.3
c5.4,0,8.2-2.3,8.8-5.1h9.1c-0.9,7.6-7,12.3-17.8,12.3C7.8,31.5,1.3,26.1,1.3,16S7.8,0.5,19.6,0.5L19.6,0.5z"/>
</g>
</g>
<path class="st3" d="M94.4,23.7c1-1.4,3.3-2.1,5.1-2.7c-2,0.1-4.4,0.3-5.8-0.6c-1.4-1-2.1-3.3-2.7-5.1c0.1,2,0.3,4.4-0.6,5.8
c-1,1.4-3.3,2.1-5.1,2.7c2-0.1,4.4-0.3,5.8,0.6c1.4,1,2.1,3.3,2.7,5.1C93.6,27.5,93.4,25.1,94.4,23.7z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.6 KiB

-11
View File
@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Ebene_1" x="0" y="0" viewBox="0 0 153 46.0">
<style>
.st3{fill:#ffffff}
.st5{fill:#00E4D8}
</style>
<g id="Logotype_00000134236662009793237430000016451412871672361145_">
<path d="m100 22.3 4.3 8.4h9.2l-5.3-10.2zm22.4-6.2V16c0-5.2 2.9-8.2 10.2-8.2 5.7 0 8.7 1.9 9.8 5.2l8.8-1.4c-1.8-7-8.2-11-18.6-11-12.4 0-19.1 5.7-19.1 15.5 0 .5 0 1.1.1 1.6zm20.1-2.7c.2.8.3 1.7.3 2.6 0 5.1-3 8.3-10.2 8.3-6.4 0-9.5-2.5-10.1-6.7l-8.7 1.8c1.4 7.7 7.9 12.2 18.8 12.2 12.4 0 19.1-5.8 19.1-15.6 0-1.6-.2-3.1-.5-4.4zM73.4 26.9l-2 3.8h9.2l2.7-5.4zm25-25.6H86.6L74.5 24.7l10.2-2.2 7.8-15.2 6.4 12.6 8.3-1.3zM53.7 30.1v.6h8.5v-2zm0-1.1 8.5-1.7V8.5h13.4V1.3H40.2v7.2h13.5zM19.6.5c10.8 0 17 4.7 17.8 12.2h-9.1c-.6-2.7-3.3-5-8.8-5-6.3 0-9.4 3.1-9.4 8.2s3.1 8.3 9.4 8.3c5.4 0 8.2-2.3 8.8-5.1h9.1c-.9 7.6-7 12.3-17.8 12.3C7.8 31.5 1.3 26.1 1.3 16S7.8.5 19.6.5" class="st3"/>
<path d="M94.4 23.7c1-1.4 3.3-2.1 5.1-2.7-2 .1-4.4.3-5.8-.6-1.4-1-2.1-3.3-2.7-5.1.1 2 .3 4.4-.6 5.8-1 1.4-3.3 2.1-5.1 2.7 2-.1 4.4-.3 5.8.6 1.4 1 2.1 3.3 2.7 5.1-.2-2-.4-4.4.6-5.8" class="st5"/>
</g>
<path d="M6.48 45.77Q5.41 45.77 4.62 45.44Q3.83 45.1 3.38 44.47Q2.94 43.83 2.9 42.92H4.53Q4.57 43.4 4.83 43.72Q5.09 44.04 5.52 44.2Q5.95 44.36 6.47 44.36Q7.01 44.36 7.43 44.19Q7.85 44.03 8.08 43.72Q8.32 43.42 8.32 43.03Q8.32 42.67 8.11 42.43Q7.9 42.2 7.53 42.03Q7.15 41.87 6.65 41.75L5.6 41.47Q4.45 41.18 3.8 40.57Q3.15 39.97 3.15 38.99Q3.15 38.18 3.59 37.57Q4.03 36.95 4.8 36.61Q5.56 36.28 6.52 36.28Q7.51 36.28 8.25 36.62Q8.99 36.96 9.41 37.56Q9.83 38.16 9.85 38.93H8.25Q8.18 38.33 7.71 38.01Q7.25 37.68 6.5 37.68Q5.98 37.68 5.6 37.84Q5.23 38 5.03 38.27Q4.83 38.55 4.83 38.9Q4.83 39.28 5.06 39.53Q5.3 39.78 5.66 39.93Q6.03 40.08 6.4 40.17L7.26 40.39Q7.77 40.51 8.26 40.72Q8.74 40.92 9.13 41.24Q9.52 41.55 9.75 41.99Q9.97 42.44 9.97 43.04Q9.97 43.85 9.56 44.47Q9.15 45.08 8.37 45.43Q7.58 45.77 6.48 45.77ZM18.24 45.75Q17.03 45.75 16.08 45.19Q15.12 44.63 14.57 43.57Q14.02 42.51 14.02 41.02Q14.02 39.53 14.58 38.47Q15.13 37.4 16.08 36.84Q17.04 36.28 18.24 36.28Q19 36.28 19.66 36.49Q20.32 36.71 20.84 37.12Q21.35 37.53 21.69 38.12Q22.02 38.72 22.13 39.47H20.47Q20.39 39.06 20.19 38.74Q20 38.43 19.71 38.21Q19.41 37.98 19.05 37.87Q18.68 37.76 18.27 37.76Q17.51 37.76 16.93 38.14Q16.34 38.52 16.01 39.25Q15.68 39.97 15.68 41.02Q15.68 42.08 16.02 42.8Q16.35 43.53 16.93 43.9Q17.52 44.27 18.27 44.27Q18.68 44.27 19.04 44.16Q19.41 44.04 19.7 43.82Q20 43.6 20.19 43.28Q20.39 42.97 20.47 42.56H22.14Q22.05 43.22 21.73 43.8Q21.42 44.38 20.91 44.82Q20.41 45.26 19.74 45.51Q19.06 45.75 18.24 45.75ZM28.13 36.4V45.63H26.48V36.4ZM32.81 45.63V36.4H38.81V37.79H34.46V40.28H38.49V41.66H34.46V44.24H38.83V45.63ZM43.31 45.63V36.4H45.18L48.38 41.48Q48.57 41.8 48.8 42.2Q49.02 42.6 49.26 43.09Q49.5 43.58 49.74 44.19H49.55Q49.51 43.66 49.48 43.12Q49.44 42.57 49.43 42.07Q49.41 41.58 49.41 41.24V36.4H51.09V45.63H49.21L46.33 41.05Q46.06 40.61 45.82 40.19Q45.57 39.76 45.3 39.22Q45.02 38.68 44.62 37.92H44.85Q44.89 38.59 44.92 39.2Q44.96 39.8 44.98 40.28Q45 40.75 45 41.05V45.63ZM59.7 45.75Q58.49 45.75 57.53 45.19Q56.58 44.63 56.03 43.57Q55.48 42.51 55.48 41.02Q55.48 39.53 56.03 38.47Q56.58 37.4 57.54 36.84Q58.5 36.28 59.7 36.28Q60.46 36.28 61.12 36.49Q61.78 36.71 62.29 37.12Q62.81 37.53 63.14 38.12Q63.48 38.72 63.59 39.47H61.92Q61.84 39.06 61.65 38.74Q61.45 38.43 61.16 38.21Q60.87 37.98 60.51 37.87Q60.14 37.76 59.73 37.76Q58.97 37.76 58.38 38.14Q57.8 38.52 57.47 39.25Q57.14 39.97 57.14 41.02Q57.14 42.08 57.47 42.8Q57.81 43.53 58.39 43.9Q58.97 44.27 59.72 44.27Q60.14 44.27 60.5 44.16Q60.86 44.04 61.16 43.82Q61.45 43.6 61.65 43.28Q61.85 42.97 61.93 42.56H63.59Q63.5 43.22 63.19 43.8Q62.88 44.38 62.37 44.82Q61.86 45.26 61.19 45.51Q60.52 45.75 59.7 45.75ZM67.93 45.63V36.4H73.93V37.79H69.59V40.28H73.61V41.66H69.59V44.24H73.95V45.63ZM84.45 45.63V36.4H87.91Q88.97 36.4 89.69 36.8Q90.41 37.2 90.78 37.89Q91.15 38.57 91.15 39.46Q91.15 40.34 90.78 41.03Q90.41 41.72 89.68 42.11Q88.95 42.51 87.88 42.51H85.59V41.15H87.66Q88.29 41.15 88.69 40.93Q89.09 40.71 89.28 40.33Q89.46 39.95 89.46 39.46Q89.46 38.96 89.28 38.59Q89.09 38.21 88.69 38Q88.28 37.78 87.65 37.78H86.11V45.63ZM99.41 45.75Q98.2 45.75 97.24 45.19Q96.28 44.63 95.72 43.57Q95.17 42.51 95.17 41.02Q95.17 39.53 95.72 38.47Q96.28 37.4 97.24 36.84Q98.2 36.28 99.41 36.28Q100.62 36.28 101.57 36.84Q102.53 37.4 103.08 38.47Q103.64 39.53 103.64 41.02Q103.64 42.51 103.08 43.57Q102.53 44.63 101.57 45.19Q100.62 45.75 99.41 45.75ZM99.41 44.27Q100.16 44.27 100.74 43.9Q101.32 43.52 101.65 42.8Q101.98 42.07 101.98 41.02Q101.98 39.96 101.65 39.23Q101.32 38.51 100.74 38.13Q100.16 37.76 99.41 37.76Q98.65 37.76 98.07 38.13Q97.49 38.51 97.16 39.24Q96.83 39.97 96.83 41.02Q96.83 42.07 97.16 42.8Q97.49 43.52 98.07 43.9Q98.65 44.27 99.41 44.27ZM108.03 45.63V36.4H111.48Q112.54 36.4 113.26 36.77Q113.99 37.14 114.36 37.81Q114.73 38.47 114.73 39.35Q114.73 40.24 114.35 40.89Q113.98 41.54 113.25 41.89Q112.52 42.24 111.45 42.24H108.99V40.87H111.23Q111.86 40.87 112.26 40.69Q112.66 40.52 112.85 40.18Q113.04 39.84 113.04 39.35Q113.04 38.86 112.85 38.51Q112.66 38.16 112.26 37.97Q111.86 37.78 111.23 37.78H109.68V45.63ZM113.22 45.63 110.98 41.44H112.79L115.07 45.63ZM118.71 37.79V36.4H126.06V37.79H123.21V45.63H121.56V37.79ZM129.7 45.63 132.92 36.4H135.02L138.31 45.63H136.5L134.86 40.84Q134.62 40.09 134.36 39.15Q134.1 38.22 133.78 37.02H134.13Q133.82 38.23 133.57 39.17Q133.32 40.12 133.09 40.84L131.51 45.63ZM131.58 43.34V42.01H136.44V43.34ZM142.37 45.63V36.4H144.02V44.24H148.1V45.63Z" class="st3"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

-10
View File
@@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Ebene_1" x="0" y="0" viewBox="0 0 153 46.0">
<style>
.st3{fill:#00004a}
</style>
<g id="Logotype_00000134236662009793237430000016451412871672361145_">
<path d="m100 22.3 4.3 8.4h9.2l-5.3-10.2zm22.4-6.2V16c0-5.2 2.9-8.2 10.2-8.2 5.7 0 8.7 1.9 9.8 5.2l8.8-1.4c-1.8-7-8.2-11-18.6-11-12.4 0-19.1 5.7-19.1 15.5 0 .5 0 1.1.1 1.6zm20.1-2.7c.2.8.3 1.7.3 2.6 0 5.1-3 8.3-10.2 8.3-6.4 0-9.5-2.5-10.1-6.7l-8.7 1.8c1.4 7.7 7.9 12.2 18.8 12.2 12.4 0 19.1-5.8 19.1-15.6 0-1.6-.2-3.1-.5-4.4zM73.4 26.9l-2 3.8h9.2l2.7-5.4zm25-25.6H86.6L74.5 24.7l10.2-2.2 7.8-15.2 6.4 12.6 8.3-1.3zM53.7 30.1v.6h8.5v-2zm0-1.1 8.5-1.7V8.5h13.4V1.3H40.2v7.2h13.5zM19.6.5c10.8 0 17 4.7 17.8 12.2h-9.1c-.6-2.7-3.3-5-8.8-5-6.3 0-9.4 3.1-9.4 8.2s3.1 8.3 9.4 8.3c5.4 0 8.2-2.3 8.8-5.1h9.1c-.9 7.6-7 12.3-17.8 12.3C7.8 31.5 1.3 26.1 1.3 16S7.8.5 19.6.5" class="st3"/>
<path d="M94.4 23.7c1-1.4 3.3-2.1 5.1-2.7-2 .1-4.4.3-5.8-.6-1.4-1-2.1-3.3-2.7-5.1.1 2 .3 4.4-.6 5.8-1 1.4-3.3 2.1-5.1 2.7 2-.1 4.4-.3 5.8.6 1.4 1 2.1 3.3 2.7 5.1-.2-2-.4-4.4.6-5.8" class="st3"/>
</g>
<path d="M6.48 45.77Q5.41 45.77 4.62 45.44Q3.83 45.1 3.38 44.47Q2.94 43.83 2.9 42.92H4.53Q4.57 43.4 4.83 43.72Q5.09 44.04 5.52 44.2Q5.95 44.36 6.47 44.36Q7.01 44.36 7.43 44.19Q7.85 44.03 8.08 43.72Q8.32 43.42 8.32 43.03Q8.32 42.67 8.11 42.43Q7.9 42.2 7.53 42.03Q7.15 41.87 6.65 41.75L5.6 41.47Q4.45 41.18 3.8 40.57Q3.15 39.97 3.15 38.99Q3.15 38.18 3.59 37.57Q4.03 36.95 4.8 36.61Q5.56 36.28 6.52 36.28Q7.51 36.28 8.25 36.62Q8.99 36.96 9.41 37.56Q9.83 38.16 9.85 38.93H8.25Q8.18 38.33 7.71 38.01Q7.25 37.68 6.5 37.68Q5.98 37.68 5.6 37.84Q5.23 38 5.03 38.27Q4.83 38.55 4.83 38.9Q4.83 39.28 5.06 39.53Q5.3 39.78 5.66 39.93Q6.03 40.08 6.4 40.17L7.26 40.39Q7.77 40.51 8.26 40.72Q8.74 40.92 9.13 41.24Q9.52 41.55 9.75 41.99Q9.97 42.44 9.97 43.04Q9.97 43.85 9.56 44.47Q9.15 45.08 8.37 45.43Q7.58 45.77 6.48 45.77ZM18.24 45.75Q17.03 45.75 16.08 45.19Q15.12 44.63 14.57 43.57Q14.02 42.51 14.02 41.02Q14.02 39.53 14.58 38.47Q15.13 37.4 16.08 36.84Q17.04 36.28 18.24 36.28Q19 36.28 19.66 36.49Q20.32 36.71 20.84 37.12Q21.35 37.53 21.69 38.12Q22.02 38.72 22.13 39.47H20.47Q20.39 39.06 20.19 38.74Q20 38.43 19.71 38.21Q19.41 37.98 19.05 37.87Q18.68 37.76 18.27 37.76Q17.51 37.76 16.93 38.14Q16.34 38.52 16.01 39.25Q15.68 39.97 15.68 41.02Q15.68 42.08 16.02 42.8Q16.35 43.53 16.93 43.9Q17.52 44.27 18.27 44.27Q18.68 44.27 19.04 44.16Q19.41 44.04 19.7 43.82Q20 43.6 20.19 43.28Q20.39 42.97 20.47 42.56H22.14Q22.05 43.22 21.73 43.8Q21.42 44.38 20.91 44.82Q20.41 45.26 19.74 45.51Q19.06 45.75 18.24 45.75ZM28.13 36.4V45.63H26.48V36.4ZM32.81 45.63V36.4H38.81V37.79H34.46V40.28H38.49V41.66H34.46V44.24H38.83V45.63ZM43.31 45.63V36.4H45.18L48.38 41.48Q48.57 41.8 48.8 42.2Q49.02 42.6 49.26 43.09Q49.5 43.58 49.74 44.19H49.55Q49.51 43.66 49.48 43.12Q49.44 42.57 49.43 42.07Q49.41 41.58 49.41 41.24V36.4H51.09V45.63H49.21L46.33 41.05Q46.06 40.61 45.82 40.19Q45.57 39.76 45.3 39.22Q45.02 38.68 44.62 37.92H44.85Q44.89 38.59 44.92 39.2Q44.96 39.8 44.98 40.28Q45 40.75 45 41.05V45.63ZM59.7 45.75Q58.49 45.75 57.53 45.19Q56.58 44.63 56.03 43.57Q55.48 42.51 55.48 41.02Q55.48 39.53 56.03 38.47Q56.58 37.4 57.54 36.84Q58.5 36.28 59.7 36.28Q60.46 36.28 61.12 36.49Q61.78 36.71 62.29 37.12Q62.81 37.53 63.14 38.12Q63.48 38.72 63.59 39.47H61.92Q61.84 39.06 61.65 38.74Q61.45 38.43 61.16 38.21Q60.87 37.98 60.51 37.87Q60.14 37.76 59.73 37.76Q58.97 37.76 58.38 38.14Q57.8 38.52 57.47 39.25Q57.14 39.97 57.14 41.02Q57.14 42.08 57.47 42.8Q57.81 43.53 58.39 43.9Q58.97 44.27 59.72 44.27Q60.14 44.27 60.5 44.16Q60.86 44.04 61.16 43.82Q61.45 43.6 61.65 43.28Q61.85 42.97 61.93 42.56H63.59Q63.5 43.22 63.19 43.8Q62.88 44.38 62.37 44.82Q61.86 45.26 61.19 45.51Q60.52 45.75 59.7 45.75ZM67.93 45.63V36.4H73.93V37.79H69.59V40.28H73.61V41.66H69.59V44.24H73.95V45.63ZM84.45 45.63V36.4H87.91Q88.97 36.4 89.69 36.8Q90.41 37.2 90.78 37.89Q91.15 38.57 91.15 39.46Q91.15 40.34 90.78 41.03Q90.41 41.72 89.68 42.11Q88.95 42.51 87.88 42.51H85.59V41.15H87.66Q88.29 41.15 88.69 40.93Q89.09 40.71 89.28 40.33Q89.46 39.95 89.46 39.46Q89.46 38.96 89.28 38.59Q89.09 38.21 88.69 38Q88.28 37.78 87.65 37.78H86.11V45.63ZM99.41 45.75Q98.2 45.75 97.24 45.19Q96.28 44.63 95.72 43.57Q95.17 42.51 95.17 41.02Q95.17 39.53 95.72 38.47Q96.28 37.4 97.24 36.84Q98.2 36.28 99.41 36.28Q100.62 36.28 101.57 36.84Q102.53 37.4 103.08 38.47Q103.64 39.53 103.64 41.02Q103.64 42.51 103.08 43.57Q102.53 44.63 101.57 45.19Q100.62 45.75 99.41 45.75ZM99.41 44.27Q100.16 44.27 100.74 43.9Q101.32 43.52 101.65 42.8Q101.98 42.07 101.98 41.02Q101.98 39.96 101.65 39.23Q101.32 38.51 100.74 38.13Q100.16 37.76 99.41 37.76Q98.65 37.76 98.07 38.13Q97.49 38.51 97.16 39.24Q96.83 39.97 96.83 41.02Q96.83 42.07 97.16 42.8Q97.49 43.52 98.07 43.9Q98.65 44.27 99.41 44.27ZM108.03 45.63V36.4H111.48Q112.54 36.4 113.26 36.77Q113.99 37.14 114.36 37.81Q114.73 38.47 114.73 39.35Q114.73 40.24 114.35 40.89Q113.98 41.54 113.25 41.89Q112.52 42.24 111.45 42.24H108.99V40.87H111.23Q111.86 40.87 112.26 40.69Q112.66 40.52 112.85 40.18Q113.04 39.84 113.04 39.35Q113.04 38.86 112.85 38.51Q112.66 38.16 112.26 37.97Q111.86 37.78 111.23 37.78H109.68V45.63ZM113.22 45.63 110.98 41.44H112.79L115.07 45.63ZM118.71 37.79V36.4H126.06V37.79H123.21V45.63H121.56V37.79ZM129.7 45.63 132.92 36.4H135.02L138.31 45.63H136.5L134.86 40.84Q134.62 40.09 134.36 39.15Q134.1 38.22 133.78 37.02H134.13Q133.82 38.23 133.57 39.17Q133.32 40.12 133.09 40.84L131.51 45.63ZM131.58 43.34V42.01H136.44V43.34ZM142.37 45.63V36.4H144.02V44.24H148.1V45.63Z" class="st3"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

-151
View File
@@ -1,151 +0,0 @@
/*
* CTAO portal mockup: header behaviour
*
* 1. The sticky header turns solid Galaxy Blue after 40px of scroll.
* 2. Submenus: every button with data-submenu-toggle (Example, language)
* toggles the element named in its aria-controls. One open at a time.
* 3. The search button toggles the search panel.
* 4. The burger button toggles the main nav on small screens.
* 5. Escape and clicks outside the header close open menus.
*
* Review helper: index.html#open=<id> opens a panel on load,
* for example #open=submenu-example, #open=submenu-lang,
* #open=search-panel or #open=site-nav.
*/
(function () {
"use strict";
// Keep in sync with the small-screen media query in css/styles.css
const MOBILE_QUERY = window.matchMedia("(max-width: 1079px)");
const header = document.getElementById("site-header");
const nav = document.getElementById("site-nav");
const searchPanel = document.getElementById("search-panel");
const searchButton = header.querySelector('[aria-controls="search-panel"]');
const menuButton = header.querySelector('[aria-controls="site-nav"]');
const submenuButtons = Array.from(
header.querySelectorAll("[data-submenu-toggle]"),
);
/* 1. Scrolled state */
function updateScrolled() {
header.classList.toggle("is-scrolled", window.scrollY > 40);
}
window.addEventListener("scroll", updateScrolled, { passive: true });
updateScrolled();
/* Keeps the header solid while a full-width panel is open */
function updateHeaderState() {
const panelOpen =
!searchPanel.hidden || header.classList.contains("is-nav-open");
header.classList.toggle("is-open", panelOpen);
}
/* 2. Submenus */
function setSubmenu(button, open) {
const submenu = document.getElementById(
button.getAttribute("aria-controls"),
);
button.setAttribute("aria-expanded", String(open));
submenu.hidden = !open;
}
function closeSubmenus(except) {
submenuButtons.forEach(function (button) {
if (button !== except) setSubmenu(button, false);
});
}
submenuButtons.forEach(function (button) {
button.addEventListener("click", function () {
const open = button.getAttribute("aria-expanded") !== "true";
closeSubmenus(button);
setSearch(false);
setSubmenu(button, open);
});
});
/* 3. Search */
function setSearch(open) {
searchButton.setAttribute("aria-expanded", String(open));
searchPanel.hidden = !open;
updateHeaderState();
if (open) searchPanel.querySelector("input").focus();
}
searchButton.addEventListener("click", function () {
const open = searchPanel.hidden;
closeSubmenus();
setMobileNav(false);
setSearch(open);
});
/* 4. Mobile nav */
function setMobileNav(open) {
menuButton.setAttribute("aria-expanded", String(open));
header.classList.toggle("is-nav-open", open);
document.body.classList.toggle("no-scroll", open);
updateHeaderState();
}
menuButton.addEventListener("click", function () {
const open = !header.classList.contains("is-nav-open");
closeSubmenus();
setSearch(false);
setMobileNav(open);
});
/* 5. Dismiss */
function closeAll() {
closeSubmenus();
setSearch(false);
setMobileNav(false);
}
document.addEventListener("click", function (event) {
if (!header.contains(event.target)) closeAll();
});
document.addEventListener("keydown", function (event) {
if (event.key !== "Escape") return;
const openSubmenuButton = submenuButtons.find(function (button) {
return button.getAttribute("aria-expanded") === "true";
});
if (openSubmenuButton) {
setSubmenu(openSubmenuButton, false);
openSubmenuButton.focus();
} else if (!searchPanel.hidden) {
setSearch(false);
searchButton.focus();
} else if (header.classList.contains("is-nav-open")) {
setMobileNav(false);
menuButton.focus();
}
});
/* Switching between desktop and small-screen layouts starts clean */
MOBILE_QUERY.addEventListener("change", closeAll);
/* Review helpers */
const openMatch = location.hash.match(/^#open=([\w-]+)$/);
if (openMatch) {
const target = header.querySelector(
'[aria-controls="' + openMatch[1] + '"]',
);
if (target) {
if (MOBILE_QUERY.matches && nav.contains(target)) menuButton.click();
target.click();
}
}
if (location.hash === "#scrolled") window.scrollTo(0, 420);
})();
+164
View File
@@ -0,0 +1,164 @@
// Shared client-side news search over /search.json (no dependencies, no modules).
// Used by the header search (field + mobile reveal) and /search (full results).
// Focused-empty state per Baymard autocomplete guidance: recent searches (local
// only — localStorage, no network) plus the newest articles, never a void.
(function () {
const RKEY = 'ctao-recent-searches';
function recents() {
try { return JSON.parse(localStorage.getItem(RKEY) || '[]'); } catch { return []; }
}
function remember(q) {
q = (q || '').trim();
if (q.length < 2) return;
try {
const r = [q, ...recents().filter((x) => x.toLowerCase() !== q.toLowerCase())].slice(0, 4);
localStorage.setItem(RKEY, JSON.stringify(r));
} catch { /* private mode — recents are a nicety */ }
}
window.newsSearch = function (opts) {
const input = document.getElementById(opts.input);
const list = document.getElementById(opts.list);
const status = document.getElementById(opts.status);
// Null-safe: a page may render only some search surfaces — bail silently
// (returning a no-op) so one missing input can't kill later inits.
if (!input || !list || !status) return function () {};
let index = null;
let timer;
async function ensureIndex() {
if (index) return index;
try {
index = await (await fetch('/search.json')).json();
} catch {
// Network hiccup: say so instead of failing silently; index stays
// null so the next keystroke retries.
status.textContent = 'Search is unavailable right now.';
return null;
}
return index;
}
// First title match wrapped in <mark> (Starlight search pattern) — built
// from text nodes, never innerHTML.
function highlight(text, q) {
const i = text.toLowerCase().indexOf(q);
if (i < 0) return [text];
const mark = document.createElement('mark');
mark.textContent = text.slice(i, i + q.length);
return [text.slice(0, i), mark, text.slice(i + q.length)];
}
function label(text) {
const li = document.createElement('li');
li.className = 'suggest-label';
li.setAttribute('aria-hidden', 'true'); // the status line announces it
li.textContent = text;
return li;
}
function row(children, href, q) {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = href;
if (q) a.dataset.q = q;
a.append(...children);
li.append(a);
return li;
}
// Dropdowns close with a "Press Enter for all results" row — a real list
// element (was a CSS ::after), aria-hidden like the group labels so
// screen-reader exposure stays consistent (the status line announces).
function hint() {
if (!opts.dismiss || !list.firstChild) return;
const li = document.createElement('li');
li.className = 'suggest-hint';
li.setAttribute('aria-hidden', 'true');
li.textContent = 'Press Enter for all results';
list.append(li);
}
// Focused-empty state: up to 4 recent queries, topped up to 5 rows with
// the newest articles from the same index.
async function idle() {
if (input.value.trim().length >= 2) return;
// Build off-DOM, swap atomically — clearing before the await shrinks the
// page mid-flight and the browser clamps the scroll position ("jumps up").
const frag = document.createDocumentFragment();
const r = recents();
if (r.length) {
frag.append(label('Recent'));
for (const term of r) frag.append(row([term], '/search?q=' + encodeURIComponent(term), term));
}
const idx = await ensureIndex();
if (!idx) return;
const fresh = idx.slice(0, Math.max(1, 5 - r.length));
if (input.value.trim().length >= 2) return; // typed meanwhile — run() owns the list
frag.append(label('Latest news'));
for (const p of fresh) {
const li = row([p.title], '/news/' + encodeURIComponent(p.slug));
const small = document.createElement('small');
small.textContent = p.date;
li.append(small);
frag.append(li);
}
list.replaceChildren(frag);
hint();
status.textContent = opts.hint || (r.length ? 'Showing recent searches and latest news' : 'Showing latest news');
}
async function run() {
const q = input.value.trim().toLowerCase();
if (q.length < 2) { list.replaceChildren(); idle(); return; }
const idx = await ensureIndex();
if (!idx) { list.replaceChildren(); return; }
const hits = idx
.filter((p) => (p.title + ' ' + p.description + ' ' + p.category).toLowerCase().includes(q))
.slice(0, opts.limit);
const unit = opts.unit || 'result';
status.textContent = hits.length
? hits.length + ' ' + unit + (hits.length === 1 ? '' : 's') + ' for “' + input.value.trim() + '”'
: 'No results for “' + input.value.trim() + '”. Try a shorter or different term.';
// Off-DOM build + atomic swap (no transient page-height collapse)
const frag = document.createDocumentFragment();
for (const p of hits) {
const li = row(highlight(p.title, q), '/news/' + encodeURIComponent(p.slug));
const small = document.createElement('small');
small.textContent = p.date;
li.append(small);
frag.append(li);
}
// Dropdown only: an empty list would collapse (.suggest:empty) and the
// panel would just vanish — show the no-results line sighted users too
// (the status line above is sr-only in the header; /search shows a
// visible notice instead).
if (!hits.length && opts.dismiss) {
const li = document.createElement('li');
li.className = 'suggest-hint';
li.setAttribute('aria-hidden', 'true'); // the status line announces it
li.textContent = 'No results for “' + input.value.trim() + '”';
frag.append(li);
}
list.replaceChildren(frag);
hint();
}
input.addEventListener('input', () => { clearTimeout(timer); timer = setTimeout(run, 150); });
input.addEventListener('focus', () => { if (input.value.trim().length < 2) idle(); });
// Remember successful queries: Enter (form submit) and suggestion clicks
if (input.form) input.form.addEventListener('submit', () => remember(input.value));
list.addEventListener('click', (e) => {
const a = e.target.closest && e.target.closest('a');
remember((a && a.dataset.q) || input.value);
});
// Dropdown dismiss (opt-in — /search keeps its results list). The
// suggestion list is a NON-MODAL popup (combobox-style; plain
// Tab-reachable links, no roving arrow-key focus): interacting outside
// the field+list closes it but the interaction is NOT swallowed —
// unlike the modal header panels, which scrim-dismiss in Base.astro.
if (opts.dismiss) {
const box = input.closest('form') || input.parentElement;
const hide = () => { clearTimeout(timer); list.replaceChildren(); status.textContent = ''; };
document.addEventListener('pointerdown', (e) => { if (list.firstChild && !box.contains(e.target)) hide(); });
// relatedTarget may be null mid-click on our own links — pointerdown covers that path
box.addEventListener('focusout', (e) => { if (e.relatedTarget && !box.contains(e.relatedTarget)) hide(); });
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && list.firstChild) { hide(); e.stopPropagation(); } // panel Escape stays a second step
});
}
return run; // callers may run() immediately (e.g. /search?q=…)
};
})();
+1 -2
View File
@@ -4,9 +4,8 @@
// URLs, mailto:, data: and pure-#fragment links are out of scope. // URLs, mailto:, data: and pure-#fragment links are out of scope.
import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs'; import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
const DIST = fileURLToPath(new URL('../dist', import.meta.url)); const DIST = new URL('../dist', import.meta.url).pathname;
if (!existsSync(DIST)) { console.error('dist/ not found — run `npm run build` first'); process.exit(1); } if (!existsSync(DIST)) { console.error('dist/ not found — run `npm run build` first'); process.exit(1); }
const htmlFiles = []; const htmlFiles = [];
-50
View File
@@ -1,50 +0,0 @@
// Point the site at a checkout of the ctao/content repository (default: the
// sibling ../content clone), the same content the live demo builds from.
// src/content/pages and public/uploads become links into that checkout, so
// saves from Sveltia's local-repository mode show up in `npm run dev` directly.
//
// node scripts/content.mjs link if missing, then check the pages (pre-hooks)
// npm run content:link -- [dir] replace existing links or copies
//
// The deploy build copies the content into place instead (deploy/build.sh);
// existing directories are left alone unless --relink is given.
import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, unlinkSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = fileURLToPath(new URL('../', import.meta.url));
const examples = JSON.parse(readFileSync(join(root, 'src/data/examples.json'), 'utf8'));
const relink = process.argv.includes('--relink');
const source = resolve(root, process.argv.slice(2).find((arg) => !arg.startsWith('--')) ?? '../content');
const targets = [
{ from: join(source, 'pages'), to: join(root, 'src/content/pages') },
{ from: join(source, 'uploads'), to: join(root, 'public/uploads') },
];
const exists = (path) => {
try { lstatSync(path); return true; } catch { return false; }
};
for (const { from, to } of targets) {
if (exists(to) && !relink) continue;
if (!existsSync(from)) {
throw new Error(`Missing ${from}. Clone the content repository first:\n` +
` git clone https://astro-git.isl-dev.grid.cyfronet.pl/ctao/content.git ${relative(root, source) || '.'}\n` +
'or pass another checkout: npm run content:link -- <path>');
}
if (exists(to)) {
// Remove only the link itself, never the content it points to.
if (lstatSync(to).isSymbolicLink()) unlinkSync(to);
else rmSync(to, { recursive: true }); // an old copied overlay
}
mkdirSync(dirname(to), { recursive: true });
symlinkSync(from, to, 'junction'); // a junction needs no admin rights on Windows
console.log(`Linked ${relative(root, to)} -> ${from}`);
}
const pages = join(root, 'src/content/pages');
for (const slug of ['home', ...examples.map(({ slug }) => slug)]) {
if (!existsSync(join(pages, `${slug}.md`))) {
throw new Error(`Missing ${join(pages, `${slug}.md`)}. Check the content checkout, or run npm run content:link -- <path>.`);
}
}
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Local dev: put the content repo's files where the build expects them.
# Usage: ./scripts/link-content.sh [path-to-content-clone] (default ../ctao-content)
# Copies (not symlinks) so the dev tree behaves exactly like the machine
# build overlay; re-run after pulling content changes.
set -euo pipefail
cd "$(dirname "$0")/.."
src="${1:-../ctao-content}"
for d in news pages uploads; do
[ -d "$src/$d" ] || { echo "content clone incomplete: $src/$d missing (git clone <gitea>/ctao/content.git first)"; exit 1; }
done
rm -rf src/content/news src/content/pages public/uploads
mkdir -p src/content public
cp -a "$src/news" src/content/news
cp -a "$src/pages" src/content/pages
cp -a "$src/uploads" public/uploads
echo "content linked from $src"
-27
View File
@@ -1,27 +0,0 @@
// Derive editor preview styles from the site CSS so the preview cannot drift.
// The preview template in src/pages/admin/index.astro renders the same markup
// as the Example pages, so the page hero, figure and prose rules apply as-is.
import { readFileSync, writeFileSync } from 'node:fs';
const css = readFileSync(new URL('../src/styles/global.css', import.meta.url), 'utf8').replace(/\r\n/g, '\n');
const between = (start, end) => {
const from = css.indexOf(start);
const to = end ? css.indexOf(end, from) : css.length;
if (from < 0 || to < 0) throw new Error(`preview-styles: marker not found in global.css: ${from < 0 ? start : end}`);
return css.slice(from, to).trim();
};
const preview = [
between('/* ===', '/* Header'), // fonts, tokens, base, utilities
between('/* Page hero', '/* Footer'), // hero, figure, prose, buttons
between('/* Preferences'), // high contrast, reduced motion
`/* Preview frame
========================================================================== */
/* There is no site header above the hero */
.page-hero {
margin-top: 0;
padding-top: 3rem;
}`,
];
writeFileSync(new URL('../public/admin/preview.css', import.meta.url),
'/* Generated by scripts/preview-styles.mjs; edit src/styles/global.css. */\n\n' + preview.join('\n\n') + '\n');
+16
View File
@@ -0,0 +1,16 @@
---
// The one news card — used by the home page and the /news archive.
// `featured` (archive page 1, first item) switches the wide lead variant and
// eager-loads its cover. Cover alt stays empty by design: the image is
// decorative next to the always-visible title.
import { fmt, iso, readMin } from '../lib/news.js';
const { post, featured = false } = Astro.props;
---
<article class={featured ? 'card card--featured' : 'card'}>
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" loading={featured ? 'eager' : 'lazy'} />}
<div class="body">
<h3><a href={`/news/${post.id}`}>{post.data.title}</a></h3>
<p class="desc">{post.data.description}</p>
<div class="meta"><span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="16" rx="2" /><path d="M4 10h16M8 3v4M16 3v4" /></svg><time datetime={iso(post.data.date)}>{fmt(post.data.date)}</time></span><span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>{readMin(post)} min read</span></div>
</div>
</article>
-17
View File
@@ -1,17 +0,0 @@
---
interface Props { title: string; introduction?: string; example?: boolean; breadcrumb?: boolean; hasImage?: boolean }
const { title, introduction, example = false, breadcrumb = true, hasImage = false } = Astro.props;
---
<section class:list={['page-hero', { 'page-hero--compact': !hasImage }]}>
<div class="container">
{breadcrumb && <nav class="breadcrumb" aria-label="Breadcrumb">
<ol>
{Astro.url.pathname !== '/' && <li><a href="/">Home</a></li>}
{example && <li>Example</li>}
<li aria-current="page">{title}</li>
</ol>
</nav>}
<h1 class="page-hero__title">{title}</h1>
{introduction && <p class="page-hero__lead">{introduction}</p>}
</div>
</section>
+34 -26
View File
@@ -1,33 +1,41 @@
import { defineCollection } from 'astro:content'; import { defineCollection } from 'astro:content';
import { z } from 'astro/zod'; import { z } from 'astro/zod';
import { glob } from 'astro/loaders'; import { glob } from 'astro/loaders';
import examples from './data/examples.json';
const destination = z.string().refine( // News/articles live as Markdown files in src/content/news/*.md.
(value) => !value || /^(\/(?!\/)|https?:\/\/|mailto:|#)/.test(value), // This is exactly what Sveltia CMS edits — the CMS writes these files, Astro
'Use a site path, https:// URL, mailto: address, or fragment.', // renders them to static HTML at build time.
); const news = defineCollection({
const pages = defineCollection({ // Top-level only ('*.md', not '**'): entry ids feed a non-rest [slug]
// Only the Home page and the four Example files can become public pages. // route, and an id from a subfolder would contain '/' and break it.
loader: glob({ loader: glob({ pattern: '*.md', base: './src/content/news' }),
pattern: `{home,${examples.map(({ slug }) => slug).join(',')}}.md`,
base: './src/content/pages',
}),
schema: z.object({ schema: z.object({
title: z.string().min(1), title: z.string(),
introduction: z.string().optional(), description: z.string(),
// Example pages: one photo date: z.coerce.date(),
image: z.string().optional(), category: z.string().default('news'),
imageAlt: z.string().optional(), author: z.string().default('CTAO'),
imageCaption: z.string().optional(), // Public-path string, e.g. "/uploads/foo.jpg" — media lives in the
// Home: up to two photos side by side // content repo's uploads/, overlaid to public/uploads at build time,
images: z.array(z.object({ // which Astro's image() helper can't validate (src/ only).
image: z.string().min(1), cover: z.string().optional(),
alt: z.string().optional(), // BCP-47 tag when an article is not in English (e.g. "pl") — set as lang
caption: z.string().optional(), // on the <article> element so screen readers pick the right voice.
})).max(2).optional(), lang: z.string().optional(),
buttonLabel: z.string().optional(), draft: z.boolean().default(false),
buttonUrl: destination.optional(),
}), }),
}); });
export const collections = { pages };
// Static portal pages (privacy, disclaimer, contact…) — separate collection
// because the content type differs (no dates/covers/categories), and editors
// get a distinct "Pages" section in the CMS.
const pages = defineCollection({
// Top-level only, same reason as news: the [slug] route is non-rest.
loader: glob({ pattern: '*.md', base: './src/content/pages' }),
schema: z.object({
title: z.string(),
description: z.string().optional(),
}),
});
export const collections = { news, pages };
-18
View File
@@ -1,18 +0,0 @@
[
{
"slug": "lorem-ipsum",
"label": "Lorem ipsum"
},
{
"slug": "dolor-sit-amet",
"label": "Dolor sit amet"
},
{
"slug": "consectetur-adipiscing",
"label": "Consectetur adipiscing"
},
{
"slug": "sed-do-eiusmod",
"label": "Sed do eiusmod"
}
]
-42
View File
@@ -1,42 +0,0 @@
[
{
"slug": "news",
"title": "News"
},
{
"slug": "data",
"title": "Data"
},
{
"slug": "proposals",
"title": "Proposals"
},
{
"slug": "dashboard",
"title": "Dashboard"
},
{
"slug": "login",
"title": "Sign in"
},
{
"slug": "search",
"title": "Search"
},
{
"slug": "support",
"title": "Support"
},
{
"slug": "pages/privacy",
"title": "Privacy"
},
{
"slug": "pages/disclaimer",
"title": "Disclaimer"
},
{
"slug": "pages/contact",
"title": "Contact"
}
]
+174 -102
View File
@@ -1,119 +1,191 @@
--- ---
import '../styles/global.css'; import '../styles/global.css';
import examples from '../data/examples.json'; // `ambient` opts a page into the whole-page drifting brand wash (home only)
interface Props { title?: string; description?: string } // `type`/`image` feed the social meta: articles pass type="article" + their cover.
const { title = 'CTAO Science Portal', description = 'CTAO Science Portal' } = Astro.props; const { title = 'CTAO Science Portal', description = 'CTAO Science Portal demo', ambient = false, type = 'website', image } = Astro.props;
const path = Astro.url.pathname.replace(/\/?$/, '/'); // Absolute URLs for canonical/OG/RSS (head pattern from the official Astro
const canonical = new URL(Astro.url.pathname, Astro.site); // blog template). `site` comes from astro.config.mjs and is always set.
const site = Astro.site;
const canonical = new URL(Astro.url.pathname, site);
// SVG covers never reach og:image — link-preview crawlers (Slack/Teams/
// LinkedIn) don't render SVG, so those articles fall back to the brand card.
const ogImage = new URL(image && !image.endsWith('.svg') ? image : '/brand/og-card.jpg', site);
const path = Astro.url.pathname;
const isActive = (href: string) => (href === '/' ? path === '/' : path.startsWith(href));
// Navigation = services from the Science Portal spec (REQUIREMENTS.md §2/§5).
// 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: '/support', label: 'Support' },
];
--- ---
<!doctype html> <!doctype html>
{/* Always "en": the chrome (nav/footer) is English; a non-English article
sets lang on its <article> element instead ([slug].astro). */}
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title === 'CTAO Science Portal' ? title : `${title} – CTAO`}</title> <title>{title}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
<meta name="theme-color" content="#00004a" />
<link rel="canonical" href={canonical} /> <link rel="canonical" href={canonical} />
<link rel="icon" href="/assets/logo-sp-positive.svg" type="image/svg+xml" /> {/* Social cards (Open Graph + Twitter) — article cover when present, brand card otherwise */}
<meta property="og:site_name" content="CTAO Science Portal" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:type" content={type} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={ogImage} />
<meta name="twitter:card" content="summary_large_image" />
<link rel="alternate" type="application/rss+xml" title="CTAO Science Portal News" href={new URL('/rss.xml', site)} />
<link rel="sitemap" href="/sitemap.xml" /> <link rel="sitemap" href="/sitemap.xml" />
<link rel="icon" href="/brand/CTAO_Logo_positive.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="theme-color" content="#00004a" />
{/* The home hero photo is a CSS background, which browsers discover late —
preloading it moves the page's LCP element to the front of the queue. */}
{path === '/' && <link rel="preload" as="image" href="/brand/hero.jpg" fetchpriority="high" />}
{/* Brand typography (BRAND D.3): Inter (body) + Space Grotesk (headlines).
Both ship inlined as data: URIs in the render-blocking stylesheet —
no font request, no swap (see global.css @font-face). */}
</head> </head>
<body> <body class={ambient ? 'has-ambient' : undefined}>
<a class="skip-link" href="#main">Skip to content</a> <a class="skip" href="#main">Skip to content</a>
<header class="site-header" id="site-header"> <header class="site-header">
<div class="site-header__inner"> <div class="container nav">
<a class="brand" href="/" aria-label="CTAO Science Portal, home">
<a class="site-header__logo" href="/"> <img src="/brand/AAFF_CTAO_Logo_RGB_Monochrome-negative.svg" alt="CTAO" />
<img src="/assets/logo-sp-negative.svg" alt="CTAO – Home" width="153" height="46"> <span class="brand-sub">Science&nbsp;Portal</span>
</a> </a>
<nav aria-label="Main navigation">
<nav class="site-nav" id="site-nav" aria-label="Main"> {links.map((l) => l.ext ? (
<ul class="site-nav__list"> <a class="navlink external" href={l.href} target="_blank" rel="noopener">{l.label}</a>
<li class="site-nav__item"> ) : (
<a class="site-nav__link" href="/news/" aria-current={path === "/news/" ? "page" : undefined}><span class="site-nav__label">News</span></a> <a class="navlink" href={l.href} aria-current={isActive(l.href) ? 'page' : undefined}>{l.label}</a>
</li> ))}
<li class="site-nav__item"> </nav>
<a class="site-nav__link" href="/data/" aria-current={path === "/data/" ? "page" : undefined}><span class="site-nav__label">Data</span></a> <span class="spacer"></span>
</li> {/* Site search. Desktop ≥1025px: a VISIBLE quiet field — NN/g's
<li class="site-nav__item"> magnifying-glass-icon research: hiding search behind an icon
<a class="site-nav__link" href="/proposals/" aria-current={path === "/proposals/" ? "page" : undefined}><span class="site-nav__label">Proposals</span></a> measurably reduces discoverability and usage; a visible input is
</li> recommended when search matters (200+ articles, spec-required).
<li class="site-nav__item"> Plain GET form (works without JS); suggestions via search-client.js. */}
<a class="site-nav__link" href="/dashboard/" aria-current={path === "/dashboard/" ? "page" : undefined}><span class="site-nav__label">Dashboard</span></a> <form class="nav-search" role="search" aria-label="Site search" action="/search" method="get">
</li> <label class="sr-only" for="hdr-q">Search news</label>
<li class="site-nav__item has-submenu"> <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>
<button class="site-nav__link site-nav__toggle" type="button" data-submenu-toggle aria-expanded="false" aria-controls="submenu-example"> <input id="hdr-q" name="q" type="search" placeholder="Search…" autocomplete="off" />
<span class="site-nav__label">Example</span> <ul class="suggest" id="hdr-suggest"></ul>
<svg class="chevron" width="10" height="6" viewBox="0 0 10 6" aria-hidden="true" focusable="false"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg> <p class="sr-only" id="hdr-suggest-status" role="status" aria-live="polite"></p>
</button> </form>
<ul class="submenu site-nav__submenu" id="submenu-example" hidden> {/* ≤1024px: the magnifier reveals a search row under the header (NN/g
{examples.map(({ slug, label }) => <li><a href={`/example/${slug}/`} aria-current={path === `/example/${slug}/` ? 'page' : undefined}>{label}</a></li>)} mobile-search pattern). name="header-panel" pairs it with the menu as
</ul> a native exclusive group — opening one closes the other, no JS. */}
</li> <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>
<li class="site-nav__item site-nav__item--signin"> </summary>
<a class="site-nav__link site-nav__signin" href="/login/">Sign in</a> <form role="search" aria-label="Site search" action="/search" method="get">
</li> <label class="sr-only" for="pop-q">Search news</label>
</ul> <input id="pop-q" name="q" type="search" placeholder="Search news…" autocomplete="off" />
</nav> <button class="btn" type="submit">Search</button>
<ul class="suggest" id="pop-suggest"></ul>
<div class="site-header__tools"> <p class="sr-only" id="pop-suggest-status" role="status" aria-live="polite"></p>
</form>
<a class="pill site-header__signin" href="/login/">Sign in</a> </details>
<a class="login-mock" href="/login">Sign in</a>
<div class="lang has-submenu"> {/* Mobile disclosure menu — native <details>/<summary>, no JS; desktop hides it via CSS */}
<button class="pill lang__toggle" type="button" data-submenu-toggle aria-expanded="false" aria-controls="submenu-lang"> <details class="menu" name="header-panel">
<span class="visually-hidden">Language:</span> <summary aria-label="Menu">
<span>EN</span> <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>
<svg class="chevron" width="10" height="6" viewBox="0 0 10 6" aria-hidden="true" focusable="false"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg> <span class="menu-label">Menu</span>
</button> </summary>
<ul class="submenu submenu--end" id="submenu-lang" hidden> <nav aria-label="Main navigation">
<li><a href={Astro.url.pathname} lang="en" aria-current="true">English</a></li> {links.map((l) => l.ext ? (
<li><span class="submenu__unavailable" lang="de" aria-disabled="true">Deutsch</span></li> <a class="external" href={l.href} target="_blank" rel="noopener">{l.label}</a>
<li><span class="submenu__unavailable" lang="es" aria-disabled="true">Español</span></li> ) : (
<li><span class="submenu__unavailable" lang="fr" aria-disabled="true">Français</span></li> <a href={l.href} aria-current={isActive(l.href) ? 'page' : undefined}>{l.label}</a>
<li><span class="submenu__unavailable" lang="it" aria-disabled="true">Italiano</span></li> ))}
<li><span class="submenu__unavailable" lang="ja" aria-disabled="true">日本語</span></li> </nav>
</ul> </details>
</div>
<button class="icon-btn" type="button" aria-label="Search" aria-expanded="false" aria-controls="search-panel">
<svg class="icon-btn__open" width="16" height="16" viewBox="0 0 16 15" aria-hidden="true" focusable="false"><path d="M5.917.292a5.417 5.417 0 0 1 4.116 8.942l.225.225h.659l4.166 4.166-1.25 1.25-4.166-4.166v-.659l-.225-.225A5.417 5.417 0 1 1 5.917.292Zm0 1.667a3.75 3.75 0 1 0 0 7.5 3.75 3.75 0 0 0 0-7.5Z" fill="currentColor"/></svg>
<svg class="icon-btn__close" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3.05 1.636 8 6.586l4.95-4.95 1.414 1.414L9.414 8l4.95 4.95-1.414 1.414L8 9.414l-4.95 4.95-1.414-1.414L6.586 8l-4.95-4.95L3.05 1.636Z" fill="currentColor"/></svg>
</button>
<button class="icon-btn icon-btn--menu" type="button" aria-label="Menu" aria-expanded="false" aria-controls="site-nav">
<svg class="icon-btn__open" width="21" height="20" viewBox="0 0 21 20" aria-hidden="true" focusable="false"><path d="M3.415 6h15v1.667h-15V6Zm0 4.167h15v1.666h-15v-1.666Zm0 4.166h15V16h-15v-1.667Z" fill="currentColor"/></svg>
<svg class="icon-btn__close" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3.05 1.636 8 6.586l4.95-4.95 1.414 1.414L9.414 8l4.95 4.95-1.414 1.414L8 9.414l-4.95 4.95-1.414-1.414L6.586 8l-4.95-4.95L3.05 1.636Z" fill="currentColor"/></svg>
</button>
</div> </div>
</div> </header>
{/* Search enhancement is deferred — a synchronous script here would stall
<div class="search-panel" id="search-panel" hidden> the parser mid-body on every navigation (blank below the header until
<form class="search-panel__form" role="search" action="/search/" method="get"> it arrives). `defer` + the inline `type="module"` init join the same
<input class="search-panel__input" type="search" name="q" placeholder="Type Search Text Here" aria-label="Search" autocomplete="off"> after-parse in-order queue (HTML spec), so newsSearch is defined first.
<button class="search-panel__submit" type="submit" aria-label="Submit search"> The forms are plain GET /search, so nothing user-facing waits on JS. */}
<span class="arrow" aria-hidden="true">→</span> <script is:inline src="/search-client.js" defer></script>
</button> <script is:inline type="module">
</form> // Header search suggestions (desktop field + mobile reveal); the forms
</div> // are plain GET /search, so everything below is progressive enhancement.
</header> newsSearch({ input: 'hdr-q', list: 'hdr-suggest', status: 'hdr-suggest-status', limit: 6, unit: 'suggestion', dismiss: true });
newsSearch({ input: 'pop-q', list: 'pop-suggest', status: 'pop-suggest-status', limit: 6, unit: 'suggestion', dismiss: true });
var pop = document.querySelector('.search-pop');
<main id="main" tabindex="-1"><slot /></main> if (pop) pop.addEventListener('toggle', function () {
<footer class="site-footer"> var q = document.getElementById('pop-q');
<div class="container site-footer__inner"> if (pop.open && q) q.focus();
<img class="site-footer__logo" src="/assets/logo-sp-negative.svg" alt="CTAO" width="153" height="46" /> });
<ul class="site-footer__nav"> // Light dismiss for the header panels (menu + search reveal) — scrim
<li><a href="/pages/privacy/">Privacy</a></li> // convention (Material/iOS, NN/g error prevention): a tap outside an
<li><a href="/pages/disclaimer/">Disclaimer</a></li> // open panel ONLY closes it and must never activate what's underneath.
<li><a href="/pages/contact/">Contact</a></li> // <details> has no native light dismiss; the Popover API was evaluated
<li><a href="/admin/">Content editor</a></li> // and rejected here — its light dismiss deliberately lets the outside
</ul> // click THROUGH (::backdrop is spec-forced pointer-events: none), the
<p class="site-footer__copy">© {new Date().getFullYear()} CTAO</p> // exact behavior research ruled out. Canceling pointerdown doesn't
// cancel the later click (Pointer Events spec), hence the click guard.
var swallow = false;
document.addEventListener('pointerdown', function (e) {
var open = document.querySelector('details[name="header-panel"][open]');
swallow = !!(open && !open.contains(e.target));
if (swallow) { open.open = false; e.preventDefault(); }
}, true);
document.addEventListener('click', function (e) {
if (swallow) { swallow = false; e.preventDefault(); e.stopPropagation(); }
}, true);
// Escape closes the open panel and returns focus to its trigger button
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
var open = document.querySelector('details[name="header-panel"][open]');
if (open) { open.open = false; open.querySelector('summary').focus(); }
});
</script>
<main id="main">
<slot />
</main>
<footer class="footer">
{/* Bottom menu required by SPEC §3.3.1: disclaimers, privacy, contact, settings, search.
Site settings has no target yet — placeholder link = <a> without href. */}
<div class="container">
<div class="foot-top">
<img src="/brand/AAFF_CTAO_Logo_RGB_Monochrome-negative.svg" alt="CTAO" />
<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="/rss.xml">RSS</a>
<a href="/pages/disclaimer">Disclaimer</a>
<a href="/pages/privacy">Privacy</a>
<a>Site settings</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>© 2026 CTAO. Demonstration, not an official service.</span>
</div>
</div> </div>
</footer> </footer>
<script is:inline src="/header.js"></script>
</body> </body>
</html> </html>
-36
View File
@@ -1,36 +0,0 @@
---
// Editable content page: hero, one photo (Example pages) or two side by side
// (Home), Markdown body and optional button. The CMS preview template in
// src/pages/admin/index.astro mirrors this markup.
import type { CollectionEntry } from 'astro:content';
import { render } from 'astro:content';
import Base from './Base.astro';
import PageHero from '../components/PageHero.astro';
interface Props { page: CollectionEntry<'pages'>; example?: boolean; breadcrumb?: boolean }
const { page, example = false, breadcrumb = true } = Astro.props;
const { Content } = await render(page);
const { title, introduction, image, imageAlt, imageCaption, images, buttonLabel, buttonUrl } = page.data;
const photos = images ?? (image ? [{ image, alt: imageAlt, caption: imageCaption }] : []);
---
<Base title={example ? title : undefined} description={introduction}>
<PageHero title={title} introduction={introduction} example={example} breadcrumb={breadcrumb} hasImage={photos.length > 0} />
{photos.length > 0 && <div class="container">
<div class:list={{ 'page-figures': photos.length > 1 }}>
{photos.map(({ image, alt, caption }) => (
<figure class="page-figure">
<img src={image} alt={alt || ''} />
{caption && <figcaption>{caption}</figcaption>}
</figure>
))}
</div>
</div>}
<article class:list={['container', 'prose', { 'prose--no-image': photos.length === 0 }]}>
<Content />
{buttonLabel && buttonUrl && <p>
<a class="btn btn--primary" href={buttonUrl}>
<span class="btn__label">{buttonLabel}</span><span class="arrow" aria-hidden="true">→</span>
</a>
</p>}
</article>
</Base>
-10
View File
@@ -1,10 +0,0 @@
---
import Base from './Base.astro';
import PageHero from '../components/PageHero.astro';
interface Props { title: string }
const { title } = Astro.props;
---
<Base title={title}>
<PageHero title={title} />
<div class="placeholder-body"></div>
</Base>
+11
View File
@@ -0,0 +1,11 @@
// Shared news helpers — single source for card metadata and archive chunking.
export const fmt = (d) => new Intl.DateTimeFormat('en-GB', { dateStyle: 'long' }).format(d);
export 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).
export const readMin = (p) => Math.max(1, Math.round((p.body ?? '').split(/\s+/).length / 220));
// Archive chunking: page 1 holds 25 items (1 featured lead + 24 grid = even
// 3-column rows), later pages 24. Used by BOTH the /news/[...page] route and
// sitemap.xml.js — the page counts cannot drift apart.
export const PAGE_FIRST = 25;
export const PAGE_REST = 24;
+20 -4
View File
@@ -1,8 +1,24 @@
--- ---
import Base from '../layouts/Base.astro'; import Base from '../layouts/Base.astro';
import PageHero from '../components/PageHero.astro';
--- ---
<Base title="Page not found"> <Base title="CTAO Science Portal - Page not found" description="This page does not exist">
<PageHero title="Page not found" /> {/* Document archetype: the document IS the page — h1, one line,
<div class="container prose prose--no-image"><p><a href="/">Return to Home</a></p></div> recovery links, and a real GET search form as the way forward. */}
<div class="container article-layout">
<article class="article">
<h1>Page not found</h1>
<div class="prose">
{/* Keep "the <a>" on one line: the compiler drops the whitespace at a
line break before a tag, which glued "the" to the link text. */}
<p>The address may be mistyped or the page may have moved.
Go to the <a href="/">home page</a> or browse <a href="/news">all news</a>.</p>
</div>
<form class="search-form" role="search" action="/search" method="get">
<label class="sr-only" for="nf-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="nf-q" name="q" type="search" placeholder="Search news…" autocomplete="off" />
<button class="btn" type="submit">Search</button>
</form>
</article>
</div>
</Base> </Base>
-9
View File
@@ -1,9 +0,0 @@
---
import Placeholder from '../layouts/Placeholder.astro';
import placeholders from '../data/placeholders.json';
export function getStaticPaths() {
return placeholders.map(({ slug, title }) => ({ params: { placeholder: slug }, props: { title } }));
}
const { title } = Astro.props;
---
<Placeholder title={title} />
+1 -63
View File
@@ -5,9 +5,6 @@
// deliberate — see README); note the bundle still fetches its own UI fonts // deliberate — see README); note the bundle still fetches its own UI fonts
// from jsDelivr at runtime. Kept is:inline so Astro doesn't process/bundle it. // from jsDelivr at runtime. Kept is:inline so Astro doesn't process/bundle it.
// CTAO branding on the sign-in screen comes from the documented `logo` option. // CTAO branding on the sign-in screen comes from the documented `logo` option.
import examples from '../../data/examples.json';
const previewNames = examples.map(({ slug }) => slug);
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@@ -32,67 +29,8 @@ const previewNames = examples.map(({ slug }) => slug);
<noscript><p>The CTAO content editor requires JavaScript.<br />Please enable it, or return to the <a href="/">Science Portal</a>.</p></noscript> <noscript><p>The CTAO content editor requires JavaScript.<br />Please enable it, or return to the <a href="/">Science Portal</a>.</p></noscript>
<script is:inline src="/vendor/sveltia-cms.js"></script> <script is:inline src="/vendor/sveltia-cms.js"></script>
<script is:inline> <script is:inline>
// Generated from the site's CSS; see scripts/preview-styles.mjs. // Officially supported API: article previews rendered with portal-like styles.
window.CMS?.registerPreviewStyle?.('/admin/preview.css'); window.CMS?.registerPreviewStyle?.('/admin/preview.css');
</script> </script>
<script is:inline define:vars={{ previewNames }}>
// Preview pane: the same markup as src/layouts/ContentPage.astro and
// PageHero.astro, without field labels. Sveltia looks templates up by
// file name for file collections, so register one per page.
const { h, rf } = window;
// Our own <img>: Sveltia's image widget keeps the image transparent until
// a visibility check that does not complete inside a custom template.
// The asset URL starts as the public path and becomes a blob URL once
// Sveltia has fetched the file, which covers images not deployed yet.
const PreviewImage = ({ asset, fallback, alt }) => h('img', {
src: asset ? String(asset) : fallback,
alt,
onError: ({ currentTarget: img }) => {
const retry = (tries) => {
const url = asset && String(asset);
if (url && url !== img.getAttribute('src')) img.src = url;
else if (tries) setTimeout(() => retry(tries - 1), 500);
};
retry(10);
},
});
const PagePreview = ({ entry, widgetFor, getAsset, home = false }) => {
const get = (key) => entry.getIn(['data', key]);
const title = get('title') || '';
const introduction = get('introduction');
const buttonLabel = get('buttonLabel');
const buttonUrl = get('buttonUrl');
// Home: a list of up to two photos; Example pages: a single photo
const photos = home
? (get('images')?.toJS?.() ?? []).filter((item) => item?.image)
: get('image') ? [{ image: get('image'), alt: get('imageAlt'), caption: get('imageCaption') }] : [];
return h(rf, null,
h('section', { className: photos.length ? 'page-hero' : 'page-hero page-hero--compact' },
h('div', { className: 'container' },
!home && h('nav', { className: 'breadcrumb', 'aria-label': 'Breadcrumb' },
h('ol', null,
h('li', null, h('a', { href: '/' }, 'Home')),
h('li', null, 'Example'),
h('li', { 'aria-current': 'page' }, title))),
h('h1', { className: 'page-hero__title' }, title),
introduction && h('p', { className: 'page-hero__lead' }, introduction))),
photos.length > 0 && h('div', { className: 'container' },
h('div', { className: photos.length > 1 ? 'page-figures' : undefined },
photos.map(({ image, alt, caption }, index) => h('figure', { className: 'page-figure', key: index },
h(PreviewImage, { key: image, asset: getAsset(image), fallback: image, alt: alt || '' }),
caption && h('figcaption', null, caption))))),
h('article', { className: photos.length ? 'container prose' : 'container prose prose--no-image' },
widgetFor('body'),
buttonLabel && buttonUrl && h('p', null,
h('a', { className: 'btn btn--primary', href: buttonUrl },
h('span', { className: 'btn__label' }, buttonLabel),
h('span', { className: 'arrow', 'aria-hidden': 'true' }, '→')))));
};
const HomePreview = (props) => h(PagePreview, { ...props, home: true });
if (window.CMS?.registerPreviewTemplate && h) {
window.CMS.registerPreviewTemplate('home', HomePreview);
for (const name of previewNames) window.CMS.registerPreviewTemplate(name, PagePreview);
}
</script>
</body> </body>
</html> </html>
+30
View File
@@ -0,0 +1,30 @@
---
import Base from '../layouts/Base.astro';
// Per SPEC §3.3.2 the dashboard aggregates proposal statuses, data products
// and help-desk tickets. No integrations exist yet, so panels show empty
// states instead of fabricated sample rows (source-documents-only rule).
---
<Base title="CTAO Science Portal - Dashboard" description="User dashboard (mock)">
<section class="band--moon">
<header class="container page-head">
<h1>Dashboard</h1>
<p class="standfirst">Your proposals, data products and support tickets in one place.</p>
</header>
<div class="container page">
<div class="dash-grid">
<div class="panel">
<h2 class="panel-title">My proposals</h2>
<p class="notice">Proposal statuses will appear here once the Proposal Handling System (APC team) is connected.</p>
</div>
<div class="panel">
<h2 class="panel-title">Recent data products</h2>
<p class="notice">Your data products will appear here. 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">Support tickets</h2>
<p class="notice">Help-desk tickets will appear here once User Support is connected.</p>
</div>
</div>
</div>
</section>
</Base>
-16
View File
@@ -1,16 +0,0 @@
---
import { getCollection } from 'astro:content';
import ContentPage from '../../layouts/ContentPage.astro';
import examples from '../../data/examples.json';
export async function getStaticPaths() {
const pages = await getCollection('pages');
return examples.map(({ slug }) => {
const page = pages.find(({ id }) => id === slug);
if (!page) throw new Error(`Missing Example page: ${slug}. Check the content checkout (npm run content:link -- <path>).`);
return { params: { slug }, props: { page } };
});
}
const { page } = Astro.props;
---
<ContentPage page={page} example />
+90 -5
View File
@@ -1,8 +1,93 @@
--- ---
import { getEntry } from 'astro:content'; import { getCollection } from 'astro:content';
import ContentPage from '../layouts/ContentPage.astro'; import Base from '../layouts/Base.astro';
import NewsCard from '../components/NewsCard.astro';
const page = await getEntry('pages', 'home'); const posts = (await getCollection('news', ({ data }) => !data.draft))
if (!page) throw new Error('Missing Home page: pages/home.md. Check the content checkout (npm run content:link -- <path>).'); .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
.slice(0, 3);
// Services per the Science Portal spec (REQUIREMENTS §5). Tiles carry no
// status chrome; the footer's demo note is the single mock disclosure.
// Planned services stay in the grid but subdued (muted, no link) with
// availability folded into the description text.
// Tile icons: hand-drawn stroke glyphs in the header-icon family (.ico —
// currentColor, round caps), aria-hidden. NN/g icon research: icons alone are
// ambiguous, but NEXT TO an always-visible label they differentiate otherwise
// uniform tiles and speed up grid scanning.
const services = [
{ title: 'Proposals', href: '/proposals', desc: 'Submit observation proposals, including ToO / MWL requests.',
icon: '<path d="M13.5 3H7a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5z"/><path d="M13.5 3v4.5H18"/><path d="M9 12.5h6M9 16h4"/>' },
{ title: 'Data Explorer', href: 'https://padc-ctao-data-explorer.obspm.fr/', ext: true, desc: 'Search and download CTAO science data products.',
icon: '<ellipse cx="12" cy="5.5" rx="7" ry="2.5"/><path d="M5 5.5V18c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5V5.5"/><path d="M5 11.8c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5"/>' },
{ title: 'Dashboard', href: '/dashboard', desc: 'Your proposals, data products and support tickets in one place.',
icon: '<rect x="4" y="4" width="7" height="7" rx="1.5"/><rect x="13" y="4" width="7" height="7" rx="1.5"/><rect x="4" y="13" width="7" height="7" rx="1.5"/><rect x="13" y="13" width="7" height="7" rx="1.5"/>' },
{ title: 'User Support', href: '/support', desc: 'Help desk, FAQ, user forum and mailing lists.',
icon: '<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/><path d="m5.7 5.7 3.5 3.5m5.6 0 3.5-3.5m0 12.6-3.5-3.5m-5.6 0-3.5 3.5"/>' },
// Two subdued planned tiles complete the 2×3 grid (an orphan 4th tile in a
// 3-col grid reads broken); the remaining TBD services stay on the quiet line.
{ title: 'Software', planned: true, desc: 'Landing page for CTAO software (planned).',
icon: '<rect x="3" y="4.5" width="18" height="15" rx="2"/><path d="m7 9.5 3 2.5-3 2.5M12.5 15H17"/>' },
{ title: 'Documentation', planned: true, desc: 'CTAO user documentation (planned).',
icon: '<path d="M12 6.7C10.6 5.2 8.6 4.5 5.7 4.5H4v13.6h1.7c2.9 0 4.9.7 6.3 2.2 1.4-1.5 3.4-2.2 6.3-2.2H20V4.5h-1.7c-2.9 0-4.9.7-6.3 2.2z"/><path d="M12 6.7v13.6"/>' },
];
const planned = ['Scheduling', 'Science alerts'];
--- ---
<ContentPage page={page} breadcrumb={false} /> <Base title="CTAO Science Portal" description="News, observation proposals, data and user services of the Cherenkov Telescope Array Observatory" ambient>
{/* Photographic hero: LST-1 under the La Palma night sky (copied from the
content library to /brand/hero.jpg) beneath the Galaxy scrim — text
contrast computed worst-case at design time. Three elements only:
headline, brand subtitle, CTA pair (Apple hero restraint). */}
<section class="hero-band">
<div class="band-bg" aria-hidden="true"></div>
<div class="container hero">
<div class="eyebrow">Cherenkov Telescope Array Observatory</div>
<h1>CTAO Science Portal</h1>
<p>Exploring the Universe at the Highest Energies</p>
{/* Hero actions = the two REAL things a logged-out visitor can do today:
news (SPEC §3.1 lists general information/announcements FIRST; §3.3.1
"displays the main information"; calls for proposals arrive VIA news)
and the external Data Explorer. Proposals stays in nav + tiles — a
mock doesn't earn hero billing. Search lives in the header. */}
<div class="hero-cta">
<a class="btn" href="/news">Browse news &amp; announcements</a>
<a class="btn btn--ghost external" href="https://padc-ctao-data-explorer.obspm.fr/" target="_blank" rel="noopener">Explore the data</a>
</div>
</div>
</section>
{/* Section order is task-first: Services directly under the hero, news after.
NN/g homepage principles — the homepage must give clear starting points
for the user's top tasks; for scientists that is proposals/data/support,
not reading news. Freshness is still signalled above the fold by the
one-line hero teaser, so the 3-card news section can sit below.
Surface rhythm: services on Moon Gray, editorial news on white. */}
<section class="band--moon">
<div class="container page">
<div class="section-head"><h2>Services</h2></div>
<div class="grid">
{services.map((s) => (
<article class={s.planned ? 'card tile tile--planned' : 'card tile'}>
<svg class="ico" aria-hidden="true" viewBox="0 0 24 24" set:html={s.icon} />
<h3>{s.planned
? s.title
: (s.ext
? <a class="external" href={s.href} target="_blank" rel="noopener">{s.title}</a>
: <a href={s.href}>{s.title}</a>)}</h3>
<p class="desc">{s.desc}</p>
</article>
))}
</div>
<p class="planned-note">Planned services: {planned.join(' · ')}</p>
</div>
</section>
<section class="container page">
<div class="section-head">
<h2>Latest news</h2>
<a class="more" href="/news">All news →</a>
</div>
<div class="grid">
{posts.map((post) => <NewsCard post={post} />)}
</div>
</section>
</Base>
+16
View File
@@ -0,0 +1,16 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="CTAO Science Portal - Sign in" description="Sign in via CTAO AAI (mock)">
<section class="auth-band">
<div class="band-bg" aria-hidden="true"></div>
<div class="auth-card">
<img class="auth-logo" src="/brand/CTAO_Logo_positive.svg" alt="CTAO" />
<h1>Sign in</h1>
{/* 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? <a>Register via CTAO AAI</a></p>
</div>
</section>
</Base>
+83
View File
@@ -0,0 +1,83 @@
---
import { getCollection } from 'astro:content';
import Base from '../../layouts/Base.astro';
import NewsCard from '../../components/NewsCard.astro';
import { PAGE_FIRST as FIRST, PAGE_REST as REST } from '../../lib/news.js';
// Paginated archive; the [...page] rest route makes page 1 the bare /news URL,
// then /news/2 … /news/N. Custom slicing instead of paginate(): page 1 holds
// FIRST items (1 featured lead + 24 grid = even 3-column rows), later pages
// REST — paginate() cannot vary page size. Chunk sizes live in lib/news.js,
// shared with sitemap.xml.js.
export async function getStaticPaths() {
const posts = (await getCollection('news', ({ data }) => !data.draft)).sort(
(a, b) => b.data.date.valueOf() - a.data.date.valueOf(),
);
const chunks = [posts.slice(0, FIRST)];
for (let i = FIRST; i < posts.length; i += REST) chunks.push(posts.slice(i, i + REST));
const lastPage = chunks.length;
const hrefOf = (n: number) => (n === 1 ? '/news' : `/news/${n}`);
return chunks.map((data, i) => {
const n = i + 1;
return {
params: { page: n === 1 ? undefined : String(n) },
props: { page: {
data,
currentPage: n,
lastPage,
url: {
prev: n > 1 ? hrefOf(n - 1) : undefined,
next: n < lastPage ? hrefOf(n + 1) : undefined,
},
} },
};
});
}
const { page } = Astro.props;
const first = page.currentPage === 1;
// Windowed page list — 1 … n-1 n n+1 … last (0 marks an ellipsis)
const nums = [];
for (let n = 1; n <= page.lastPage; n++) {
if (n === 1 || n === page.lastPage || Math.abs(n - page.currentPage) <= 1) nums.push(n);
else if (nums.at(-1) !== 0) nums.push(0);
}
const hrefFor = (n: number) => (n === 1 ? '/news' : `/news/${n}`);
---
<Base
title={first ? 'CTAO Science Portal - News' : `CTAO Science Portal - News, page ${page.currentPage}`}
description={first ? 'News and announcements of the CTAO' : `News and announcements of the CTAO, page ${page.currentPage} of ${page.lastPage}`}
>
{/* Section-landing archetype: white head, display Galaxy h1 + standfirst
(the navy band is reserved for / and /login). */}
<header class="container page-head">
<h1>News &amp; Announcements</h1>
<p class="standfirst">Science highlights and updates from the observatory and its partners.</p>
</header>
<section class="container page">
<h2 class="sr-only">{first ? 'All news' : `All news, page ${page.currentPage}`}</h2>
<div class="grid">
{page.data.map((post, i) => <NewsCard post={post} featured={first && i === 0} />)}
</div>
{page.lastPage > 1 && (
<nav class="pagination" aria-label="News pages">
{page.url.prev
? <a class="page-link page-step" href={page.url.prev} rel="prev">← Newer</a>
: <span class="page-link page-step" aria-hidden="true">← Newer</span>}
<ol>
{nums.map((n) => (
<li>
{n === 0
? <span class="page-gap" aria-hidden="true">…</span>
: <a class="page-link" href={hrefFor(n)} aria-current={n === page.currentPage ? 'page' : undefined}><span class="sr-only">Page </span>{n}</a>}
</li>
))}
</ol>
{page.url.next
? <a class="page-link page-step" href={page.url.next} rel="next">Older →</a>
: <span class="page-link page-step" aria-hidden="true">Older →</span>}
</nav>
)}
</section>
</Base>
+144
View File
@@ -0,0 +1,144 @@
---
import { getCollection, render } from 'astro:content';
import Base from '../../layouts/Base.astro';
import { fmt, readMin as readMinOf } from '../../lib/news.js';
export async function getStaticPaths() {
// 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, newer, older } = Astro.props;
const { Content, headings } = await render(post);
const toc = headings.filter((h) => h.depth === 2 || h.depth === 3);
const readMin = readMinOf(post);
---
<Base title={`${post.data.title} - CTAO`} description={post.data.description} type="article" image={post.data.cover && encodeURI(post.data.cover)}>
{/* Reading progress (scroll-driven CSS, no JS) — styled only where
animation-timeline is supported; elsewhere it stays an empty div. */}
<div class="read-progress" aria-hidden="true"></div>
{/* TOC pattern (MDN/Stripe/NN-g): sticky right rail ≥1200px, collapsed <details>
under the title below that. CSS shows exactly one of the two. */}
{/* lang sits on the article, not <html>: the chrome (nav/footer) stays
English for screen readers even when the article body is not. */}
<div class="container article-layout">
<article class="article" lang={post.data.lang}>
<a class="back" href="/news">← All news</a>
{/* Editorial anatomy (Guardian/BBC/Reuters convention): headline →
standfirst → byline/meta → lead image → body. The standfirst is the
description made visible — cards already show it, the article should too. */}
<h1>{post.data.title}</h1>
<p class="standfirst">{post.data.description}</p>
{/* Meta — one quiet muted line: category · author · date · reading time */}
<div class="meta">
<span class="cat"><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><path d="M20 13.2 13.2 20a2 2 0 0 1-2.9 0L4 13.7V4h9.7l6.3 6.3a2 2 0 0 1 0 2.9Z" stroke-linejoin="round" /><circle cx="8.5" cy="8.5" r="1" /></svg>{post.data.category}</span>·
<span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4" /><path d="M4.5 20c1.6-3.8 4.6-5.5 7.5-5.5s5.9 1.7 7.5 5.5" /></svg>{post.data.author}</span>·
<time datetime={post.data.date.toISOString().slice(0, 10)}><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="16" rx="2" /><path d="M4 10h16M8 3v4M16 3v4" /></svg>{fmt(post.data.date)}</time>·
<span><svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></svg>{readMin} min read</span>
</div>
{post.data.cover && <img class="cover" src={encodeURI(post.data.cover)} alt="" />}
{/* TOC sits between lead image and body (Wikipedia/GOV.UK contents
position): the press head above stays unbroken, and the list is
adjacent to the content it indexes. */}
{toc.length >= 3 && (
<details class="toc toc--inline">
<summary>On this page <svg class="ico" aria-hidden="true" viewBox="0 0 24 24"><path d="m5 9 7 7 7-7" /></svg></summary>
<ol>
{toc.map((h) => (
<li class={h.depth === 3 ? 'd3' : undefined}><a href={`#${h.slug}`}>{h.text}</a></li>
))}
</ol>
</details>
)}
<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">
<strong>On this page</strong>
<ol>
{toc.map((h) => (
<li class={h.depth === 3 ? 'd3' : undefined}><a href={`#${h.slug}`}>{h.text}</a></li>
))}
</ol>
</nav>
)}
</div>
</Base>
<script>
// TOC scrollspy — minimal vanilla take on Starlight's starlight-toc.ts:
// an IntersectionObserver band near the viewport top; whichever prose block
// enters it is mapped back to its governing h2/h3, whose TOC link(s) get
// aria-current="true" (set in BOTH renderings — inline <details> and rail).
// Progressive enhancement: without JS the TOC is plain working anchors.
const links = [...document.querySelectorAll<HTMLAnchorElement>('.toc a[href^="#"]')];
if (links.length) {
let current: HTMLAnchorElement[] = [];
const setCurrent = (id: string | null) => {
const next = id ? links.filter((a) => decodeURIComponent(a.hash.slice(1)) === id) : [];
if (next[0] === current[0]) return;
for (const a of current) a.removeAttribute('aria-current');
for (const a of next) a.setAttribute('aria-current', 'true');
current = next;
};
const blocks = [...document.querySelectorAll('.prose > *')];
// Governing heading = the block itself or the nearest h2/h3[id] above it
const headingFor = (el: Element) => {
for (let i = blocks.indexOf(el); i >= 0; i--) {
if (blocks[i].matches('h2[id], h3[id]')) return blocks[i].id;
}
return null; // intro before the first heading — nothing highlighted
};
const onIntersect = (entries: IntersectionObserverEntry[]) => {
for (const e of entries) {
if (e.isIntersecting) { setCurrent(headingFor(e.target)); break; }
}
};
let observer: IntersectionObserver | undefined;
const observe = () => {
observer?.disconnect();
// Narrow band below the sticky header (--header-h 56px + 12px slack;
// JS can't read the token cheaply, keep in sync) — Starlight's rootMargin trick
const top = 68, band = 64;
const io = new IntersectionObserver(onIntersect, {
rootMargin: `-${top}px 0px ${top + band - document.documentElement.clientHeight}px`,
});
observer = io;
blocks.forEach((b) => io.observe(b));
};
observe();
let timer: ReturnType<typeof setTimeout> | undefined;
addEventListener('resize', () => { clearTimeout(timer); timer = setTimeout(observe, 200); });
// Short final section: at page bottom, the last heading wins
addEventListener('scroll', () => {
if (innerHeight + scrollY >= document.documentElement.scrollHeight - 4) {
setCurrent(headingFor(blocks[blocks.length - 1]));
}
}, { passive: true });
}
</script>
+27
View File
@@ -0,0 +1,27 @@
---
import { getCollection, render } from 'astro:content';
import Base from '../../layouts/Base.astro';
export async function getStaticPaths() {
const pages = await getCollection('pages');
return pages.map((page) => ({ params: { slug: page.id }, props: { page } }));
}
const { page } = Astro.props;
const { Content } = await render(page);
---
<Base title={`${page.data.title} - CTAO Science Portal`} description={page.data.description}>
{/* Same wrapper pattern as news/[slug] (never .container and .article on
one element — both set max-width and would depend on rule order). */}
<div class="container article-layout">
<article class="article">
{/* Static pages are footer-reached documents; same back affordance as
articles ("← All news") so deep links aren't dead ends. */}
<a class="back" href="/">← Home</a>
<h1>{page.data.title}</h1>
<div class="prose">
<Content />
</div>
</article>
</div>
</Base>
+28
View File
@@ -0,0 +1,28 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="CTAO Science Portal - Proposals" description="Observation proposals: Proposal Handling System integration (planned)">
<section class="band--moon">
<header class="container page-head">
<h1>Observation proposals</h1>
<p class="standfirst">Submit observation proposals, including ToO and MWL requests.</p>
</header>
<div class="container page">
{/* Integration landing, not a form: the PHS is APC's product — the spec
(§3.3.3) scopes this portal to integrating it, so we describe the
hand-off instead of imitating their submission UI. */}
<div class="panel panel--narrow">
<h2 class="panel-title">Proposal Handling System</h2>
<p>Submitting observation proposals, including Target of Opportunity (ToO)
and multi-wavelength (MWL) requests, happens in the Proposal Handling
System. That system is built by the APC team and integrated into this portal.</p>
<p>What the portal contributes:</p>
<ul class="panel-list">
<li>This menu entry, with a single sign-on hand-off (CTAO AAI).</li>
<li>Proposal status feedback in your <a href="/dashboard">Dashboard</a>.</li>
</ul>
<button class="btn" type="button" disabled>Open the Proposal Handling System (integration planned)</button>
</div>
</div>
</section>
</Base>
+43
View File
@@ -0,0 +1,43 @@
// RSS 2.0 feed — hand-rolled static endpoint like search.json.js (no @astrojs/rss
// dependency; the spec is 20 lines of XML). Newest 30 items, absolute URLs from
// the configured `site`.
import { getCollection } from 'astro:content';
const esc = (s = '') =>
s.replace(/[<>&'"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' })[c]);
export async function GET(context) {
const site = context.site; // always set in astro.config.mjs
const posts = (await getCollection('news', ({ data }) => !data.draft))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
.slice(0, 30);
const items = posts
.map((p) => {
const url = new URL(`/news/${p.id}`, site).href;
return ` <item>
<title>${esc(p.data.title)}</title>
<link>${url}</link>
<guid isPermaLink="true">${url}</guid>
<pubDate>${p.data.date.toUTCString()}</pubDate>
<description>${esc(p.data.description)}</description>
<category>${esc(p.data.category)}</category>
</item>`;
})
.join('\n');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>CTAO Science Portal News</title>
<link>${new URL('/news', site).href}</link>
<description>News and announcements of the Cherenkov Telescope Array Observatory</description>
<language>en</language>
<lastBuildDate>${(posts[0]?.data.date ?? new Date()).toUTCString()}</lastBuildDate>
<atom:link href="${new URL('/rss.xml', site).href}" rel="self" type="application/rss+xml"/>
${items}
</channel>
</rss>
`;
return new Response(xml, { headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' } });
}
+36
View File
@@ -0,0 +1,36 @@
---
import Base from '../layouts/Base.astro';
---
<Base title="CTAO Science Portal - Search" description="Search CTAO news and announcements">
{/* Utility archetype: ONE surface per page — Moon canvas from the landing
head down (borderless cards/panels pop against it); editorial pages stay
all-white. No mid-page surface seams. */}
<section class="band--moon">
<header class="container page-head">
<h1>Search</h1>
<p class="standfirst">Find news and announcements.</p>
</header>
<div class="container page">
<form class="search-form" role="search" action="/search" method="get">
<label class="sr-only" for="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="q" name="q" type="search" placeholder="e.g. LST camera, Council, data…" autocomplete="off" />
<button class="btn" type="submit">Search</button>
</form>
<p class="notice" id="search-status" role="status" aria-live="polite">Type to search the news archive.</p>
<ol class="search-results" id="search-results"></ol>
<noscript><p class="notice">Search requires JavaScript. Browse all news on the <a href="/news">news page</a>.</p></noscript>
</div>
</section>
</Base>
{/* search-client.js is loaded once (deferred) by the Base layout; this inline
module runs after it in the shared after-parse queue — order guaranteed. */}
<script is:inline type="module">
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)
const q0 = new URLSearchParams(location.search).get('q');
const q = document.getElementById('q');
if (q0) { q.value = q0; run(); }
q.focus();
</script>
+19
View File
@@ -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' },
});
}
+35 -7
View File
@@ -1,12 +1,40 @@
import examples from '../data/examples.json'; // Sitemap — hand-rolled static endpoint (no @astrojs/sitemap dependency; our URL
import placeholders from '../data/placeholders.json'; // set is fully known at build time). /admin is intentionally excluded.
import { getCollection } from 'astro:content';
import { PAGE_FIRST, PAGE_REST } from '../lib/news.js';
// Chunk sizes are imported from lib/news.js — the same values the
// /news/[...page] route slices with, so the page count here cannot drift.
const newsPageCount = (n) => (n <= PAGE_FIRST ? 1 : 1 + Math.ceil((n - PAGE_FIRST) / PAGE_REST));
export async function GET(context) {
const site = context.site; // always set in astro.config.mjs
const news = (await getCollection('news', ({ data }) => !data.draft))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const pages = await getCollection('pages');
// Static pages come from the actual route files, so a new src/pages/*.astro
// can never be forgotten here. 404 is excluded; /admin (subdirectory) is
// intentionally unlisted. Go-live note: /login and /dashboard are mock
// pages — filter them out before robots.txt stops disallowing everything.
const staticPages = Object.keys(import.meta.glob('./*.astro'))
.map((p) => p.slice(1, -'.astro'.length))
.filter((p) => p !== '/404')
.map((p) => (p === '/index' ? '/' : p))
.sort();
const urls = [
...staticPages, '/news',
...Array.from({ length: newsPageCount(news.length) - 1 }, (_, i) => `/news/${i + 2}`),
...pages.map((p) => `/pages/${p.id}`),
].map((path) => ` <url><loc>${new URL(path, site).href}</loc></url>`);
const articles = news.map(
(p) => ` <url><loc>${new URL(`/news/${p.id}`, site).href}</loc><lastmod>${p.data.date.toISOString().slice(0, 10)}</lastmod></url>`,
);
export function GET({ site }) {
const paths = ['/', ...placeholders.map(({ slug }) => `/${slug}/`),
...examples.map(({ slug }) => `/example/${slug}/`)];
const xml = `<?xml version="1.0" encoding="UTF-8"?> const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${paths.map((path) => ` <url><loc>${new URL(path, site).href}</loc></url>`).join('\n')} ${[...urls, ...articles].join('\n')}
</urlset>`; </urlset>
`;
return new Response(xml, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } }); return new Response(xml, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } });
} }
+29
View File
@@ -0,0 +1,29 @@
---
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.' },
];
---
<Base title="CTAO Science Portal - Support" description="User support: help desk, FAQ, forum, mailing lists (mock)">
<section class="band--moon">
<header class="container page-head">
<h1>User Support</h1>
<p class="standfirst">Help desk, FAQ, user forum and mailing lists.</p>
</header>
<div class="container page">
<h2 class="sr-only">Support channels</h2>
<div class="grid">
{channels.map((c) => (
<article class="card tile">
<h3>{c.title}</h3>
<p class="desc">{c.desc}</p>
</article>
))}
</div>
</div>
</section>
</Base>
+819 -892
View File
File diff suppressed because one or more lines are too long