AGENTS.md@website · diff
git:20260422.ec337f4 to git:20260929.e629cc7
38 added, 12 removed. Audit B to B.
# AGENTS.md — Bub Website
> Guidelines for AI agents (Copilot, Claude Code, Cursor, etc.) working on the `website/` directory.
---
## Project Overview
This is the **Bub marketing & docs site** — a static Astro site combining:
- **Landing pages** (home, 404) with custom components — i18n via `src/i18n/`.
- **Starlight docs** (`/docs/getting-started/…`, `/zh-cn/docs/getting-started/…`) auto-generated from `src/content/docs/` — i18n via Starlight built-in.
- **Blog** (`/posts/…`, `/zh-cn/posts/…`) powered by Astro content collections — i18n via `src/i18n/`.
Site URL: `https://bub.build`
---
## Tech Stack
| Layer | Tool |
|---------------|----------------------------------------|
- | Framework | **Astro 6** (static output) |
- | Docs | **@astrojs/starlight** ≥ 0.38 |
+ | Framework | **Astro 7** (static output, SSR landing pages via Cloudflare) |
+ | Docs | **@astrojs/starlight** 0.42 |
| Styling | **Tailwind CSS v4** via `@tailwindcss/vite` + `@astrojs/starlight-tailwind` |
| Component lib | shadcn/ui conventions (base-vega style) |
| Animations | `motion` (formerly Framer Motion) |
| Code blocks | `astro-expressive-code` |
| Fonts | Outfit Variable (sans), JetBrains Mono Variable (mono) |
| Icons | **@lucide/astro** — see Icon section below |
| Type checking | TypeScript strict mode |
---
## Package Manager
**pnpm** — always use `pnpm` to install packages and run scripts.
+ Use the version pinned in `package.json#packageManager`; CI reads the same field.
+ Keep the root and website `.node-version` pins aligned. Use Node 24.21.0 for
+ development and deployment; `package.json#engines` and `engineStrict` enforce
+ the dependency minimum (Node 22.19+ on the 22.x line, or Node 24+). Older Node
+ versions can silently omit optional WASM dependencies before the build fails.
+ Keep dependency overrides and build-script permissions in `pnpm-workspace.yaml`.
+ Astro 7, `@astrojs/cloudflare` 14, and Starlight 0.42 use Vite 8 and the default
+ Sätteri Markdown processor. Upgrade these packages together and do not restore
+ the old Vite 7 override. `astro-mermaid` 2.1 supports this processor but requires
+ Mermaid 10/11; keep Mermaid on 11 until the integration supports a newer major.
+ Register Mermaid before Starlight. Preserve `compressHTML: true` to retain the
+ site's HTML-aware spacing between inline elements.
+ Keep both `current` and `wasm32` in `supportedArchitectures.cpu`: Starlight's
+ HTML processors need Sätteri's WASI package when bundled for Cloudflare, while
+ Node prerendering uses the native binding. Keep strict peer checks enabled.
+ Keep `session: false` while the site has no server-side user state. The generated
+ Cloudflare configuration should contain only `ASSETS`, without a `SESSION` KV binding.
+
+ Check the [Astro migration guide](https://docs.astro.build/en/guides/upgrade-to/v7/)
+ and [Starlight changelog](https://github.com/withastro/starlight/blob/main/packages/starlight/CHANGELOG.md)
+ when updating this stack. Starlight 0.42's mobile menu uses `.sl-menu-button` and
+ `sl-sidebar-pane:popover-open`; it requires Chromium 116+, Safari 17+, or Firefox 125+.
+
```bash
pnpm install # install deps
pnpm dev # dev server
pnpm build # production build
pnpm preview # preview production build
```
## Image Handling
- Prefer the standard `Image` component from `astro:assets` for local raster assets used by pages and shared UI.
- Do not replace local logo or illustration components with raw `<img>` just to work around Cloudflare adapter behavior.
- The repository intentionally splits image handling in `astro.config.mjs`:
- `BUB_ASTRO_IMAGE_MODE=dev`: Cloudflare image service, so `make docs` works under Node without `cloudflare:workers` import failures.
- `BUB_ASTRO_IMAGE_MODE=build`: compile-time optimization with passthrough runtime, so `make docs-test`, `make docs-preview`, and production serve stable built asset URLs.
- `Makefile` is the source of truth for that mode selection. `process.argv` command detection only exists as a fallback for direct `pnpm dev` / `pnpm build` usage inside `website/`.
- If you change adapter image settings, re-verify all three paths: `make docs`, `make docs-test`, and `make docs-preview`.
---
## Directory Structure
```
website/
├── astro.config.mjs # Astro + Starlight + Tailwind config (locales, sidebar translations)
├── components.json # shadcn/ui settings (base-vega style)
├── package.json
├── tsconfig.json # strict, path alias @/* → src/*
├── public/ # static assets (logos, favicon)
├── src/
│ ├── content.config.ts # Content collections: docs (Starlight), i18n (Starlight), posts (blog)
│ ├── components/ # shared Astro components
│ │ ├── ui/ # primitives (Icon, SectionHeading)
│ │ ├── NavBar.astro
│ │ ├── Footer.astro
│ │ ├── Hero.astro
│ │ ├── Features.astro
│ │ ├── HookIntro.astro
│ │ ├── TapeModel.astro
│ │ ├── Testimonials.astro
│ │ ├── Contributors.astro
│ │ ├── PostCard.astro
│ │ └── ThemeToggle.astro
│ ├── content/
│ │ ├── docs/ # [Zone 1] Starlight content collection
│ │ │ ├── docs/ # EN docs — maps to /docs/… routes
│ │ │ │ ├── getting-started/
│ │ │ │ ├── concepts/
│ │ │ │ ├── guides/
│ │ │ │ └── extending/
│ │ │ └── zh-cn/ # Chinese translations
│ │ │ └── docs/
│ │ │ ├── getting-started/
│ │ │ ├── concepts/
│ │ │ └── …
│ │ ├── i18n/ # [Zone 1] Starlight UI string overrides — BCP-47 filenames
│ │ │ └── zh-CN.json # ⚠️ ONLY for Starlight UI — never duplicate into src/i18n/
│ │ └── posts/ # [Zone 3] Blog posts
│ │ ├── en/ # English posts
│ │ └── zh-cn/ # Chinese posts
│ ├── i18n/ # [Zone 2] Custom page i18n module (NOT for Starlight docs)
│ │ ├── ui.ts # Flat-key UI strings: nav, footer, 404, posts, site meta
│ │ ├── utils.ts # getLangFromUrl, useTranslations, getNavProps, etc.
│ │ └── landing-page.ts # Single source of truth for all landing-page copy (both locales)
│ ├── layouts/
│ │ ├── BaseLayout.astro # Shared HTML shell (head, nav, footer, scripts)
│ │ ├── LandingLayout.astro
│ │ ├── PostLayout.astro
│ │ └── PostListLayout.astro
│ ├── pages/
│ │ ├── 404.astro # 404 page (custom page, uses Zone 2 i18n)
│ │ └── [...locale]/ # Dynamic locale routing — generates EN (root) + ZH-CN variants
│ │ ├── index.astro # Landing page (both locales via getStaticPaths)
│ │ └── posts/
│ │ ├── index.astro # Post list (both locales via getStaticPaths)
│ │ └── [slug].astro # Single post (both locales via getStaticPaths)
│ └── styles/
│ └── global.css # Tailwind v4 + Starlight bridge + CSS custom properties
└── DESIGN.md # Visual design guide
```
---
## i18n Architecture
> **Design reference**: This architecture follows patterns from the [Starlight docs site](https://github.com/withastro/starlight/tree/main/docs) and the [Astro docs site](https://github.com/withastro/docs). Both use Starlight for documentation and separate systems for non-doc pages. Study those repos when in doubt.
### Three zones, clear boundaries
The site has **three distinct i18n zones**. Each zone has its own translation mechanism. **Never mix them.**
| Zone | Pages | i18n mechanism | String source |
|------|-------|---------------|---------------|
| **1 — Starlight docs** | `/docs/…`, `/zh-cn/docs/…` (all under `src/content/docs/`) | Starlight built-in i18n | `src/content/i18n/{locale}.json` + sidebar `translations` in config |
| **2 — Custom pages** | Landing (`/`, `/zh-cn/`), 404 | Project's own `src/i18n/` module | `src/i18n/ui.ts` + `src/i18n/landing-page.ts` |
| **3 — Blog** | `/posts/…`, `/zh-cn/posts/…` | Content collection + project `src/i18n/` | Post markdown in `src/content/posts/{locale}/`, UI strings in `ui.ts` |
### Zone 1 — Starlight docs i18n
Follows the **exact pattern** from [`withastro/starlight/docs`](https://github.com/withastro/starlight/tree/main/docs).
**Locale configuration** — `astro.config.mjs`:
- English is the **`root` locale** — content files live under `src/content/docs/docs/` (e.g., `docs/getting-started/run-bub-locally.mdx`).
- Other locales get **subdirectories**: `src/content/docs/zh-cn/docs/getting-started/run-bub-locally.mdx`.
- - The inner `docs/` directory creates the `/docs/` URL prefix — Starlight 0.38 has no `routePrefix` option, so this nesting is the standard way to namespace docs routes.
+ - The inner `docs/` directory creates the `/docs/` URL prefix — this nesting namespaces docs routes.
- This keeps the URL scheme consistent: `/docs/…` for docs, `/posts/…` for blog.
```js
// astro.config.mjs
locales: {
root: { label: 'English', lang: 'en' }, // ← root = no prefix for EN docs
'zh-cn': { label: '简体中文', lang: 'zh-CN' },
},
```
**Starlight UI string overrides** — `src/content/i18n/{locale}.json`:
- File names use **BCP-47 language tags** (e.g., `zh-CN.json`, not `zh-cn.json`).
- Override Starlight's built-in UI strings (theme toggle, nav labels, pagination, 404 text, etc.).
- Extend with custom keys via `i18nSchema({ extend: z.object({...}) })` in `content.config.ts` (see [Starlight docs `content.config.ts`](https://github.com/withastro/starlight/blob/main/docs/src/content.config.ts) and [Astro docs `i18n-schema.ts`](https://github.com/withastro/docs/blob/main/src/content/i18n-schema.ts) for examples).
- **Do NOT** put these strings in `src/i18n/ui.ts` — that's Zone 2 only.
**Sidebar translations** — inline in `astro.config.mjs`:
- Uses the `translations` property on each sidebar group/item.
- Keys are **BCP-47 language tags** (e.g., `'zh-CN'`), NOT URL-slug locale keys.
- This matches the Starlight docs pattern where sidebar labels carry translation maps inline.
```js
sidebar: [{
label: 'Getting Started',
translations: { 'zh-CN': '快速开始' },
- autogenerate: { directory: 'docs/getting-started' },
+ items: [{ autogenerate: { directory: 'docs/getting-started' } }],
}]
```
+ Starlight 0.39+ requires autogenerated entries inside `items`. Keep the group
+ label and its translations on the parent; set `autogenerate.collapsed` explicitly
+ if generated subgroups should start collapsed.
+
**Content schema** — `src/content.config.ts`:
- Uses `docsLoader()` + `docsSchema()` from `@astrojs/starlight/loaders` and `@astrojs/starlight/schema`.
- Uses `i18nLoader()` + `i18nSchema()` for the i18n collection.
+ - Import Zod from `astro/zod`, using `z.url()` for URL fields.
- Extend the i18n schema with a Zod object if custom Starlight component overrides need translated keys.
- Extend the docs schema if docs need custom frontmatter fields.
### Zone 2 — Custom page i18n (`src/i18n/`)
For pages **outside Starlight** (landing page, 404). Starlight has no control over these pages.
**`[...locale]` rest-param routing** — All custom pages live under `src/pages/[...locale]/`. A single page file generates both locales via `getStaticPaths()`:
```ts
// In any page under src/pages/[...locale]/
export function getStaticPaths() {
return Object.keys(languages).map((lang) => ({
params: { locale: lang === defaultLang ? undefined : lang },
props: { locale: lang as Locale },
}));
}
// → generates / (en, root)
// → generates /zh-cn/ (zh-cn)
```
**Never duplicate a page file per locale** — use this pattern instead. The 404 page is the only exception (lives at `src/pages/404.astro` root because Astro requires it there).
| File | Purpose |
|--------------------|---------|
| `ui.ts` | **Flat-key** UI string dictionary for shared UI: site meta, nav, footer, 404, post list, language switcher. English is source of truth; other locales override selectively. |
| `utils.ts` | `getLangFromUrl()`, `useTranslations()`, `useTranslatedPath()`, `getNavProps()`, `getAlternateLocaleHref()` helpers. |
| `landing-page.ts` | **Single source of truth for all landing-page copy** — both locales. Structured data (feature arrays with icons/colors, testimonials, hook stages, hero text, CTAs). |
> **Convention** (from Astro docs pattern): Landing-page components (Hero, Features, HookIntro, TapeModel, Testimonials) are **pure presentation** — they receive all text/content via props from `landing-page.ts`. They have no hardcoded text defaults. Non-landing UI strings (nav, footer, 404, posts) go in `ui.ts` and are accessed via `t()`.
### Zone 3 — Blog i18n
Blog posts are a **content collection** (`src/content/posts/{locale}/`). The collection schema includes a `locale` field to identify the language. Blog UI strings (page titles, back-links, etc.) live in `src/i18n/ui.ts` (Zone 2), not in Starlight's i18n.
### Boundary rules
| ✅ Do | ❌ Don't |
|-------|----------|
| Put Starlight UI overrides in `src/content/i18n/` | Duplicate Starlight keys into `src/i18n/ui.ts` |
| Put nav/footer/404/post-list strings in `src/i18n/ui.ts` | Put custom page strings in `src/content/i18n/` |
| Put landing-page structured copy in `src/i18n/landing-page.ts` | Put landing text in `ui.ts` or Starlight i18n |
| Use `[...locale]` rest-param pages for custom pages | Duplicate page files per locale (e.g., `zh-cn/posts/`) |
| Use `translations` on sidebar items in `astro.config.mjs` | Create separate sidebar translation files for 2 locales |
| Use BCP-47 tags (`zh-CN`) in Starlight config/i18n files | Use URL slugs (`zh-cn`) in Starlight i18n JSON filenames |
| Use URL slugs (`zh-cn`) in directory paths and page routes | Use BCP-47 tags in directory/route paths |
### Adding a UI string (non-landing pages)
1. Add the English string to `ui.ts` under the `en` object.
2. Add the translation under the target locale.
3. Use `const t = useTranslations(locale); t('your.key')` in any `.astro` file.
### Adding landing-page content
1. Add the English content to `landing-page.ts` under the `en` section.
2. Add the translation under the target locale section.
3. Pass as props from the page to the component via `getLandingPageCopy(locale)`.
### Adding a Starlight UI string override
1. Add the translated string to `src/content/i18n/zh-CN.json` (or the target locale file).
2. If the key is custom (not a built-in Starlight key), define it in the Zod extension inside `content.config.ts` via `i18nSchema({ extend: z.object({ 'your.key': z.string().optional() }) })`.
3. Reference: [Starlight i18n docs](https://starlight.astro.build/guides/i18n/).
### Adding a new locale
1. **Starlight zone**: Add the locale to `astro.config.mjs` `locales`. Create `src/content/docs/<locale>/docs/` with translated docs. Create `src/content/i18n/<BCP-47>.json` for UI overrides.
2. **Custom page zone**: Add the locale key to `languages` in `ui.ts`. Add translations in `ui.ts` and `landing-page.ts`. The `[...locale]` pages auto-generate routes via `getStaticPaths()`.
3. **Blog zone**: Create `src/content/posts/<locale>/` with translated posts.
### URL scheme
- English (default): **no prefix** — `/`, `/posts/`, `/docs/getting-started/`, etc.
- Chinese: `/zh-cn/`, `/zh-cn/posts/`, `/zh-cn/docs/getting-started/`, etc.
- Starlight docs have a `/docs/` prefix via the `docs/` content subdirectory.
- Custom pages (landing, blog) use `[...locale]` rest-param routing — one page file generates both `/` and `/zh-cn/` variants.
### Locale key conventions
| Context | Key format | Example |
|---------|-----------|---------|
| URL paths & directory names | lowercase slug | `zh-cn` |
| `astro.config.mjs` locale keys | lowercase slug | `'zh-cn': { ... }` |
| `astro.config.mjs` sidebar `translations` keys | BCP-47 | `'zh-CN': '快速开始'` |
| `src/content/i18n/` filenames | BCP-47 | `zh-CN.json` |
| HTML `lang` attribute | BCP-47 | `zh-CN` |
| `src/i18n/ui.ts` locale keys | lowercase slug | `'zh-cn': { ... }` |
### Adding a new page
**Custom page** (landing/blog style):
1. Create `src/pages/[...locale]/<path>.astro` with `getStaticPaths()` generating all locales.
2. Import and wrap with the appropriate layout (BaseLayout, PostLayout, etc.).
3. Extract text into `ui.ts` (Zone 2) with appropriate flat keys.
4. Run `pnpm build` to verify.
**Starlight doc page**:
1. Create the English file in `src/content/docs/docs/<section>/<slug>.md`.
2. Create the Chinese translation in `src/content/docs/zh-cn/docs/<section>/<slug>.md`.
3. Sidebar item is auto-generated if the parent group uses `autogenerate`. Otherwise add to `sidebar` in `astro.config.mjs` with `translations`.
4. Run `pnpm build` to verify.
### Adding a new component
1. Create in `src/components/`.
2. **No hardcoded text defaults** — components are pure presentation. Accept all text/content via props.
3. Let the caller pass content via `getLandingPageCopy()` or `t()` from the page level.
4. Add `data-reveal` if the component should animate on scroll.
### Common mistakes
| Mistake | Fix |
|---------|-----|
| Writing `<!doctype html>` in a page | Use BaseLayout |
| Hardcoding nav/footer props in a page | Use `getNavProps()` |
| Adding nested objects to `ui.ts` | Use flat `'section.key'` format |
| Using non-Lucide icon libraries | Use `@lucide/astro` via `Icon.astro` |
| Adding `dark:text-*` for themed colors | Use semantic tokens (`text-foreground`) that auto-switch |
| Duplicating scroll-reveal `<script>` | BaseLayout includes it |
| Putting landing text in `ui.ts` | Use `landing-page.ts` — `ui.ts` is for shared UI only |
| Hardcoding English in component defaults | Components are pure presentation; all text from i18n |
| Duplicating a page file per locale | Use `[...locale]` rest-param routing with `getStaticPaths()` |
| Putting custom page strings in `src/content/i18n/` | Zone 1 only. Custom pages use `src/i18n/` (Zone 2) |
| Putting Starlight overrides in `src/i18n/ui.ts` | Zone 2 only. Starlight uses `src/content/i18n/` (Zone 1) |
| Naming i18n JSON with URL slugs (`zh-cn.json`) | Use BCP-47 (`zh-CN.json`) for Starlight i18n files |
| Using BCP-47 in directory paths (`docs/zh-CN/`) | Use lowercase slugs (`docs/zh-cn/`) for directories |
| Using `'zh-cn'` in sidebar `translations` keys | Use BCP-47 (`'zh-CN'`) |
| Adding hue/chroma to Starlight color overrides | Site is monochrome — keep oklch with 0 chroma |
| Adding new `--sl-color-*` overrides without checking the existing unlayered block | Edit the existing `:root { --sl-color-* }` / `.dark, [data-theme="dark"] { --sl-color-* }` blocks — they override the bridge for correct monochrome contrast |
| Importing `@import "tailwindcss"` in `global.css` | Use `tailwindcss/theme.css` + `tailwindcss/utilities.css` in proper layers — full import brings Preflight that breaks Starlight/EC |
---
## Icons — @lucide/astro
All icons come from **Lucide** via `@lucide/astro`. The project wraps them in `src/components/ui/Icon.astro` which handles camelCase → PascalCase name mapping.
```astro
<Icon name="arrowUpRight" size={15} />
<Icon name="github" size={14} /> <!-- special-cased brand icon -->
```
- Use **camelCase** icon names (e.g., `arrowUpRight`, `radioTower`, `fileSearch`).
- Browse icons at https://lucide.dev/icons.
- The GitHub brand icon is manually defined inside `Icon.astro` (not a Lucide built-in).
---
## Theme System — CSS Custom Properties
The design system uses **oklch** color tokens defined in `src/styles/global.css`.
### Light / Dark
- Light tokens in `:root { }`.
- Dark tokens in `.dark, [data-theme="dark"] { }` — both selectors are needed to cover landing pages (`.dark` class) and Starlight docs (`[data-theme]` attribute). `ThemeToggle.astro` sets both.
- Theme toggle logic lives in `ThemeToggle.astro` (in `NavBar.astro`), with an inline init script in `BaseLayout.astro` `<head>` to prevent FOUC.
- Stored in `localStorage` under key `bub-theme`.
### Core tokens (selection)
| Token | Usage |
|----------------------|------------------------------------|
| `--background` | Page background |
| `--foreground` | Primary text |
| `--primary` | CTA buttons, active states |
| `--primary-foreground` | Text on primary backgrounds |
| `--secondary` | Hover backgrounds, cards |
| `--muted-foreground` | Secondary text, meta info |
| `--border` | All borders |
| `--card` / `--card-foreground` | Card surfaces |
| `--chart-1` … `--chart-5` | Accent colors for charts/tags |
### Fonts
| CSS var | Font | Usage |
|-----------------|--------------------------|----------------------|
| `--font-sans` | Outfit Variable | Body, headings |
| `--font-mono` | JetBrains Mono Variable | Code, badges, labels |
### Starlight + Tailwind v4 integration
Starlight ships with a blue accent (hue 224/234) and blue-tinted grays. The main site is **monochrome** (all oklch colors have `0` chroma). The bridge is `@astrojs/starlight-tailwind` — it reads `--color-accent-*` and `--color-gray-*` from the `@theme` block and generates `--sl-color-*` variables in `@layer utilities`. However, the bridge's mapping (e.g. `--sl-color-accent` ← `--color-accent-600`) does not produce the right contrast for a monochrome theme, so **manual unlayered `--sl-*` overrides are still required** for precise control. Unlayered CSS always beats `@layer utilities`.
**Reference:** [Starlight CSS + Tailwind guide](https://starlight.astro.build/guides/css-and-tailwind/)
**How `global.css` is structured (follow this order):**
```
1. Font imports (@import "@fontsource-variable/…")
2. Cascade layer ordering: @layer base, starlight, theme, components, utilities;
3. Starlight bridge: @import '@astrojs/starlight-tailwind';
4. Tailwind layers: @import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/utilities.css' layer(utilities);
- 5. Animation utilities: @import "tw-animate-css"; (unlayered — @utility can't nest)
- 6. @theme inline { … } — fonts, Starlight color scales, site design tokens, radius
- 7. :root { … } — raw light tokens (unlayered)
- 8. .dark, [data-theme="dark"] { … } — raw dark tokens (unlayered)
- 9. :root { --sl-font/color overrides } — unlayered to beat bridge @layer utilities
- 10. .dark, [data-theme="dark"] { --sl-color-* overrides }
- 11. @layer base { … } — Tailwind preflight + site base resets (lowest priority)
+ 5. @theme inline { … } — fonts, Starlight color scales, site design tokens, radius
+ 6. :root { … } — raw light tokens (unlayered)
+ 7. .dark, [data-theme="dark"] { … } — raw dark tokens (unlayered)
+ 8. :root { --sl-font/color overrides } — unlayered to beat bridge @layer utilities
+ 9. .dark, [data-theme="dark"] { --sl-color-* overrides }
+ 10. @layer base { … } — Tailwind preflight + site base resets (lowest priority)
```
**Why this order matters:**
| Layer | Contains | Priority |
|---|---|---|
| `@layer base` | Site resets (`* { border-border }`, body bg/text, etc.) | Lowest |
| `@layer starlight` | Starlight + expressive-code styles | Overrides `base` |
| `@layer theme` | Tailwind theme variables | Overrides `starlight` |
| `@layer utilities` | Tailwind utilities | Overrides `theme` |
| Unlayered CSS | Raw tokens, `--sl-font-*` / `--sl-color-*` overrides, `@media` queries | Highest |
**Key rules:**
- **NEVER use `@import "tailwindcss"`** — it brings in the full Preflight reset that conflicts with Starlight and astro-expressive-code. Only import `tailwindcss/theme.css` and `tailwindcss/utilities.css` in their proper layers.
- **Define Starlight colors via `@theme` scales** — `--color-accent-50` through `--color-accent-950` and `--color-gray-50` through `--color-gray-950`. The bridge reads these and generates `--sl-color-*` in `@layer utilities`.
- **Override `--sl-*` colors and fonts manually (unlayered)** — the bridge's auto-mapped values don't produce the right contrast for the monochrome theme. Unlayered `:root` / `.dark, [data-theme="dark"]` blocks with explicit `--sl-color-*` values win over the bridge's `@layer utilities` output.
- **Import `tailwindcss/preflight.css` in `@layer base`** — restores box-sizing, link resets, and other base styles that the split Tailwind import omits. Because `base` is the lowest layer, EC and Starlight styles still override it.
- - **`tw-animate-css` must be imported unlayered** — it contains `@utility` directives that cannot be nested inside `@layer`.
- **The `@layer base` `*` reset is safe** — because EC styles live in `@layer starlight.components` (higher priority), they always win.
**Starlight color scales (in `@theme`):**
Both scales use pure neutral oklch values (0 chroma) to match the site's monochrome identity:
| Scale | Example values | Starlight usage |
|---|---|---|
| `--color-accent-50` … `--color-accent-950` | oklch(0.985 0 0) → oklch(0.145 0 0) | Links, highlights, active nav items |
| `--color-gray-50` … `--color-gray-950` | oklch(0.985 0 0) → oklch(0.145 0 0) | Backgrounds, text, borders |
> **Do NOT** add hue or chroma to these scales — the entire site identity is neutral/monochrome. Aside callout colors (orange, green, blue, purple, red) are intentionally left at Starlight defaults for semantic clarity.
**How to customize Starlight appearance going forward:**
1. To change Starlight's gray palette → update `--color-gray-*` values in `@theme inline`.
2. To change Starlight's accent color → update `--color-accent-*` values in `@theme inline`.
3. To change fonts → update `--font-sans` / `--font-mono` in `@theme inline` AND `--sl-font` / `--sl-font-mono` in the unlayered `:root` block.
4. To add site-specific overrides that should beat Starlight → put them **unlayered** (outside any `@layer`).
5. To add base resets that Starlight/EC can override → put them in `@layer base`.
**Overriding expressive-code on custom pages (e.g., Hero):**
When using `<Code />` from `astro-expressive-code` outside Starlight docs, override EC's CSS custom properties in the component's scoped `<style>` block:
```css
.hero-terminal :global(.expressive-code) {
--ec-codeBg: var(--card);
--ec-frm-edTabBarBg: var(--secondary);
/* … etc. — see Hero.astro for the full list */
}
```
Scoped Astro styles are unlayered, so they beat both `@layer base` and `@layer starlight`.
### Radius
Radius tokens follow a scale: `--radius-sm` through `--radius-4xl`, all computed from `--radius: 0.625rem`.
---
## Layout Hierarchy
```
BaseLayout.astro ← HTML shell, <head>, NavBar, Footer, scroll-reveal, back-to-top
├── LandingLayout.astro ← Hero + section components
├── PostLayout.astro ← Single blog post with article styles
├── PostListLayout.astro ← Blog listing page
└── (404.astro uses BaseLayout directly)
```
**All pages go through BaseLayout.** Never duplicate `<!doctype>`, `<head>`, `NavBar`, `Footer`, or scroll-reveal scripts.
---
## Coding Conventions
- **Astro components** for static content; avoid client-side JS unless interactive.
- **Tailwind utility classes** for styling; avoid custom CSS unless for animations.
- `class:list` for conditional classes.
- Props interfaces at the top of the frontmatter.
- `data-reveal` attribute on elements that should animate on scroll.
- Keep line length ≤ 120 chars.
---
## Before Committing
1. `pnpm build` — must pass with no errors.
2. Check for hardcoded nav props — use `getNavProps()` from `i18n/utils.ts`.
3. Check for duplicated HTML shell — compose on `BaseLayout`.
4. Ensure all user-visible strings use the correct zone:
- **Zone 1** (Starlight docs): UI overrides in `src/content/i18n/`, sidebar translations inline in config.
- **Zone 2** (Custom pages): Landing copy → `landing-page.ts`, shared UI strings → `ui.ts`.
- **Zone 3** (Blog): Post content in `src/content/posts/{locale}/`, UI strings in `ui.ts`.
- Never duplicate text across zones.