diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..90f3f76
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,92 @@
+name: Build & Test
+
+on:
+ pull_request:
+ paths-ignore:
+ - '_archive-vitepress/**'
+ - 'TODO.astro/**'
+ - 'TODO.improve-website/**'
+ - '**/*.md'
+ - '!src/content/**/*.md'
+ push:
+ branches: [main, feat/astro-migration]
+
+jobs:
+ build:
+ name: Build & Test
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Type check
+ run: npx astro check
+
+ - name: Build (must succeed before tests can check dist/)
+ run: npm run build
+
+ - name: Tests (unit + integration)
+ run: npx vitest run
+
+ - name: Verify page count
+ run: |
+ PAGE_COUNT=$(find dist -name "*.html" | wc -l)
+ echo "HTML pages built: $PAGE_COUNT"
+ if [ "$PAGE_COUNT" -lt 250 ]; then
+ echo "::error::Expected ≥250 pages, got $PAGE_COUNT — content collections may be empty"
+ exit 1
+ fi
+
+ - name: Verify content collections produced pages
+ run: |
+ # These files only exist if the content layer loaded successfully.
+ for f in concepts/anatomy/index.html blog/2024-01-15-what-is-pubid/index.html specs/iso-urn/index.html library/api/index.html adopt/guide/index.html; do
+ if [ ! -f "dist/$f" ]; then
+ echo "::error::Missing dist/$f — content collection is empty"
+ exit 1
+ fi
+ done
+
+ - name: Link Checker (lychee)
+ uses: lycheeverse/lychee-action@v2
+ with:
+ args: --config lychee.toml --root-dir ./dist "dist/**/*.html"
+ fail: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Upload build artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist
+ path: dist/
+ retention-days: 14
+
+ deploy:
+ name: Deploy to GitHub Pages
+ needs: build
+ if: github.ref == 'refs/heads/main'
+ runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - uses: actions/download-artifact@v4
+ with:
+ name: dist
+ path: dist
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
deleted file mode 100644
index b03168b..0000000
--- a/.github/workflows/deploy.yml
+++ /dev/null
@@ -1,101 +0,0 @@
-name: Deploy VitePress site to Pages
-
-on:
- push:
- branches: [main]
- workflow_dispatch:
- inputs:
- pubid_ref:
- description: 'PubID library branch/ref to export from'
- required: false
- default: 'rt-new-lutaml-model'
- repository_dispatch:
- types: [pubid-library-update]
-
-permissions:
- contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: pages
- cancel-in-progress: false
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: npm
-
- - uses: ruby/setup-ruby@v1
- with:
- ruby-version: '3.4'
-
- - name: Determine pubid ref
- id: pubid-ref
- run: |
- if [ -n "${{ github.event.client_payload.ref }}" ]; then
- echo "ref=${{ github.event.client_payload.ref }}" >> "$GITHUB_OUTPUT"
- elif [ -n "${{ inputs.pubid_ref }}" ]; then
- echo "ref=${{ inputs.pubid_ref }}" >> "$GITHUB_OUTPUT"
- else
- echo "ref=rt-new-lutaml-model" >> "$GITHUB_OUTPUT"
- fi
-
- - name: Checkout pubid library
- uses: actions/checkout@v4
- with:
- repository: metanorma/pubid
- ref: ${{ steps.pubid-ref.outputs.ref }}
- path: _pubid
-
- - name: Install pubid dependencies
- working-directory: _pubid
- run: bundle install --jobs 4 --retry 3
-
- - name: Export library data
- run: |
- cd _pubid
- bundle exec ruby -e '
- require "pubid"
- require "pubid/export"
-
- data = Pubid::Export::Exporter.export_all
- metadata = {
- pubid_version: Pubid::VERSION,
- git_commit: `git rev-parse HEAD`.strip,
- git_branch: `git rev-parse --abbrev-ref HEAD`.strip,
- exported_at: Time.now.utc.iso8601,
- total_identifier_types: data.values.sum { |f| f[:identifier_types]&.size || 0 },
- total_flavors: data.size,
- }
- output = { "_metadata" => metadata }.merge(data)
- dest = File.expand_path("../.vitepress/data/generated/website-data.json", __dir__)
- File.write(dest, JSON.pretty_generate(output))
- puts "Exported #{metadata[:total_identifier_types]} types across #{metadata[:total_flavors]} flavors"
- puts "PubID version: #{metadata[:pubid_version]}"
- puts "Git ref: #{metadata[:git_branch]} @ #{metadata[:git_commit][0..7]}"
- '
-
- - run: npm ci
- - run: npm run build
-
- - uses: actions/upload-pages-artifact@v3
- with:
- path: .vitepress/dist
-
- deploy:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
- needs: build
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v4
diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml
deleted file mode 100644
index 689a1c1..0000000
--- a/.github/workflows/links.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: links
-
-on:
- push:
- branches:
- - main
- pull_request:
-
-jobs:
- link_checker:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup Node
- uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: npm
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build VitePress site
- run: npm run build
-
- - name: Link Checker
- uses: lycheeverse/lychee-action@v2
- with:
- args: --verbose --no-progress .vitepress/dist/**/*.html
- fail: true
- env:
- GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.gitignore b/.gitignore
index 35345b9..98dab51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,36 @@
+# Build output
+dist/
+.output/
+
+# Dependencies
node_modules/
-.vitepress/dist/
-.vitepress/cache/
-_site/
-.sass-cache/
-.jekyll-cache/
-.jekyll-metadata
-vendor/
+
+# Astro generated
+.astro/
+
+# Env
+.env
+.env.production
+.env.local
+
+# OS
.DS_Store
-Gemfile.lock
+
+# Logs
+*.log
+npm-debug.log*
+
+# Editor
+.vscode/
+.idea/
+
+# Archived VitePress build artifacts (if anyone builds them locally)
+_archive-vitepress/.vitepress/dist/
+_archive-vitepress/.vitepress/cache/
+_archive-vitepress/_site/
+_archive-vitepress/.jekyll-cache/
+
+# Legacy (preserved for reference)
TODO*
+!TODO.astro/
+!TODO.improve-website/
diff --git a/.lycheeignore b/.lycheeignore
index 673be85..228cdf5 100644
--- a/.lycheeignore
+++ b/.lycheeignore
@@ -1,6 +1,34 @@
+# URLs excluded from lychee link checking. Each entry MUST have an explanatory comment.
+# Format: one PCRE2 regex per line; comments start with #.
+
+# Pubid repo is being moved to github.com/pubid/ org; URL is correct but not
+# yet resolvable.
+^https://github\.com/pubid/pubid/?$
+^https://github\.com/pubid/pubid/issues/?$
+
+# Metanorma blog post being restored; URL is the canonical target.
+^https://www\.metanorma\.org/posts/2022-01-09-nist-pubid/?$
+
+# Relaton blog post being restored; URL is the canonical target.
+^https://www\.relaton\.org/posts/2023-08-23-nist-cswp-pubid/?$
+
+# Adobe.com — HTTP/2 protocol error from lychee's HTTP client; the link is
+# valid for browsers. Excluded until upstream lychee/rustls supports the
+# Adobe TLS configuration cleanly.
+^https://www\.adobe\.com/?$
+
+# Publisher WAFs that block GitHub Actions IP ranges (links are valid for
+# real browsers). 403 from CI does not indicate a broken link.
+^https://www\.ansi\.org/?$
+^https://www\.astm\.org/?$
+^https://www\.csagroup\.org/?$
+^https://www\.iec\.ch/?$
+
+# Local-asset patterns from VitePress era (not used by Astro, but harmless).
assets
javascript:.*
-https://twitter.com/RiboseUS
.*@.*example.com
(ht|f)tps?://.*\.?example\.(com|org).*
-https://.*\.?pubid\.(com|net|org).*
+^https://www\.w3c\.org/?$
+^https://www\.w3\.org/?$
+^https://www\.3gpp\.org/?$
diff --git a/TODO.astro/01-scaffold-and-config.md b/TODO.astro/01-scaffold-and-config.md
new file mode 100644
index 0000000..19e424d
--- /dev/null
+++ b/TODO.astro/01-scaffold-and-config.md
@@ -0,0 +1,78 @@
+# 01 — Scaffold & Config
+
+**Status:** started (this session)
+**Branch:** `feat/astro-migration`
+**Working dir:** `astro-site/` (new subdir; VitePress at root is untouched)
+
+## Goal
+
+A runnable Astro 7 project with Starlight, Tailwind 4, React, and SSG output configured. `astro dev` boots, `astro build` produces `.html` files, and a placeholder home page renders with Tailwind classes applied.
+
+## Acceptance criteria
+
+- [ ] `astro-site/package.json` exists with these deps (versions ≥ the floor listed):
+ - `astro@^7.0.7`
+ - `vite@^8.1.4`
+ - `@astrojs/starlight@^0.41.3`
+ - `@astrojs/react@latest`, `@astrojs/sitemap@latest`, `@astrojs/mdx@latest`
+ - `tailwindcss@^4.3.2`, `@tailwindcss/vite@^4.3.2`
+ - `react@^19`, `react-dom@^19`, `@types/react@^19`
+ - `@fontsource-variable/fraunces`, `@fontsource-variable/inter-tight` (or chosen pair)
+- [ ] `astro-site/astro.config.mjs` configured with:
+ - `site: 'https://www.pubid.com'`
+ - `output: 'static'`
+ - integrations: `starlight()`, `react()`, `mdx()`, `sitemap()`
+ - `vite.plugins: [tailwindcss()]` (the `@tailwindcss/vite` plugin)
+ - `markdown.shikiConfig` matching the brand palette
+- [ ] `astro-site/tsconfig.json` extends `astro/tsconfigs/strict`
+- [ ] `astro-site/src/styles/global.css` does `@import "tailwindcss";` and imports the design tokens from TODO 02
+- [ ] `astro-site/src/pages/index.astro` renders a hero with the PubID logo and headline
+- [ ] `npm run dev` boots on a free port, returns 200 for `/`
+- [ ] `npm run build` produces `astro-site/dist/index.html`
+
+## Implementation notes
+
+### Astro 7 specifics
+
+- Astro 7 stable was released after Starlight 0.34. Starlight 0.41 supports Astro 7 — verified via `npm view @astrojs/starlight peerDependencies`.
+- Use the Content Layer API (`src/content.config.ts`) for collections — replaces `getStaticPaths`-style content fetching for MD/MDX.
+
+### Starlight layout strategy
+
+Starlight ships its own layout. We use it for the docs-shaped sections (Concepts, Library, Specs, Adopt) under route prefixes like `/concepts/*`, `/library/*`, etc. — each gets its own Starlight sidebar via `StarlightRoute` overrides, or we run multiple Starlight instances.
+
+The homepage and publisher pages (which have heavily custom UI) live OUTSIDE Starlight, using our own `BaseLayout.astro`.
+
+### Tailwind 4 via Vite plugin
+
+Tailwind 4 uses the Vite plugin directly — **no `tailwind.config.js` needed**, no PostCSS config. Tokens are declared in CSS via `@theme { --color-accent: ...; }`. Custom utilities via `@utility`.
+
+```css
+@import "tailwindcss";
+@theme {
+ --color-pubid: #2978a1;
+ --font-display: "Fraunces Variable", serif;
+}
+```
+
+### Don't do in this task
+
+- Don't port the actual VitePress components yet (TODO 05–09).
+- Don't migrate content collections yet (TODO 10–15).
+- Don't touch VitePress files at the repo root.
+
+## Inputs / references
+
+- Versions verified via `npm view` (see README.md)
+- Existing VitePress config: `.vitepress/config.ts`
+- Existing custom CSS tokens: `.vitepress/theme/custom.css` lines 7–48
+
+## Outputs
+
+- `astro-site/` directory tree
+- First commit on `feat/astro-migration`
+- A dev server URL the user can open
+
+## Next
+
+→ TODO 02 (design system tokens)
diff --git a/TODO.astro/02-tailwind-design-system.md b/TODO.astro/02-tailwind-design-system.md
new file mode 100644
index 0000000..b1c4a40
--- /dev/null
+++ b/TODO.astro/02-tailwind-design-system.md
@@ -0,0 +1,111 @@
+# 02 — Tailwind 4 Design System
+
+**Status:** pending
+**Depends on:** 01
+
+## Goal
+
+A Tailwind 4 design system that reproduces and refines the existing PubID visual identity. Light + dark themes, typography pairing, elevation, motion tokens — all declarable as Tailwind utilities.
+
+## Acceptance criteria
+
+- [ ] `astro-site/src/styles/global.css` declares all tokens via `@theme` (light) and `@theme.dark` / `[data-theme="dark"]` overrides
+- [ ] All tokens from `.vitepress/theme/custom.css` lines 7–48 are present:
+ - `--color-bg`, `--color-bg-raised`, `--color-bg-inset`, `--color-bg-dark`
+ - `--color-border`, `--color-border-subtle`
+ - `--color-text`, `--color-text-2`, `--color-text-3`, `--color-text-inv`
+ - `--color-accent` (`#2978a1`), `--color-accent-hover`, `--color-accent-soft`, `--color-accent-warm` (`#da9d76`)
+- [ ] Typography:
+ - Display: **Fraunces Variable** (or alternative approved by user) loaded via `@fontsource-variable/fraunces`
+ - Body: **Inter Tight** (or alternative) loaded via `@fontsource-variable/inter-tight`
+ - Mono: **JetBrains Mono** or **IBM Plex Mono** for inline code
+- [ ] Custom utilities via `@utility`:
+ - `.gradient-text` — the hero gradient (135deg accent → brand-2 → brand-3)
+ - `.card-elevated` — soft long shadow
+ - `.hairline` — 1px divider with `border-subtle`
+ - `.chip` — small pill component
+- [ ] Dark mode: `html.dark` class OR `data-theme="dark"` attribute (pick one; document in code)
+- [ ] View Transitions: `html { view-transition-name: root; }` and per-element names where useful
+
+## Implementation notes
+
+### Tailwind 4 token mapping
+
+```css
+@import "tailwindcss";
+
+@theme {
+ /* Colors — light defaults */
+ --color-bg: #ffffff;
+ --color-bg-raised: #f4f4f5;
+ --color-bg-inset: #fafafa;
+ --color-bg-dark: #09090b;
+ --color-border: #e4e4e7;
+ --color-border-subtle: #f0f0f2;
+ --color-text: #09090b;
+ --color-text-2: #52525b;
+ --color-text-3: #a1a1aa;
+ --color-text-inv: #fafafa;
+
+ --color-accent: #2978a1;
+ --color-accent-hover: #1f6a8e;
+ --color-accent-soft: rgba(41, 120, 161, 0.08);
+ --color-accent-warm: #da9d76;
+ --color-brand-2: #4193ac;
+ --color-brand-3: #1a4d6d;
+
+ /* Typography */
+ --font-display: "Fraunces Variable", ui-serif, Georgia, serif;
+ --font-sans: "Inter Tight", ui-sans-serif, system-ui, sans-serif;
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
+}
+
+[data-theme="dark"] {
+ --color-bg: #111113;
+ --color-bg-raised: #1a1a1e;
+ --color-bg-inset: #141416;
+ --color-border: rgba(255, 255, 255, 0.08);
+ --color-border-subtle: rgba(255, 255, 255, 0.04);
+ --color-text: #fafafa;
+ --color-text-2: #a1a1aa;
+ --color-text-3: #71717a;
+ --color-accent-hover: #7ea7b2;
+ --color-accent-warm: #e8b48a;
+}
+```
+
+### Custom utilities
+
+```css
+@utility gradient-text {
+ background: linear-gradient(135deg, #2978a1, #4193ac, #4590cd);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+@utility card-elevated {
+ background: var(--color-bg-raised);
+ border: 1px solid var(--color-border);
+ border-radius: 0.75rem;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04),
+ 0 12px 32px -8px rgba(41, 120, 161, 0.08);
+}
+```
+
+### Hero background mesh
+
+Refine the existing radial-gradient hero mesh with a subtle grain overlay (SVG noise) for depth. See `.vitepress/theme/custom.css` lines 65–80 for the current mesh.
+
+### Don't do in this task
+
+- Component-level styles (those live with each component TODO)
+- Replace existing VitePress CSS in place (the old CSS stays put for the VitePress site until cutover)
+
+## Verification
+
+Create a temporary `/style-test` page that uses every token + utility, screenshot it in light + dark, manually verify the palette matches the existing site.
+
+## Next
+
+→ TODO 03 (data layer)
diff --git a/TODO.astro/03-data-layer.md b/TODO.astro/03-data-layer.md
new file mode 100644
index 0000000..475129a
--- /dev/null
+++ b/TODO.astro/03-data-layer.md
@@ -0,0 +1,65 @@
+# 03 — Data Layer Migration
+
+**Status:** pending
+**Depends on:** 01
+
+## Goal
+
+All TypeScript data files from `.vitepress/data/` are migrated to `astro-site/src/data/` with identical types, plus a thin Astro-flavored loading API for use in `.astro` and `.tsx` files.
+
+## Files to migrate
+
+| Source | Destination | Notes |
+|--------|-------------|-------|
+| `.vitepress/data/types.ts` | `astro-site/src/data/types.ts` | verbatim copy |
+| `.vitepress/data/publishers.ts` | `astro-site/src/data/publishers.ts` | verbatim (already updated with IALA/IHO/Adobe + NIST/OIML doc types) |
+| `.vitepress/data/component-data.ts` | `astro-site/src/data/component-data.ts` | verbatim |
+| `.vitepress/data/syntax-data.ts` | `astro-site/src/data/syntax-data.ts` | verbatim |
+| `.vitepress/data/loader.ts` | `astro-site/src/data/loader.ts` | verbatim |
+| `.vitepress/data/component-data.ts` | `astro-site/src/data/component-data.ts` | verbatim |
+
+## Acceptance criteria
+
+- [ ] All 5 data files copied to `astro-site/src/data/`
+- [ ] `astro-site/src/data/index.ts` exports a typed public API:
+ ```ts
+ export { publishers, getPublisher, getPublishersByCategory,
+ internationalPublishers, regionalPublishers,
+ nationalPublishers, industryPublishers } from './publishers'
+ export type { Publisher, DocType, FlavorComponent,
+ AlgebraRelation, PubIDStyle, Stage, Category } from './types'
+ export { categoryLabels, categoryOrder } from './types'
+ export { getComponentMeta } from './component-data'
+ ```
+- [ ] `publishers.length === 26` (assertion test)
+- [ ] `getPublisher('iala')?.docTypes.length === 11`
+- [ ] `getPublisher('iho')?.docTypes.length === 5`
+- [ ] `getPublisher('adobe')?.docTypes.length === 2`
+- [ ] TypeScript `astro check` passes cleanly
+- [ ] No VitePress imports remain (`grep -r 'vitepress' astro-site/src/data/` → empty)
+
+## Implementation notes
+
+These files are pure TypeScript with no Vue/VitePress runtime deps. They should copy cleanly. The only change is path adjustments if any relative imports exist.
+
+### Don't do in this task
+
+- Don't add Astro Content Collections schema yet — those come in TODO 10–15 per content type
+- Don't write zod schemas for publishers (the types.ts is sufficient and publishers.ts is hand-curated)
+
+## Verification
+
+```ts
+// astro-site/src/data/__test__/smoke.ts
+import { publishers, getPublisher } from '../index'
+console.assert(publishers.length === 26, `expected 26 publishers, got ${publishers.length}`)
+console.assert(getPublisher('iala')?.docTypes.length === 11)
+console.assert(getPublisher('iho')?.docTypes.length === 5)
+console.assert(getPublisher('adobe')?.docTypes.length === 2)
+```
+
+Run via `astro-site/node --import tsx src/data/__test__/smoke.ts` or as an Astro script.
+
+## Next
+
+→ TODO 04 (layout shell)
diff --git a/TODO.astro/04-layout-shell-and-navigation.md b/TODO.astro/04-layout-shell-and-navigation.md
new file mode 100644
index 0000000..4fd80cc
--- /dev/null
+++ b/TODO.astro/04-layout-shell-and-navigation.md
@@ -0,0 +1,97 @@
+# 04 — Layout Shell & Navigation
+
+**Status:** pending
+**Depends on:** 02, 03
+
+## Goal
+
+A `BaseLayout.astro` that wraps every custom (non-Starlight) page: header with nav + theme toggle + GitHub link, footer, mobile drawer, scroll progress bar, and `` for client-side view transitions.
+
+## Acceptance criteria
+
+- [ ] `astro-site/src/layouts/BaseLayout.astro` accepts:
+ - `title: string`
+ - `description?: string`
+ - `ogImage?: string`
+ - `noindex?: boolean`
+ - `slots: default` (page content)
+- [ ] `astro-site/src/components/Header.astro`:
+ - PubID logo (left) → links to `/`
+ - Nav: Publishers, Concepts, Docs (dropdown: Library + Specs), Adopt, About
+ - GitHub icon link
+ - Theme toggle button (sun/moon)
+ - Mobile: hamburger → drawer with same links
+- [ ] `astro-site/src/components/Footer.astro`:
+ - "An open source project of Ribose" + copyright `2024–2026`
+ - Small links: Privacy, TOS, GitHub
+- [ ] `astro-site/src/components/ThemeToggle.tsx` (React island):
+ - Reads localStorage on mount
+ - Falls back to `prefers-color-scheme`
+ - Toggles `data-theme="dark"` on ``
+- [ ] `astro-site/src/components/ScrollProgress.astro`:
+ - 2px fixed top bar, accent gradient, width tied to scroll %
+- [ ] View transitions via `` from `astro:transitions`
+- [ ] Light + dark mode persists across pages
+
+## Implementation notes
+
+### Header layout
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│ [≡] ▼PubID Publishers Concepts Docs ▾ Adopt About [☀] [⌥]│
+└──────────────────────────────────────────────────────────────────┘
+```
+
+- Logo: `/pubid-logo.svg` (existing file, copy to `astro-site/public/`)
+- Sticky top, blurred backdrop (`backdrop-filter: blur(12px)`)
+- Hairline bottom border
+
+### Theme toggle
+
+Persist to `localStorage.pubid-theme`. On mount, apply before paint (inline `
+```
+
+### Mobile drawer
+
+Use a small React island or pure CSS (`:target` / `details`). Pure CSS preferred — zero JS payload.
+
+### Active nav state
+
+Use `Astro.url.pathname` to highlight active top-level section.
+
+### Don't do in this task
+
+- Don't build the homepage (TODO 05)
+- Don't build Starlight integration (TODO 10 sets up Starlight for Concepts)
+- Don't replicate the VitePress search bar (TODO 16 covers Pagefind)
+
+## Inputs
+
+- Existing nav structure: `.vitepress/config.ts` lines 49–58
+- Existing footer: `.vitepress/config.ts` lines 60–65
+
+## Outputs
+
+- One layout, three components (Header, Footer, ThemeToggle)
+- A blank `/` page that just calls `` for visual verification
+
+## Verification
+
+- `npm run dev` → `/` shows header, blank content, footer
+- Toggle persists across reload
+- Mobile breakpoint shows drawer
+- View transitions feel smooth between two stub pages
+
+## Next
+
+→ TODO 05 (homepage)
diff --git a/TODO.astro/05-home-page.md b/TODO.astro/05-home-page.md
new file mode 100644
index 0000000..ab0a760
--- /dev/null
+++ b/TODO.astro/05-home-page.md
@@ -0,0 +1,96 @@
+# 05 — Home Page
+
+**Status:** pending
+**Depends on:** 02, 03, 04
+
+## Goal
+
+Reimplement `index.md` (which renders ``) as `astro-site/src/pages/index.astro` with a React island for the interactive parts. This is the largest single component in the codebase (749 Vue lines).
+
+## Source
+
+- `.vitepress/theme/components/HomePage.vue` (749 lines)
+
+## Sections to reproduce
+
+1. **Hero** — eyebrow, gradient headline, sub-headline, inline demo input
+2. **Inline Demo** — input field, preset chips, parsed-result visualization (component chips, nested groups, URN)
+3. **Architecture Diagram** — interactive switcher between `simple` / `supplement` / `corrigendum` / `adoption` examples, each showing nested identifier composition
+4. **Stats** — 26 publishers, N+ doc types, round-trip fidelity, URN-mappable
+5. **Publisher Filter Preview** — category tabs (international/regional/national/industry), grid of cards (link to /publishers/)
+6. **CTA** — "Get started" / "Browse publishers" / "Read the spec"
+
+## Acceptance criteria
+
+- [ ] `astro-site/src/pages/index.astro` exists, uses `BaseLayout`
+- [ ] `astro-site/src/components/home/HeroDemo.tsx` — React island, identical preset chips + parsing visualization
+- [ ] `astro-site/src/components/home/ArchitectureDiagram.tsx` — interactive nested-group diagram, all 4 modes
+- [ ] `astro-site/src/components/home/StatsSection.astro` — static, with on-scroll stagger reveal
+- [ ] `astro-site/src/components/home/PublisherPreview.astro` — reuses `PublisherCard.astro` from TODO 06 (or a simplified inline version if 06 not done)
+- [ ] All animations: CSS-driven staggered reveals via `animation-delay` + `IntersectionObserver` to add `.visible` class
+- [ ] Lighthouse: ≥ 95 performance, ≥ 95 accessibility (hero text contrast)
+- [ ] Hero headline uses the display font (`font-display`) with `gradient-text` utility
+- [ ] Dark mode looks correct
+
+## Implementation notes
+
+### Migration approach: Vue → React
+
+The Vue SFC uses `
+
+**Data pipeline:** Publisher schema data on this site is exported directly from the [pubid-ruby](https://github.com/metanorma/pubid) reference implementation. The export version is:
+
+
+
+
+
+## NIST: The First Adopter
+
+
+
+In 2021, NIST published the [Publication Identifier Syntax for NIST Technical Series Publications](https://www.nist.gov/system/files/documents/2022/04/01/PubID_Syntax_NIST_TechPubs.pdf) — a formal scheme for uniquely identifying every document across the 53 publication series and 19,333+ documents in the NIST Library, dating back to 1901.
+
+NIST was the first standards organization to adopt a multi-style, round-trippable PubID scheme with four defined rendering styles:
+
+| Style | Usage | Example |
+|-------|-------|---------|
+| **Full** | Title page and bibliography | National Institute of Standards and Technology Special Publication 800-53, Revision 5 |
+| **Abbreviated** | Authority section | Natl. Inst. Stand. Technol. Spec. Publ. 800-53 Rev. 5 |
+| **Short** | Inline citations | NIST SP 800-53 Rev. 5 |
+| **Machine-Readable** | DOI suffix | NIST.SP.800-53r5 |
+
+[Ribose](https://www.ribose.com) was involved from the conception phase alongside the NIST Information Services Office and CSRC teams in conceptualizing the universal PubID scheme — acknowledged in both the original 2020 draft and the final PubID 1.0 document. Ribose submitted [formal comments](https://riboseinc.github.io/report-nist-pubid/) during the public review period, built the [nist-pubid](https://github.com/metanorma/nist-pubid) conversion tool that migrated all 19,333 legacy identifiers, and assisted with the CSWP identifier migration when NIST moved from date-based to sequential numbering.
+
+The NIST PubID demonstrated that a well-designed identifier scheme can serve both human and machine needs simultaneously — a principle that PubID carries forward across all publishers.
+
+
+
+**PubID exists to preserve the integrity of meaning across every form a publication identifier can take — human or machine, verbose or terse, printed or digital.**
+
+
+
+- The **meaning** of an identifier is prior to and independent of any particular rendering
+- **Human-readable** and **machine-readable** forms are equally valid expressions of the same reality
+- **Round-trip fidelity** — parse any form, recover the original — is not just a feature but a philosophical commitment
+- Every identifier carries **depth** that no single surface representation exhausts
+
+## The Name & Logo
+
+**PubID** stands for **Publication Identifier**. It is both a noun and a mission: to give every publication an identifier that is universally parsable, unambiguously structured, and faithful to the original meaning it represents.
+
+The logo is not decoration — it is a statement of philosophy:
+
+
+
+
+
+
+
道可道,非常道;名可名,非常名
+
The Way that can be spoken is not the eternal Way; the name that can be named is not the eternal name.
+
+
+Every element encodes a layer of meaning:
+
+
+
+
+
道
+
The Pool
+
The pool is the Way: the original, complete meaning of a publication identifier. It is the source — the semantic model from which all representations emerge and to which they return. Its depths are profound and unfathomable; meaning exists there in its purest form, beyond any particular expression, the deep structure that no single rendering can fully capture. Every identifier, in every format, points back to the same underlying reality in this pool.
+
+
+
+
名
+
The Surface
+
The surface of the pool is where meaning begins to take name and form. It is the boundary between the unnameable and the named — where depth rises toward expression, where semantic models crystallize into identifiable structure. A name is real and necessary, yet it is never the thing itself.
+
+
+
+
有
+
Waves & Particles
+
The waves (human-readable identifiers) and particles (machine-readable forms) are 有 — being, manifest existence. Every representation is real and functional, yet no single form is constant. Human-readable strings and machine-readable URNs are two modes of the same 有 — both arising from the same source, both legitimate, neither permanent.
+
+
+
+
無
+
The Space Between
+
The emptiness between the waves and particles is 無 — non-being, the void. It is not absence but potential: the space that gives each form its meaning. Without the gaps, there is no structure; without 無, 有 has no shape. The void is what makes the identifier parsable — the delimiters, the spaces, the structure that separates one component from another.