{"owner":"ethereum","repo":"ethereum-org-website","hasSkills":true,"totalSkillsCount":61,"totalTokensCount":45745,"categories":["anthropic-skill","root-instruction","command-prompt","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/new-page/SKILL.md",".claude/skills/design-system/SKILL.md",".claude/skills/image-gen/SKILL.md",".claude/skills/intl-pipeline/SKILL.md",".claude/skills/data-layer/SKILL.md",".claude/skills/intl-review/SKILL.md",".claude/skills/page-visual-tests/SKILL.md",".claude/skills/review-page/SKILL.md","AGENTS.md",".claude/skills/performance/SKILL.md",".claude/commands/fix-sanitizer-bug.md","CLAUDE.md",".claude/commands/netlify-build-check.md",".claude/commands/prepare-release.md",".claude/commands/review-release.md",".claude/commands/review-translations.md",".claude/skills/design-system/evals/evals.json",".claude/skills/design-system/references/a11y.md",".claude/skills/design-system/references/callout-walkthrough.md",".claude/skills/design-system/references/canonical-imports.md",".claude/skills/design-system/references/card-walkthrough.md",".claude/skills/design-system/references/cleanup-playbook.md",".claude/skills/design-system/references/components.md",".claude/skills/design-system/references/gotchas.md",".claude/skills/design-system/references/i18n-rtl.md",".claude/skills/design-system/references/layouts.md",".claude/skills/design-system/references/new-component-checklist.md",".claude/skills/design-system/references/page-hero-walkthrough.md",".claude/skills/design-system/references/server-vs-client.md",".claude/skills/design-system/references/spacing-typography.md",".claude/skills/design-system/references/tokens.md",".claude/skills/design-system/references/variant-vs-new.md",".claude/skills/intl-pipeline/evals/evals.json",".claude/skills/intl-pipeline/references/architecture.md",".claude/skills/intl-pipeline/references/ethglossary.md",".claude/skills/intl-pipeline/references/gotchas.md",".claude/skills/intl-pipeline/references/manifests.md",".claude/skills/intl-pipeline/references/non-english-edits.md",".claude/skills/intl-pipeline/references/orchestration.md",".claude/skills/intl-pipeline/references/recovery.md",".claude/skills/intl-pipeline/references/sanitizer.md",".claude/skills/intl-review/evals/evals.json",".claude/skills/intl-review/references/agent-roles.md",".claude/skills/intl-review/references/critical-vs-warning.md",".claude/skills/intl-review/references/ethglossary-usage.md",".claude/skills/intl-review/references/gotchas.md",".claude/skills/intl-review/references/known-patterns.md",".claude/skills/intl-review/references/language-rules.md",".claude/skills/intl-review/references/per-language-tracking.md",".claude/skills/intl-review/references/scoring-rubric.md",".claude/skills/performance/references/build.md",".claude/skills/performance/references/bundle.md",".claude/skills/performance/references/diagnose-table.md",".claude/skills/performance/references/edge-caching.md",".claude/skills/performance/references/images.md",".claude/skills/performance/references/inp.md",".claude/skills/performance/references/review-checklist.md",".claude/skills/review-page/reviewers/copy.md",".claude/skills/review-page/reviewers/design-system.md",".claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md",".claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md"],"skills":{".claude/skills/new-page/SKILL.md":"---\nname: new-page\ndescription: Interview-then-scaffold guide for adding a new page to ethereum.org. Forks first on markdown-backed vs App Router page, then picks the layout and hero, maps the Figma design to existing components, and builds it reuse-first. Invoke with /new-page.\ndisable-model-invocation: true\n---\n\n# New Page\n\nA new ethereum.org page is **reuse-first**: every region maps to a layout, hero, primitive, or variant that already exists — you *configure* what's there, you don't build a parallel version of it. This skill interviews you for the missing context, then scaffolds the page against the design system.\n\nWork the steps in order. Each ends on a checkable condition — don't start the next until the current one is met.\n\n## Step 0 — Load the design system\n\nInvoke the `design-system` skill and read its `SKILL.md` fully before asking anything else. It is the source of truth for component choices, tokens, layouts, RTL/i18n, and the server/client boundary. **Do not restate its catalog here** — when you need the layout inventory read `design-system/references/layouts.md`, and verify it against `src/layouts/index.ts` (layouts get refactored).\n\n**Done when:** `design-system` SKILL.md has been read this session.\n\n## Step 1 — Interview\n\nThe primary fork is **markdown-backed or not**. It decides the layout system, the build, and how the page is translated — so settle it first. It is an *authoring* decision (how the content is written and maintained), **not** a visual one: a hero-topped prose page can be either, because `ContentLayout` gives markdown `TopicLayout` and App Router pages the same hero + card-TOC chrome. So **ask it; never infer it from the design.**\n\nUse `AskUserQuestion`:\n\n1. **Markdown page or App Router page?**\n   - *Markdown page* — content authored in `public/content/<route>/index.md`, rendered through `[...slug]`, translated by the intl-pipeline, built from a fixed set of MDX shortcodes. Choose for editorial/educational content a writer maintains in markdown.\n   - *App Router page* — a hand-built `app/[locale]/<route>/page.tsx` that composes `ContentLayout`. Choose when the page is interactive, data-backed, or needs layout/components the markdown shortcodes can't express. Translated via `getTranslations` JSON keys.\n2. **Route** — the URL path (e.g. `/stablecoins/`). The folder name *is* the route.\n3. **Design source** — a Figma link/selection, a written description, or none.\n4. **Content** (markdown only) — does the user have the body ready, or should you draft it?\n\nThen **check the route is free**: no existing `public/content/<route>/index.md` or `app/[locale]/<route>/page.tsx`. If one exists you're *editing*, not creating — surface it and confirm before any write; never silently overwrite a page you didn't author.\n\n**Done when:** markdown-vs-App-Router, route, and design source are known, and the route is confirmed free (or editing is okayed).\n\n## Step 2 — Pull the design (only if Figma was provided)\n\nRead the design **fresh** with the Figma MCP (designs change between runs — never rely on a read from an earlier turn): `get_metadata` for the frame tree, then `get_design_context` / `get_screenshot` on each region; `search_design_system` / `get_code_connect_map` for existing code mappings. Load the `figma-use` skill before any `use_figma` call.\n\nProduce a **component map** and **always show it to the user as a table** — three columns: **Region** (each design region, top to bottom) · **Maps to** (the existing primitive + variant that renders it — `PageHero`, `Card`, `BigNumber`, …) · **Status** (`reuse`, `missing asset`, `placeholder copy`, `missing data`, …). Walk the frame top to bottom and map **every** region. The table is the user's only window into what you saw in the frame — render it every run; never just reason about the mapping internally.\n\n**Node names lie.** A node called `Screenshot 2026-…` or `Frame 1789` is routinely real content — a stat band (`BigNumber`), a video embed (`YouTube`), a callout (`Alert`) — not a throwaway. Never omit a region because its name *looks* like a draft; `get_screenshot` it and see what it actually is before deciding.\n\n**Missing data → ask, don't guess.** When a region needs a value the design doesn't carry — a YouTube ID behind a thumbnail, a real destination URL, a live data source — you can't extract it from the frame. Flag it and ask the user; a guessed embed or invented link is worse than an open question.\n\n**Match, don't mimic.** Reproduce the design with existing components and their variants. Do **not** layer custom Tailwind/CSS on a primitive to chase pixel parity. If a region has no fitting primitive or variant, the answer is *add a variant* (`design-system/references/variant-vs-new.md`) — never a one-off component or bespoke styles. Flag any such gap and confirm before inventing.\n\n**Pressure-test the authoring fork.** The map can reveal a region markdown shortcodes can't express — an interactive widget, a data-backed list, a bespoke multi-column grid, or cards with **lucide** icons (`MarkdownCard` accepts `emoji` *or* `icon: ReactNode`, but a plain `.md` file can't author a ReactNode, so from markdown the `<Card>` shortcode is effectively emoji-only). Any of these means the page wants to be App Router. If Step 1 said markdown, surface the conflict and re-confirm before scaffolding — flipping it later is a rewrite.\n\n**Done when:** the component-map table has been shown to the user and they've confirmed or corrected it; every region is accounted for — mapped, flagged for an approved variant, or raised as missing data — and the authoring fork still holds.\n\n## Step 3 — Pick the layout and hero\n\nBranches on Step 1.\n\n### Markdown page — pick the `template:`\n\nThe layout is selected by the `template:` frontmatter value (default `static`). Full inventory in `design-system/references/layouts.md`; live set in `src/layouts/index.ts`.\n\n| `template:` | Layout | Use for | Hero it renders |\n|---|---|---|---|\n| `static` (default) | `StaticLayout` | one-off editorial prose, no sub-nav | title + breadcrumbs only (`PageHero variant=\"no-divider\"`, **no image**) |\n| `use-cases` / `staking` / `roadmap` / `upgrade` | `TopicLayout` | topic hub: full side-image hero + sibling sub-nav | image `PageHero` from frontmatter `image` / `summary` / `buttons` |\n| `docs` | `DocsLayout` | developer docs with the docs sidebar | — |\n| `tutorial` | `TutorialLayout` | developer tutorial with author/date/skill metadata | — |\n\n- Each Topic template carries a distinct **MDX component list** (`componentsMapping`, `src/layouts/index.ts`) — pick the one whose shortcodes match the content.\n- **Side-image hero in the design → a Topic template** (`static` renders only a title hero). `showDropdown`, new-topic wiring, and the never-add-a-new-layout rule: `design-system/references/layouts.md`.\n\n### App Router page — compose `ContentLayout`, pass the hero that fits\n\nHand-build `app/[locale]/<route>/page.tsx` composing `ContentLayout`, and choose the hero from the design:\n\n- `PageHero` — breadcrumbs + title + optional `heroImg` + description + buttons. The workhorse; covers both image and title-only heroes.\n- `HubHero` — large hub hero (exemplar: `/learn/`).\n\nBuild the `tocItems` array by hand and wrap each prose section in `<Section id>`. `ContentLayout` owns the card TOC, contributors block, and feedback. Exemplars: `/learn/`, `/what-is-ethereum/`.\n\n**Done when:** markdown → `template:` (and `showDropdown` if relevant) chosen; App Router → hero component chosen. Confirmed with the user.\n\n## Step 4 — Scaffold\n\nBuild from the Step 2 component map. Reuse-first throughout — no raw `<a>`/`<button>`, no hex colors, logical CSS props (`ms-`/`me-`/…), locale-aware `numberFormat()`/`dateTimeFormat()`. Icons: **lucide in `.tsx`, emoji in markdown** (markdown can't pass the `icon` ReactNode prop).\n\n**Markdown page:**\n- Create `public/content/<route>/index.md` with frontmatter (`title`, `description`, `lang: en`, plus `template:` unless static). English only — never hand-write translated copies; the intl-pipeline propagates them.\n- **Inline images co-locate** next to `index.md`, referenced relatively — a `/images/...` inline path 500s the MDX render; the frontmatter `image:` hero is the exception. Details: `design-system/references/gotchas.md`.\n- Every h1–h4 needs a `{#kebab-id}`; run `pnpm lint:md:fix`.\n- If using `TopicLayout`: add `src/data/topics/<key>.ts`, wire `layoutMapping`/`componentsMapping` in `src/layouts/index.ts`, and add `src/intl/en/page-<key>.json`.\n\n**App Router page:**\n- Create `app/[locale]/<route>/page.tsx` composing `ContentLayout` (mirror `/learn/` or `/what-is-ethereum/`): pass `heroSection`, the hand-built `tocItems`, and `contributors`; wrap prose in `<Section id>`. Server Component unless it needs state/effects/handlers.\n- All user-facing strings via `getTranslations`; add keys to `src/intl/en/`.\n\n**Done when:** the page renders through its layout with content/strings in place and no inlined reinventions of existing primitives.\n\n## Step 5 — Verify (static checks)\n\n- `pnpm type-check` (catches invalid chain names and type errors).\n- Walk the design-system **Pre-Merge Smoke Test** checklist.\n- Add a `.stories.tsx` for any genuinely new UI primitive.\n\n**Done when:** type-check passes and the smoke checklist is clean.\n\n## Step 6 — QA via the adversarial review loop\n\nStatic checks pass on pages that are visibly broken — a wrong image path, a failed MDX compile, a section that silently didn't render. So the page must be run and audited via the **adversarial reviewer loop**, not an eyeball comparison.\n\n1. **Run it.** Start `pnpm dev`, wait for \"Ready\", load the page (locale-stripped for `en`: `/<route>/`). Confirm **HTTP 200** — a 500 is almost always an MDX or asset error; read the dev log (a non-co-located inline image is the classic one). There is nothing to review on a broken page.\n2. **Run the review loop.** Read `review-page/SKILL.md` and execute its Steps 1–5 against this route until green. You already hold the design source and route from Steps 1–2, so its Step 0 (running page + known design source) is already satisfied. Read and run that procedure; don't try to invoke `/review-page` as a skill from here.\n   - **A hero finding is a template bug, not a CSS gap.** When the design-system reviewer flags a title-only hero against a side-image design: on a markdown page that's a wrong-`template:` (use a Topic template, not `static`); on an App Router page, pass `heroImg` to `PageHero`. Never rebuild a hero with custom markup.\n3. **Hand off.** Leave the dev server running and give the user the local URL the server printed (e.g. `http://localhost:3000/<route>/`).\n\n**Done when:** the review loop is green (or hits its round cap with the remaining findings handed off), and the user has the live URL.\n",".claude/skills/design-system/SKILL.md":"---\nname: design-system\ndescription: Use when building, refactoring, or styling any UI in the ethereum.org Next.js site (`src/components/`, `app/`, `src/styles/`, `public/content/`, or any `.tsx`/`.mdx`/`.css` change that affects the rendered UI). Provides canonical component choices, design tokens, RTL/i18n rules, server/client guidance, and the \"use a variant, not a new component\" pattern for the project's Tailwind v4 + Radix + shadcn-style design system.\n---\n\n# ethereum.org Design System\n\nTailwind v4 (CSS-first config, no `tailwind.config.ts`) + React 19 / Next.js App Router + Radix UI primitives + shadcn-style component layer. Tokens live in CSS. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Habit: Reuse Over Reinvent\n\nThe single highest-leverage habit for keeping this codebase consistent: **when you need new UI, look for a primitive or variant first, only invent if nothing fits.** Most \"new component\" instincts are actually \"new variant\" instincts in disguise.\n\nBefore you write any UI code, ask:\n- Is there a primitive that already does this? (`Card`, `Button`, `Alert`, `Tag`, `Hero/*`)\n- Is the difference small enough to express as a *variant* on an existing primitive?\n- Can I compose existing primitives instead of inlining a long Tailwind class chain?\n\nIf you find yourself writing `flex items-center gap-X rounded-Y border bg-... p-Z` for a card-like thing, you're reinventing `<Card>`. If you write `<p className=\"text-4xl font-bold\">N</p>` for a stat, you're reinventing `<BigNumber>`. If you write `<div className=\"text-5xl font-bold\">Title</div>`, you're reinventing `<h1>` (which is already styled by `base.css`). **Compose, don't inline.**\n\nWhen the existing primitive doesn't quite fit, the answer is usually \"add a variant,\" not \"create a new file.\" See `references/variant-vs-new.md`.\n\n## Top Rules\n\n1. **No raw `<a>` or `<button>`.** Use `<Button>`/`<ButtonLink>` from `@/components/ui/buttons/Button` and `InlineLink`/`BaseLink`/`LinkWithArrow` from `@/components/ui/Link`. These primitives handle event tracking, external-link safety, locale routing, and focus rings.\n2. **No raw color values.** Use semantic tokens (`text-body`, `bg-background`, `border-border`, `text-primary`). Hex literals and `rgb()` calls bypass dark mode.\n3. **Prefer adding a variant** to an existing primitive over creating a new component. Card, Button, Alert, Tag are the most common targets.\n4. **Server Components by default.** Only `\"use client\"` when you need state, effects, browser APIs, or inline event handlers.\n5. **All text is translatable.** `getTranslations` from `next-intl/server` (server) or `useTranslations` from `next-intl` (client). One namespace-bound `t` per namespace -- bind a second function (e.g. `const tCommon = useTranslations(\"common\")`) to access another namespace. The legacy `@/hooks/useTranslation` wrapper is deprecated for new code. Never hard-code user-facing English. **In an `app/[locale]/` page or `generateMetadata`, call `setRequestLocale(locale)` before any next-intl API** or on-demand renders throw `static to dynamic ... reason: headers` -- see `references/i18n-rtl.md`.\n6. **Logical CSS for direction.** Use `ms-`/`me-`/`ps-`/`pe-`/`inset-s-`/`inset-e-`/`border-s`/`border-e`/`text-start`/`text-end`. The site supports Arabic and Urdu (RTL). Hard-coded `left-`/`right-`/`ml-`/`mr-`/`pl-`/`pr-` breaks RTL.\n7. **Locale-aware formatters.** `numberFormat()` from `@/lib/utils/numbers`, `dateTimeFormat()` from `@/lib/utils/date`. Never `toLocaleString` / `Intl.NumberFormat` directly.\n8. **`useRtlFlip()` for directional icons** (right-pointing arrows/chevrons). Or use `ChevronNext`/`ChevronPrev` from `@/components/Chevron`.\n9. **Markdown content goes through `MdComponents`.** The `<Card>` markdown shortcode is backed by `@/components/MarkdownCard` (a thin wrapper around the `ui/card` primitives with an MDX-friendly prop shape). For app code, compose the primitives directly from `@/components/ui/card`.\n10. **Storybook stories ship with new UI components.** No automated unit tests; Storybook + Chromatic + types are the verification layer.\n11. **Don't add new layouts.** There are six canonical layouts (`TopicLayout`, `StaticLayout`, `DocsLayout`, `TutorialLayout`, `ContentLayout`, `BaseLayout`). New sectioned content goes in `src/data/topics/<key>.ts` as a `TopicLayout` config -- not a new layout component. See `references/layouts.md`.\n\n## Highest-Value Gotchas (index)\n\nLandmines where the code looks reasonable but the pattern is wrong. One line each; the canonical write-ups live in the named reference.\n\n- **Look-alike imports** -- Tooltip (`@/components/Tooltip`, not `ui/tooltip`), Modal (`ui/dialog-modal` vs `ui/dialog`; don't mix sources within a feature), Card (`ui/card` for app code, not `MarkdownCard`), Heroes (named exports of `@/components/Hero`; `title` is always the `<h1>`, no `header` prop). Decision trees: `references/canonical-imports.md`.\n- **Stale shadcn token names** (`bg-popover`, `bg-muted`, `text-muted-foreground`, ...) linger in a few `ui/` files but don't resolve in this project's tokens. Don't add new uses. List: `references/tokens.md`; replacements: `references/cleanup-playbook.md`.\n- **`useColorModeValue` is a deprecated Chakra leftover** -- use `dark:` + semantic tokens.\n- **`Card` is variant-driven, not `className`-driven.** Padding/spacing/background/radius/text color go through `variant`/`size`; a `className` override means a missing variant case. Full system: `references/card-walkthrough.md`. Surprise behaviors (`isSecondary` no-op on `solid`, `CardBanner fit=\"contain\"` auto-backdrop, `LinkBox` needs `LinkOverlay`, `commonControlClasses` shared with `Switch`): `references/gotchas.md`.\n- **No `Heading` primitive** -- `base.css` styles `<h1>`-`<h6>`; just write the tag. To match a heading level's size anywhere, use `text-h1`-`text-h6` (size + line-height only) -- never the raw `text-3xl lg:text-4xl` pair, and never re-apply a weight on a real heading. Canonical write-up: `references/spacing-typography.md`.\n- **Spacing is `.flow` + the `page`/`space`/`hero` tokens**, not hand-rolled margins. App pages follow a `<main className=\"p-page\"> > <MainArticle className=\"flow\"> > <Section id>` skeleton. Details: `references/spacing-typography.md`; token table: `references/tokens.md`.\n- **Locale formatting** -- one stray `toLocaleString` remains in `ui/chart.tsx`; don't add more. Use `numberFormat()`.\n\n### Shadows: default to the Tailwind scale\n\nPick by surface: dropdowns/tooltips `shadow-md`, cards/popovers/modals `shadow-lg`, large framed boxes/sheets `shadow-xl`. Three rules:\n\n1. Only two custom shadows exist (`shadow-primary-xl`, `shadow-primary-no-blur-*`, both in `utilities.css`) -- anything new needs a brand-tint justification.\n2. Hover elevation is the `hover-lift-*` utility or `Card hoverLift` (both `motion-safe`-gated), never a per-component resting/hover shadow pair.\n3. A genuinely-needed custom shadow is a raw `box-shadow` `@utility`, never an arbitrary `shadow-[...]` -- arbitrary shadows route color through `--tw-shadow-color`, which the global `* { dark:shadow-body }` rule grays out in dark mode.\n\nOld-shadow-token mapping table: `references/cleanup-playbook.md`.\n\n## Quick \"Where Do I Import From?\" Cheatsheet\n\n| I need... | Import |\n|---|---|\n| Card | `import { Card, CardBanner, CardContent, CardTitle, CardParagraph } from \"@/components/ui/card\"` |\n| Modal/Dialog (typical) | `import Modal from \"@/components/ui/dialog-modal\"` (default export) |\n| Side sheet | `import { Sheet, ... } from \"@/components/ui/sheet\"` |\n| Tooltip | `import Tooltip from \"@/components/Tooltip\"` (NOT `@/components/ui/tooltip`) |\n| Button | `import { Button, ButtonLink } from \"@/components/ui/buttons/Button\"` |\n| Anchor (in prose) | `import InlineLink from \"@/components/ui/Link\"` (default) |\n| Anchor (CTA with arrow) | `import { LinkWithArrow } from \"@/components/ui/Link\"` |\n| Page hero | `import { PageHero, HubHero } from \"@/components/Hero\"` |\n| Inline alert | `import { Alert, AlertContent, AlertDescription } from \"@/components/ui/alert\"` |\n| Top-of-page banner | `import { Alert } from \"@/components/ui/alert\"` then `<Alert variant=\"banner\">` |\n| Big numeric display | `import BigNumber from \"@/components/BigNumber\"` |\n| Layout | `import { Stack, HStack, VStack, Flex, Center } from \"@/components/ui/flex\"` |\n| Number formatting | `import { numberFormat } from \"@/lib/utils/numbers\"` |\n| Date formatting | `import { dateTimeFormat } from \"@/lib/utils/date\"` |\n| RTL flip helper | `import { useRtlFlip } from \"@/hooks/useRtlFlip\"` |\n\nFor full decision trees with all the look-alike landmines, see `references/canonical-imports.md`.\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies. Don't read them all upfront.\n\n- **`references/canonical-imports.md`** -- unsure which of several look-alike imports is canonical.\n- **`references/components.md`** -- the full component inventory with variants and usage.\n- **`references/tokens.md`** -- adding a token, gradients, z-index, semantic-token choices, work in `src/styles/`.\n- **`references/spacing-typography.md`** -- page/section layout, heading sizes, spacing rhythm, the `.flow` system, citation and footnote markers.\n- **`references/gotchas.md`** -- unexpected primitive behavior; the long-tail confusion patterns.\n- **`references/variant-vs-new.md`** -- before creating any new component file.\n- **`references/cleanup-playbook.md`** -- refactoring existing anti-patterns; the \"old pattern -> new pattern\" map.\n- **`references/i18n-rtl.md`** -- user-facing text, number/date formatting, RTL, translation keys, `setRequestLocale`.\n- **`references/server-vs-client.md`** -- `\"use client\"` decisions and SSR-boundary structure.\n- **`references/a11y.md`** -- interactive elements, forms, images, heading hierarchy.\n- **`references/card-walkthrough.md`** -- any card-shaped UI work.\n- **`references/callout-walkthrough.md`** -- adding or modifying an in-content `Callout`.\n- **`references/page-hero-walkthrough.md`** -- a new page that needs a hero.\n- **`references/layouts.md`** -- layout selection, new topic hubs, one-off layout refactors.\n- **`references/new-component-checklist.md`** -- before opening a PR for a new component.\n\n## Other Project Skills That May Apply\n\n- **`data-layer`** -- For data fetching/sources. UI work that needs data should compose with this.\n\n## Pre-Merge Smoke Test\n\nBefore opening a PR for any UI work:\n\n- [ ] No raw `<a>` or `<button>`\n- [ ] No hard-coded colors (`#hex`, `rgb()`, `hsla()`); semantic tokens only\n- [ ] No `left-`/`right-`/`ml-`/`mr-`/`pl-`/`pr-` (use logical equivalents)\n- [ ] All user-facing strings translatable\n- [ ] `numberFormat()`/`dateTimeFormat()` for formatting (not native APIs)\n- [ ] Server Components wherever possible\n- [ ] New UI primitives have a `.stories.tsx`\n- [ ] Headings use `<h1>`-`<h6>` (not `<div className=\"text-5xl font-bold\">`)\n- [ ] If introducing a new component, justify why it isn't a variant of an existing one\n",".claude/skills/image-gen/SKILL.md":"---\nname: image-gen\ndescription: Generate illustration images (heroes, decorations, section art) for ethereum.org via the OpenRouter image API, then key/trim/check and place them in the repo. Trigger on \"make an image for\", \"generate a hero\", \"new illustration for <page>\", \"fix the glyph\", \"redraw this image\", \"chroma key this\", or edits to `src/scripts/image-gen/`. Not for screenshots, logos, or brand assets.\n---\n\n# Image generation\n\nThe user asks in chat (\"make a hero for /privacy\"); Claude drives the flow below. The script is Claude's tool for the parts that need the key or pixel math: it wraps OpenRouter's `POST /api/v1/images` and the post-processing. Three subcommands:\n\n```bash\npnpm tsx src/scripts/image-gen/main.ts models [--filter gemini]          # live params per model (aspect, resolution, background, n, ref limit)\npnpm tsx src/scripts/image-gen/main.ts generate --type cutout|full-bleed --prompt-file <f> [--ref <img>]... [--model <id>] [--aspect 16:9] [--quality <q>] [--eth-glyph auto|on|off] [--n 1] [--name <slug>] [--out-dir <dir>] [--dry-run]\npnpm tsx src/scripts/image-gen/main.ts process <img> --type cutout|full-bleed --out <file> [--key-color green|magenta] [--pad 0] [--max-size <px>]\n```\n\n## The key\n\n`OPENROUTER_API_KEY` lives in `.env.local`, loaded by the script itself. **Never read `.env.local`** (`.claude/settings.json` denies it; `.env.example` is fine to read). Never echo, log, or pass the key on a command line. Each person uses their own OpenRouter key with a spend limit set.\n\n**If a key ever appears in your context** (tool output, an error body, a file you read), tell the user right away that it was exposed and should be revoked and replaced at openrouter.ai/settings/keys, without repeating the key. Then carry on with the task; rotation happens separately.\n\n## Workflow\n\n1. **Context.** Read the target page and component, then decide the type. If the page doesn't settle it, ask with AskUserQuestion; never guess.\n   - **cutout** -- subject on transparency, nothing touching any edge (`PageHero` `heroImg`, section decorations).\n   - **full-bleed** -- scene fills the frame edge to edge (`HubHero` backgrounds, banners).\n2. **References.** Pick 2-6 existing images that match the target style (look at them first); see **House style** below. More refs pull harder toward house style; mismatched refs muddy it.\n3. **Prompt.** Write the subject/scene and style only. The script appends the cutout or full-bleed constraints (key color, margins, no borders) and, when the prompt mentions Ethereum, the ETH glyph spec and reference (see **The ETH glyph**), so don't restate them.\n4. **Confirm before spending.** Show the prompt, refs, model, aspect, and `--dry-run` output. Generate only after the user agrees, and get a fresh go-ahead for every further call; agreement to one generation isn't agreement to the next. A call costs about $0.11-0.26 and the price follows output pixels (21:9 is the cheapest, 1:1 and 3:2 the most).\n5. **Generate** one image per call: `--n 1`, never higher unless the user asks for a batch. Use a stable kebab `--name` describing the subject (see **Naming** below); reuse it for every iteration on that target, even if the subject drifts. Don't pass `--out-dir`: the default archive, `/tmp/image-gen/<name>/`, sits outside the repo and the session scratchpad, so candidates survive across iterations and sessions. Numbering continues (`-1`, `-2`, ...) and nothing is ever overwritten. Each candidate gets `<name>-<i>-raw.*`, a processed `<name>-<i>.png`, and `<name>-<i>.json` (prompt, refs, params, cost, check results).\n6. **Review.** Look at every output. `CHECK` lines are automatic failures (subject touching an edge, flat border strip, background not a key color); regenerate those rather than placing them. Then zoom in (crop at 3-4x) on every ETH glyph and every structure the user has flagged before, and check them against **The ETH glyph** and **Compositions that fail**. Report defects you find rather than letting the user discover them.\n7. **Place in situ, automatically.** Don't wait for a pick: put the best passing candidate at its target location right away so the user can judge it on the real page in their running dev server.\n   - `process` the archived raw (or `cp` the archived `.png`) to its final path, `public/images/<area>/<descriptive-name>.png`, sized to roughly 2x its largest rendered width with `--max-size`. Cutouts are trimmed to zero margin (`--pad 0`, the default); the page sets the spacing. Run a lossless PNG optimizer (oxipng) on the result.\n   - If the target also uses a portrait version on small viewports (a `*-portrait.png` beside it, e.g. some `HubHero` backgrounds), crop it from the landscape image rather than generating it separately, so both stay identical.\n   - Wire it into the target component (camelCase raster import, see the design-system skill's `page-hero-walkthrough.md`). If it replaces an existing image, repoint the import and leave the old file alone, so reverting is just `git checkout` on the component.\n   - Tell the user the URL to check (e.g. `/organizations/`) and list the other candidates by path.\n   - Never commit, and never start a dev server; the user runs their own.\n8. **Iterate in place.** Each new trial is copied from the archive over the same in-repo file, so the page updates without touching the import again. Static imports pick up the change through their content hash; for a plain `/images/...` string `src`, tell the user to hard-refresh. Revise by adding a candidate as a `--ref` with an updated prompt. To go back to an earlier candidate (\"I prefer the first one\"), copy that archived file in; never regenerate it. Always say which archive number is currently in situ.\n9. **Never delete the archive.** Leave `/tmp/image-gen/<name>/` alone, even after the image is placed. The user decides when it's done. When the user is happy, show `git status` so they can see exactly which files changed.\n\n## Editing an existing image\n\nMost work after the first generation is fixing details. Doing it well:\n\n- **Fix only what the user flagged.** Starting over or \"improving\" unflagged areas changes sizes, colors and objects the user had already accepted, and is usually worse.\n- **Redraw faithfully.** Pass the approved image as the sole (first) `--ref` and open with \"Redraw the reference image faithfully: identical camera, framing, <list every element>, same colors, same linework. Change only <X>: <exact description of the fix>. Everything else stays exactly as it is.\" OpenAI reproduces the rest closely.\n- **Take the whole redraw.** Patching just the fixed region back into the old image fails: a redraw shifts edges by around 10px. If everything else matches, place the full redraw.\n- **Check the neighbors.** Removing or changing one element can break an adjacent one (removing an elevator shaft truncated the next one). Compare the whole image against the previous version before placing.\n- **If the user supplies an edited version, it becomes the source.** Archive it in `/tmp/image-gen/<name>/`, place it, and use it as ref 1 for any later redraw. Never assume the user can edit images; for a detail the model keeps getting wrong, a manual edit can be suggested, not expected.\n\n## House style\n\nethereum.org illustrations are hand-drawn, airy and utopian. Good references: `public/images/home/hero.png` (plus a full-resolution crop of its palms and strokes, which shows the line texture better than the whole image), `public/images/heroes/developers-hub-hero.png`, `learn-hub-hero.png` and `guides-hub-hero.jpg`. Avoid the roadmap, garden and quizzes heroes as references; they lean purple and pink.\n\nCues that work in prompts:\n\n- Lines: hand-drawn in cobalt and royal blue, never black, lighter periwinkle for distant details. Gentle crayon quality: \"lines waver a little in weight and have slightly soft, lightly textured edges, fills carry a faint paper grain; subtle and clean, mostly crisp, not scratchy\". Full crayon wording overshoots into grain the user doesn't want (aim for about 75% clean, 25% texture).\n- Color: mostly white and pale blue with peach, orange and cyan accents; red awnings are fine. Flat fills, no gradients, glow or halos.\n- Architecture: sci-fi, white and rounded, slender columns, curved balconies, glass as pale blue panels with thin white grid lines, thin peach accent bands, rooftop plants. Classical forms (columns, pediments, domes) stay but in that white style. No brick or boxy present-day buildings.\n- Ground: white or pale-blue tiles; floating slab edges in pale periwinkle, not brown earth.\n- Plants: palms with feathery fronds of thin cobalt and periwinkle strokes, in white planters. No green, no round deciduous trees.\n- People: simple outlined figures, flat-colored clothes, simple hair, low detail.\n- Always add \"No text, letters, numbers or currency symbols anywhere, no logos other than the Ethereum diamond, no UI labels.\" Clock faces get no numbers.\n\n## The ETH glyph\n\nImage models get the Ethereum diamond wrong more than anything else: one solid rhombus, a white fill or a line bridging the gap, an extra lattice of facets, a glyph rotated off the surface it sits on, or a smudge on a small coin. The **shape** must match `src/components/icons/eth-home-icon.svg` exactly. Its **colors don't matter**: they can change to suit the design (a coin imprint in coin tones, a white glyph on a pediment).\n\nThe shape, for checking outputs and describing fixes (the script's reference renders it as unfilled outlines for this reason):\n\n- Two separate pieces stacked vertically, with an open gap between them where the background shows through.\n- Upper piece: a tall kite with exactly four facets. Its side corners sit about halfway down the whole glyph and its bottom point about two-thirds down. A center line runs top to bottom, and a line from each side corner rises to meet it about a third of the way down.\n- Lower piece: a downward chevron with exactly two facets. Its top edge is a shallow V parallel to the upper piece's bottom edges, and its tips sit just below the upper piece's corners.\n- About 1.6 times as tall as wide, symmetric, the same width top and bottom. On a tilted or foreshortened surface it follows that surface's perspective and keeps the same structure.\n\nHow the script helps: with `--eth-glyph auto` (the default), any prompt that mentions Ethereum, ETH, a glyph or a diamond gets the detailed shape spec (`src/scripts/image-gen/eth-glyph.ts`) appended and a rendering of the canonical glyph attached as the **last** reference image. User refs keep their order, so \"the first reference image\" still means yours; never call the glyph anything but \"the last reference image\" in your own prompt. Use `--eth-glyph on` to force it, `off` when the image has no glyph and the prompt only names Ethereum in passing, or when the reference slot is needed.\n\nIn prompts, also:\n\n- Ask for few glyphs, each drawn large. A glyph below about 15px in the output, or lying on foreshortened paper, is unreadable at any prompt fidelity; leave it off small coins and badges.\n- Say where each glyph sits and which way it faces (\"upright and centered at the top of the certificate\", \"lying flat in the center of the floor square, aligned with its edges\").\n- Don't ask for glows, highlights or \"dimensional\" facets on it; they turn into extra lines inside the shape.\n- To fix a wrong glyph on an otherwise approved image, redraw faithfully (see **Editing an existing image**) with \"Change only the Ethereum diamond on <where>\" and its position, size and facing; the spec and reference supply the shape.\n\n## Compositions that fail\n\nThese cost several failed generations each. Design them out in the prompt instead of fighting them:\n\n- **Merging or branching rail tracks:** the model can't draw a clean switch, and \"hide it under a train\" produced two misaligned trains. Use a single continuous track.\n- **Hard occlusion:** legs under tables, columns behind foreground objects, a bridge running behind a floating object. Use a solid-front desk or counter that reaches the floor, a single colonnade, fewer overlapping layers.\n- **Repeated structure:** column rows, colonnades and dome drums come out unevenly spaced. Spell it out (\"exactly four identical columns at exactly equal spacing on the same base line\") and check spacing at zoom.\n- **Impossible layering:** floors that don't continue behind objects, staircases that loop. Keep depth simple: one island, one level change.\n\n## Naming\n\nName images for what they depict, never for where they're used: a town square with people used as the `/some-page/` hero is `town-square-buildings-with-people.png`, not `some-page-hero.png`. Placement changes; content doesn't. Kebab-case, specific enough to tell it apart from neighbors in the same folder. The `--name` passed to `generate` follows the same rule, so the archive folder and the final file match. If the final image ends up depicting something different from the original name, rename the in-repo file to match what's in the image (the archive folder keeps its name).\n\n## Models\n\nDefault `openai/gpt-image-2.5-sunburst` at the highest `quality` it offers. It matches the house style (clothing, palms, architecture, ground, people, flat color) where `google/gemini-3-pro-image` (Nano Banana Pro) kept drawing black vector lines, purple and red casts and ordinary buildings despite explicit prompts. Switch with `--model` only when the user asks; run `models` to see what a model accepts -- the script validates flags against that list and drops options a model lacks.\n\n- **Cutouts:** OpenAI supports `background: transparent`, so the script requests real alpha and skips keying. Models without it get a flat key background that the script removes.\n- **Size:** OpenRouter exposes no size parameter; output tops out around 1536px on the long side (21:9 comes back 1536x656). For a wide hero that needs more, a local Real-ESRGAN upscale reads better on the page than the raw 1536px, though it flattens some crayon texture.\n- `n` is 1 per call on Gemini; `--n 3` makes three calls. Models that accept a higher `n` batch it.\n\n## Chroma keying\n\nOnly for models without native alpha, or for chroma images made elsewhere.\n\n- Green (`#00FF00`) by default. Use `--key-color magenta` when the subject itself is green or teal; a green subject keyed on green turns partly transparent.\n- Alpha comes from how strongly a pixel leans toward the sampled border color, then the background is unmixed from partially transparent pixels. Despill applies only to a 2px rim near transparency, so interior colors are untouched.\n- Prefer PNG sources. JPEG compression smears the key into dark outlines, which leaves a faint tinted rim.\n- `process` works on any existing chroma image, e.g. one generated by hand on the OpenRouter site.\n",".claude/skills/intl-pipeline/SKILL.md":"---\nname: intl-pipeline\ndescription: Use when working on the translation pipeline (`src/scripts/intl-pipeline/`), the post-import sanitizer (`intl-sanitizer.ts`), the `intl/pending-*` branches, the `intl-pipeline.yml` GitHub Actions workflow, ETHGlossary integration, or any translation-related issue (broken translations, missing locales, manifest drift, sanitizer false positives, ETHGlossary term lookups, language-group transliteration questions). Provides the manifest-driven incremental pipeline mental model, the \"don't hand-propagate\" rule, recovery procedures, the ETHGlossary-as-canonical-source policy, and the `intl/pending-{base}` orchestration contract.\n---\n\n# intl-pipeline\n\nLLM-based, manifest-driven, incremental translation pipeline for ethereum.org. Translates English content (markdown in `public/content/`, JSON UI strings in `src/intl/en/`) into 24 target languages via Gemini — reached either directly or through OpenRouter (`LLM_PROVIDER`), whose keys carry a server-side spend limit — with a post-import sanitizer normalizing common artifacts. **The workflow is manual-dispatch only**; the daily cron was removed after a run billed $1,108 unattended (`references/runbooks/cost-guard-tripped.md`). ETHGlossary is the authoritative source for all Ethereum-ecosystem term translations consumed by the pipeline. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Rule: Don't Hand-Propagate English Changes\n\nThe single highest-leverage habit for keeping translations correct: **never hand-edit translated content to reflect an English change.** The pipeline tracks state via manifests (`.manifests/{destPath}/source.json` + `translation.json` per file+locale); hand-edits that follow an English change desynchronize the manifest from reality, and the next pipeline run either re-translates over your edit or produces merge conflicts.\n\nThe rule is NOT \"never edit locales.\" It IS \"don't hand-propagate English updates\":\n\n- **Allowed:** Fixing a translation error when the English side hasn't moved (review-time corrections, sanitizer follow-ups). The manifest's English-to-locale map stays valid.\n- **Not allowed:** Editing a locale to reflect a new English value (URL change, attribute change, restructured paragraph). The manifest map becomes wrong.\n- **If English-to-locale sync is genuinely urgent** (build-breaking structural change with no pending PR open): make the English edit, then trigger `intl-pipeline.yml` with `stamp_only: true` to refresh manifests without translation. Safe only when no `intl/pending-{base}` branch exists for that base.\n\n**Deletions go through English too.** To remove a string — including an orphaned key code no longer references — delete it from the English source only; the pipeline propagates the removal on its next run. Hand-deleting the locale copies is hand-propagation.\n\n## Top Rules\n\n1. **ETHGlossary is canonical for term translations.** Brand names, people's names, programming languages, OS/platform names, concept terms — all live in ETHGlossary. Don't maintain parallel term banks. The pipeline queries the API via `GLOSSARY_API_URL` (default in `src/scripts/intl-pipeline/config.ts`).\n2. **One translation PR at a time per base branch.** The pipeline commits to `intl/pending-{base}` (e.g., `intl/pending-dev`). Subsequent runs merge `{base}` into pending first, then translate the delta. Parallel translation PRs against the same base will conflict.\n3. **The pipeline only targets `dev` in production.** Hot fixes to `staging` / `master` go out English-only and catch up via prepare-release. Don't translate against `staging` / `master` unless you have a specific reason and use a custom `target_branch`.\n4. **Manifests are inseparable from their locale file.** Each translated file has two manifests, centralized under `.manifests/{destPath}/`: `source.json` and `translation.json`. Delete one, you must regenerate both — easiest via the pipeline in `full` mode for that file+locale. Never hand-edit a manifest.\n5. **The sanitizer runs post-translation, not pre.** Its job is to fix Gemini-introduced artifacts (BiDi mistakes, code-fence drift, brand-name mistranslations). It receives translation outputs, never English source.\n6. **Don't add transliteration data here.** All term/brand/person transliteration policy lives in ETHGlossary's `docs/translation-policy.md` and per-language entries. The intl-pipeline consumes; it does not author.\n7. **Sanitizer fixes must split on code blocks first.** Every text transformation in `intl-sanitizer.ts` MUST start with the code-block split pattern. Modifying code-fence contents breaks Solidity / Python / TypeScript examples in tutorials.\n8. **An untagged code fence is code.** Fence bodies never reach the model; only a prose-tagged fence (```` ```text ````, `txt`, `md`, `markdown`, `mdx` -- `PROSE_FENCE_TAGS` in `lib/shared-patterns.ts`) is translatable. To make a fence's prose translatable, tag it ```` ```text ```` in the English source. `code-fence-content` fails any other fence whose body drifts from English, comments excepted.\n9. **Pipeline failures are not always pipeline bugs.** A \"translation looks wrong\" report may be: bad Gemini output (file upstream), missing ETHGlossary term (add there), correct per language-group policy (read translation-policy.md), or an actual pipeline bug. Triage before patching.\n\n## Highest-Value Gotchas\n\nThese are landmines where the obvious-looking action is wrong. The full set is in `references/gotchas.md`; these come up most often.\n\n### Naming legacy\n\nOld names are dead — `src/scripts/i18n/`, `gemini-translations.yml`, `main-incremental.ts`, \"i18n pipeline\"; the canonical name everywhere is `intl-pipeline`. Update old names on sight.\n\n### Manifests are centralized under `.manifests/`, not next to the locale file\n\nTwo manifests per file+locale at `.manifests/{destPath}/source.json` + `translation.json` (e.g. `.manifests/src/intl/ja/common.json/source.json`). Structure, lifecycle, and debugging: `references/manifests.md`.\n\n### Spend bounds are input-side, and the tell is input tokens per call\n\nHealthy runs average 4k–9k input tokens per call; the input:output ratio is NOT a signal. Before merging anything that touches batching, prompt assembly, or context selection, run `pnpm intl:estimate` and compare per-request input tokens against that range. Bounds, triage, and the incident that motivated them: `references/runbooks/cost-guard-tripped.md`.\n\n### `intl/pending-{base}` branch lifecycle\n\nThe pipeline creates `intl/pending-{base}` on first run, commits translations to it, opens a PR against `{base}`. Subsequent runs MERGE `{base}` into pending first, then translate the delta. After the pending PR is merged, the branch is deleted; the next run creates a fresh one.\n\nDo NOT rebase, squash, or force-push `intl/pending-{base}`. The pipeline depends on its history.\n\n### SOV-language inline element reordering\n\nFor Korean, Urdu, and other SOV (subject-object-verb) languages, inline elements (links, inline code, JSX components) often appear in REVERSE order vs. English. The pipeline's inert-propagation pass matches elements by **value**, not by position. Any logic that assumes positional order will silently corrupt those languages.\n\n### JSX attribute translation is a separate pass\n\nJSX attribute values are NOT translated in the main Phase 4 LLM call. Phase 4b is a dedicated pass with an allow-list of translatable attribute names (`title`, `description`, `alt`, `label`, `aria-label`, `placeholder`, etc.) defined in `src/scripts/intl-pipeline/lib/shared-patterns.ts`. Touching attribute translation means touching that pass, not Phase 4.\n\n### The sanitizer runs inside each task, before the gates\n\nSince the gates landed, `runSanitizer` is called per task on that task's own output (`sanitizeAndGate` in `main.ts`), then the gates judge the sanitized bytes, then content and manifest are recorded together. There is no post-squash sanitize commit any more; a sanitizer change is exercised on the next run's tasks, not on a branch sweep.\n\n### Sanitizer test scope is per-file, never per-language sweep\n\nNEVER run the sanitizer against an entire language. It processes thousands of files and hangs for 30+ minutes. There is no per-file env var — the only env-based scoping is per-language via `TARGET_LANGUAGES` (e.g. `TARGET_LANGUAGES=ja`), which still sweeps that whole language. Scope to specific files programmatically by calling the exported `runSanitizer(filesWithContent)` from `intl-sanitizer.ts` with just the affected file(s). The slash command `/fix-sanitizer-bug` enforces this; if you script around it, preserve the constraint.\n\n## Quick \"Where Do I Look?\" Cheatsheet\n\n| I need...                               | Path                                                                                  |\n| --------------------------------------- | ------------------------------------------------------------------------------------- |\n| Pipeline entry                          | `src/scripts/intl-pipeline/main.ts`                                                   |\n| Sanitizer                               | `src/scripts/intl-pipeline/intl-sanitizer.ts`                                         |\n| Gemini adapter                          | `src/scripts/intl-pipeline/lib/llm/gemini.ts`                                         |\n| OpenRouter transport                    | `src/scripts/intl-pipeline/lib/llm/openrouter.ts`                                     |\n| Adapter registry / provider selection   | `src/scripts/intl-pipeline/lib/llm/adapters.ts`, `constants.ts`                       |\n| Spend bounds (budget + run fuse)        | `src/scripts/intl-pipeline/lib/llm/cost-meter.ts`                                     |\n| Pre-commit gates (structure, MDX, JSON) | `src/scripts/intl-pipeline/lib/gates.ts`                                              |\n| Deterministic-failure quarantine        | `src/scripts/intl-pipeline/lib/quarantine.ts`, `.manifests/quarantine.json`           |\n| Work planner (shared with estimate)     | `src/scripts/intl-pipeline/lib/llm/plan.ts`                                           |\n| Cost estimate, no LLM calls             | `pnpm intl:estimate` (`src/scripts/intl-pipeline/estimate.ts`)                        |\n| Structural gate, standalone CLI         | `src/scripts/intl-pipeline/verify-structure.ts` (checks in `lib/verify-structure.ts`) |\n| One-off repairs (never run by a run)    | `migrate-manifests.ts`, `repair-anchors.ts` -- see \"One-off repair scripts\"           |\n| Prompt builder                          | `src/scripts/intl-pipeline/lib/llm/prompt-builder.ts`                                 |\n| Content normalizer                      | `src/scripts/intl-pipeline/lib/llm/content-normalizer.ts`                             |\n| Shared patterns (JSX attrs allow-list)  | `src/scripts/intl-pipeline/lib/shared-patterns.ts`                                    |\n| Glossary config                         | `src/scripts/intl-pipeline/config.ts`                                                 |\n| Workflow file                           | `.github/workflows/intl-pipeline.yml`                                                 |\n| Per-file pipeline spec (canonical)      | `tests/specs/PIPELINE-SPEC.md`                                                        |\n| Concurrency / chunking spec             | `tests/specs/CONCURRENCY-SPEC.md`                                                     |\n| Test fixture mutation table             | `tests/specs/SPEC.md`                                                                 |\n| Sanitizer test suite                    | `tests/unit/intl-pipeline/sanitizer/`                                                 |\n| Pipeline test suite                     | `tests/unit/intl-pipeline/`                                                           |\n| Future-work backlog                     | `src/scripts/intl-pipeline/FUTURE.md`                                                 |\n| Language config (canonical list)        | `i18n.config.json`                                                                    |\n| ETHGlossary repo                        | https://github.com/ethereum/ethglossary                                               |\n| ETHGlossary API root                    | https://glossary.ethereum.org                                                         |\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies. Don't read them all upfront.\n\n- **`references/architecture.md`** — debugging pipeline behavior; the phase-by-phase walkthrough.\n- **`references/manifests.md`** — manifests misbehave, or changing what the pipeline tracks.\n- **`references/orchestration.md`** — working with `intl/pending-{base}` branches.\n- **`references/recovery.md`** — translations broken, triaging \"the pipeline did something wrong.\"\n- **`references/sanitizer.md`** — sanitizer behavior, false positives/negatives, fix-function catalog.\n- **`references/runbooks/fix-sanitizer-bug.md`** — confirmed sanitizer bug; when (not) to run `/fix-sanitizer-bug`.\n- **`references/runbooks/cost-guard-tripped.md`** — `[cost-guard]` aborts, budget skips, cost-bound design, provider selection.\n- **`references/ethglossary.md`** — term lookups, `script_rule`/`term_role` semantics, missing terms.\n- **`references/non-english-edits.md`** — about to hand-edit a translated file.\n- **`references/gotchas.md`** — something feels off and it isn't inline above.\n\n## Other Project Skills That May Apply\n\n- **`intl-review`** — For translation quality review (scoring rubric, language-group rules, brand-name policies, per-language findings). The review side of the same coin; the pipeline produces, intl-review evaluates.\n- **`data-layer`** — For data fetching, if pipeline changes touch externally-sourced content.\n\n## Pre-Merge Smoke Test\n\nBefore opening a PR that touches the pipeline:\n\n- [ ] Sanitizer test suite passes (`npx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/`)\n- [ ] Pipeline test suite passes (`npx playwright test --project=unit tests/unit/intl-pipeline/`)\n- [ ] No hand-edits to `public/content/translations/` or `src/intl/{non-en}/` files\n- [ ] No manual changes to manifest files under `.manifests/`\n- [ ] If renaming workflow / config paths, all doc references updated (search for old names)\n- [ ] If adding pipeline-affecting code, `src/scripts/intl-pipeline/FUTURE.md` updated (or item removed if completed)\n- [ ] If new sanitizer fix function, the code-block-split pattern is the first operation inside it\n- [ ] If touching term policy or transliteration, change goes in ETHGlossary first — never duplicate term data here\n- [ ] If touching the orchestration model (pending branch, temp branch, stamp_only), `references/orchestration.md` updated alongside\n- [ ] If touching batching, prompt assembly, or context selection: `pnpm intl:estimate` run and per-request input tokens sane (4k–9k), plus `tests/unit/intl-pipeline/cost-incident.spec.ts` passing\n- [ ] No new LLM call site that bypasses `callGeminiRaw` — it is the single choke point where the per-call ceiling and run fuse are enforced\n",".claude/skills/data-layer/SKILL.md":"---\nname: data-layer\ndescription: This skill provides patterns for working with the data-layer module. Use when creating/editing files in src/data-layer/, src/lib/data/, or adding new data sources.\n---\n\n# Data Layer\n\n## Architecture\n\n```\nsrc/data-layer/\n├── fetchers/         # Fetch functions (one per data source)\n│   └── developer-tools/  # Multi-file fetcher (builder resources, GitHub/npm stats, ranking)\n├── index.ts          # Public API - typed getter functions\n├── tasks.ts          # Trigger.dev scheduled tasks\n├── storage.ts        # KEYS constant + get/set (Netlify Blobs or mock files)\n├── s3.ts             # S3 image upload utility for external images\n├── docs.md           # Module documentation\n├── mocks/            # Mock data files for local development\n└── .env.example      # Environment variables for data-layer/Trigger.dev\n\nsrc/lib/data/\n└── index.ts          # Next.js caching adapter (createCachedGetter)\n```\n\n## Environment Variables\n\nThe data-layer uses a **dedicated `.env.local`** at `src/data-layer/.env.local`, separate from the root `.env.local`: `cp src/data-layer/.env.example src/data-layer/.env.local`, fill in the keys (see `.env.example` for all options), then `pnpm trigger:dev` runs tasks locally. `GITHUB_TOKEN_READ_ONLY` and Sentry vars are shared with the main app (configure in both files); everything else (API keys, Netlify Blobs tokens, S3 credentials, Trigger.dev config) is data-layer only. In production, configure vars in the Trigger.dev project dashboard — the app and data-layer run in separate environments.\n\n## Key Files\n\n`storage.ts` defines the `KEYS` constant, `tasks.ts` the `WEEKLY`/`DAILY`/`HOURLY` task tuples; `index.ts` holds the one-liner getters — snippets for both under \"Adding a New Data Source\" below.\n\n### storage.ts - Storage Abstraction\n\n`get<T>(key)` / `set(key, data)` switch between Netlify Blobs (prod) and local mock JSON files (`USE_MOCK_DATA=true` for local development).\n\n### s3.ts - Image Upload Utility\n\nCentralized S3 upload for external images. Fetchers use this to upload external images to a single S3 bucket, reducing Next.js `remotePatterns` complexity.\n\n```typescript\n// Upload single image\nconst s3Url = await uploadToS3(sourceUrl, \"events/logos\")\n\n// Batch upload (parallel)\nconst s3Urls = await uploadManyToS3(urls, \"apps/banners\")\n```\n\nKey features:\n\n- **SSRF protection** - Blocks private/internal network addresses\n- **Deduplication** - SHA256 hash of source URL as key\n- **Existence check** - Skips if already uploaded\n- **5MB size limit** - Returns `null` for large images\n- **Content-Type detection** - From header or URL extension fallback\n\n## Rules\n\n### 1. Getters must be pure passthrough\n\nNo transformations in `index.ts` - just `get<T>(KEYS.X)`:\n\n```typescript\n// Correct\nexport const getEventsData = () => get<EventItem[]>(KEYS.EVENTS)\n\n// Wrong - no transformations in getters\nexport const getEventsData = () => {\n  const data = await get<EventItem[]>(KEYS.EVENTS)\n  return data?.map(transform) ?? null\n}\n```\n\nAll transformations belong in the fetcher (`src/data-layer/fetchers/`).\n\n### 2. KEYS is the single source of truth\n\nAll task IDs are defined in `KEYS` in `storage.ts`. The getter in `index.ts` and the task tuple in `WEEKLY`/`DAILY`/`HOURLY` must use the same key.\n\n### 3. Expose via lib/data for caching\n\nAdd cached wrapper in `src/lib/data/index.ts`:\n\n```typescript\nexport const getEventsData = createCachedGetter(\n  dataLayer.getEventsData,\n  [\"events-data\"],\n  CACHE_REVALIDATE_DAY // or CACHE_REVALIDATE_HOUR\n)\n```\n\nThe `revalidate` parameter is `number | false`. Passing `false` is a deliberate pattern to keep a route fully static — a finite revalidate opts the page into ISR, which fails on Netlify for pages reading `public/content/` files. Example: `getStaticAppsData` in `src/lib/data/index.ts`, used by components embedded in MDX pages (data refreshes only on deploy).\n\n### 4. Use S3 for external images\n\nExternal images should be uploaded to S3 in the fetcher to centralize image domains:\n\n```typescript\n// In fetcher - correct\nimport { uploadToS3 } from \"../s3\"\n\nconst logoUrl = await uploadToS3(event.logoImage, \"events/logos\")\nreturn { ...event, logoImage: logoUrl ?? \"\" }\n```\n\nAlways handle `null` returns (upload failures) with fallback/empty string.\n\n### 5. Keep fetchers isolated from the app\n\nFetchers run on Trigger.dev — a separate runtime, deployment, and bundle from the Next.js app. They cannot assume the app's filesystem, environment, or modules are available.\n\nAny import or runtime dependency reaching outside `src/data-layer/` is a warning sign. Allowed: types (`@/lib/types`, `@/lib/interfaces`), pure constants (`@/lib/constants`), and pure utility functions with no app-runtime dependencies. Not allowed: anything that reads `process.cwd()`, anything from `app/` or `public/`, anything from `src/components/`, or `src/lib/data/` (which wraps the data layer and would create a cycle).\n\nIf a fetcher needs data that lives in the app — content files, frontmatter, etc. — fetch it over the network via the GitHub API and treat the repo as an external system. See `fetchGitHubContributors.ts` for the pattern. Don't work around this with `additionalFiles` in `trigger.config.ts`; bundling app files into the data-layer deployment re-creates the coupling.\n\n## Adding a New Data Source\n\n1. **Create fetcher** in `src/data-layer/fetchers/fetchNewData.ts`:\n\n   ```typescript\n   export async function fetchNewData(): Promise<YourDataType> {\n     // Fetch and transform data here\n   }\n   ```\n\n2. **Add key** to `KEYS` in `src/data-layer/storage.ts`:\n\n   ```typescript\n   export const KEYS = {\n     // ...existing keys\n     NEW_DATA: \"fetch-new-data\",\n   } as const\n   ```\n\n3. **Add task tuple** to `WEEKLY`, `DAILY`, or `HOURLY` in `tasks.ts` (getter and tuple must use the same `KEYS` entry — Rule 2):\n\n   ```typescript\n   const DAILY: TaskDef[] = [\n     // ...existing tasks\n     [KEYS.NEW_DATA, fetchNewData],\n   ]\n   ```\n\n4. **Add getter** in `src/data-layer/index.ts`:\n\n   ```typescript\n   export const getNewData = () => get<YourDataType>(KEYS.NEW_DATA)\n   ```\n\n5. **Add mock file** at `src/data-layer/mocks/fetch-new-data.json` (read when `USE_MOCK_DATA=true`)\n\n6. **Add cached wrapper** in `src/lib/data/index.ts`:\n   ```typescript\n   export const getNewData = createCachedGetter(\n     dataLayer.getNewData,\n     [\"new-data\"],\n     CACHE_REVALIDATE_HOUR\n   )\n   ```\n",".claude/skills/intl-review/SKILL.md":"---\nname: intl-review\ndescription: Use when reviewing translation imports/PRs (especially against `intl/pending-*` branches), evaluating translation quality across any of the 24 target languages, triaging issues in `.claude/translation-review/`, scoring a locale's translation, running the `/review-translations` slash command, debugging brand-name mistranslations or glossary deviations, or working with the per-language findings tracking files. Covers the scoring rubric, ETHGlossary-as-authority policy, language-group rules, agent-role split (structural / terminology / semantic), the concept-tag-vs-brand-tag distinction in frontmatter, and the critical-vs-warning severity policy.\n---\n\n# intl-review\n\nTranslation-quality review for ethereum.org's 24-language pipeline output. Reviews target the LLM (Gemini)-produced translations on `intl/pending-{base}` branches plus any historical Crowdin imports still in flight. ETHGlossary (https://glossary.ethereum.org) is the authoritative source for terminology — deviations are **critical issues**, not warnings. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Rule: ETHGlossary Is Authority\n\nWhen evaluating a translated brand name, person name, programming language, OS name, or any Ethereum-ecosystem term, **the ETHGlossary entry for that term in that language is the truth**. Deviations are **critical** issues that must be flagged (and auto-fixed when running `/review-translations` without `--no-fix`).\n\nThis is not a stylistic preference — it's a determinism guarantee. The translation pipeline queries ETHGlossary; reviewers verify the output matches. If you think the glossary is wrong, the fix is to update ETHGlossary (https://github.com/ethereum/ethglossary), not to leave the translation as-is.\n\nUse the `/filter` endpoint to get the subset of glossary terms that actually appear in a given English source file. Don't reason from memory about brand names; look them up.\n\n## Top Rules\n\n1. **Glossary deviations are CRITICAL, not warnings.** Any time a translated term differs from the ETHGlossary entry for that language, that's a critical issue. The review report must list them so Phase 5 can auto-fix.\n2. **Internal hrefs must stay English.** `/governance` in Spanish content must be `/governance`, not `/gobernanza`. Translated URLs break navigation. Same for anchor IDs (`#section-id`).\n3. **Concept tags translate; brand-name tags don't.** Tutorial frontmatter `tags:` arrays contain a mix. Brand names (`\"solidity\"`, `\"hardhat\"`, `\"alchemy\"`) stay English. Concept/category tags (`\"smart contracts\"`, `\"testing\"`, `\"security\"`) are intentionally translated by the pipeline and must NOT be reverted to English.\n4. **Code blocks: functional code stays English; comments may be translated.** Identifiers, strings, config keys, console output — English. Code comments (`//`, `/* */`, `#`) may be translated to aid reader comprehension.\n5. **Brand-name policy is per-language, per-term.** Some brands stay Latin in CJK (Solidity, Hardhat per ETHGlossary's `script_rule: always_latin`); some transliterate (Ethereum → イーサリアム per `transliterate`); some have native calques in `zh`/`zh-tw`. Use the glossary; don't pattern-match.\n6. **MDX syntax errors are CRITICAL (build-breaking).** Raw `<` before numbers, unclosed backticks, orphaned closing tags, JSX attributes with embedded unescaped quotes — any of these breaks the build for that locale. Flag and fix.\n7. **Reporting zero critical issues is a valid outcome.** Don't invent issues to \"show your work.\" If a thorough review surfaces no criticals, report `0 critical, N warnings` (or `0/0`) and that's a valid result.\n8. **Auto-fix is opt-out, not opt-in.** `/review-translations` applies fixes by default. Use `--no-fix` for review-only runs (GitHub Actions context, no commit auth). The review report still lists what would have been fixed.\n9. **All review changes go on the `intl/pending-*` branch being reviewed — NEVER `dev`.** Translation fixes AND knowledge-base updates (`known-patterns.md`, `per-language/{lang}.md`) are committed to that review's own `intl/pending-{base}` branch (the `/review-translations` worktree) so they ride with the PR. If you catch yourself editing `.claude/translation-review/` in the `dev` checkout, move the changes to the review branch and restore `dev`. Sequence: commit → push → submit review (`--approve` once the fix is pushed and no criticals remain, else `--comment`).\n\n## Highest-Value Gotchas\n\nThese are landmines where the obvious-looking call is wrong. The full set is in `references/gotchas.md`.\n\n### Frontmatter `tags` arrays mix two policies\n\nTutorial frontmatter `tags:` mix brand-name tags (stay English) with concept/category tags (intentionally translated — do NOT revert). Rule of thumb: proper noun / product name → English; generic descriptive term → translated form is right. Details and examples: `references/known-patterns.md`.\n\n### Semantic inversions on consensus terms\n\nHigh-frequency real-world failure mode: Gemini occasionally swaps proof-of-stake and proof-of-work, or mistranslates \"mainnet\" as \"market\" or \"main network.\" These are critical semantic errors, not warnings. Spot-check consensus, mainnet, testnet, validator, miner, client when reviewing.\n\n### Cross-script contamination\n\nOld Crowdin imports sometimes have characters from one script leaking into another (Devanagari in Turkish, CJK in Arabic). Less common with the new pipeline but still possible. Flag any unexpected-script characters; they indicate translation memory leaks.\n\n### Lorem ipsum / placeholder text in JSON\n\nReal translation values sometimes contain \"Lorem ipsum dolor sit amet\" or similar placeholder text. Treat as a critical user-visible issue.\n\n### Translated GitHub `@username` handles\n\n`@axic` becoming `@kwaaxic` (Swahili) or similar — broken attribution. Flag.\n\n### Asymmetric backticks and orphaned HTML tags\n\nSingle-open / double-close backticks (`` `text`` ``), missing `</em>`before`</li>`, raw `</a>` without opener — all break MDX compilation. Critical.\n\n## Quick \"Where Do I Look?\" Cheatsheet\n\n| I need...                              | Path                                                                                                               |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |\n| Slash command (full pipeline)          | `/review-translations` (`.claude/commands/review-translations.md`)                                                 |\n| Issue-pattern catalog                  | `.claude/translation-review/known-patterns.md` + `references/known-patterns.md`                                    |\n| Per-language rules                     | `references/language-rules.md` (sourced from `.claude/translation-review/localization-rules-by-language-group.md`) |\n| Scoring rubric                         | `references/scoring-rubric.md`                                                                                     |\n| Per-language findings                  | `.claude/translation-review/per-language/{lang}.md`                                                                |\n| ETHGlossary terms (filtered to source) | `POST /api/v1/filter`                                                                                              |\n| ETHGlossary terms (full per-language)  | `GET /api/v1/translations/{lang}`                                                                                  |\n| ETHGlossary policy                     | https://github.com/ethereum/ethglossary/blob/main/docs/translation-policy.md                                       |\n| Sanitizer source                       | `src/scripts/intl-pipeline/intl-sanitizer.ts`                                                                      |\n| Sanitizer test research                | `docs/solutions/integration-issues/sanitizer-test-research.md`                                                     |\n| Past review post-mortems               | `docs/solutions/integration-issues/` + `docs/solutions/logic-errors/`                                              |\n| Language config                        | `i18n.config.json`                                                                                                 |\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies.\n\n- **`references/known-patterns.md`** — Load when starting any review. The catalog of recurring issue patterns (brand mistranslations, MDX syntax errors, semantic inversions, cross-script contamination, etc.).\n- **`references/language-rules.md`** — Load when working on a specific non-Latin-script language (ar, bn, hi, ja, ko, mr, ru, ta, te, uk, ur, zh, zh-tw). Per-language-group rules for prose, UI tags, numerals, code, people's names.\n- **`references/scoring-rubric.md`** — Load when scoring a PR or producing a quality report. The 5-category rubric (Brand Name Preservation, Technical Accuracy, Semantic Fidelity, Terminology Consistency, Tone/Register) with weighting.\n- **`references/ethglossary-usage.md`** — Load when integrating ETHGlossary lookups into a review (which endpoint, when to use `/filter` vs `/translations/{lang}`, how to handle missing terms, what `script_rule` and `term_role` mean for reviewers).\n- **`references/per-language-tracking.md`** — Load when writing to or reading from `.claude/translation-review/per-language/{lang}.md`. The convention for accumulating findings across reviews.\n- **`references/gotchas.md`** — Load when something feels off and you can't find it inline. The long tail of confusion patterns.\n- **`references/critical-vs-warning.md`** — Load when deciding whether to flag an issue as critical (auto-fix) or warning (manual review). The severity rubric.\n- **`references/agent-roles.md`** — Load when planning a multi-agent review (structural / terminology / semantic). The split and what each role focuses on.\n\n## Other Project Skills That May Apply\n\n- **`intl-pipeline`** — For the pipeline side: how translations are produced, manifest invariants, sanitizer behavior, the `intl/pending-{base}` model. When a review surfaces a pipeline-level bug (not a translation-level one), reach for this skill.\n- **`design-system`** — When a translation issue is rendering-related (BiDi flip, RTL spacing, layout overflow from longer non-English strings).\n\n## Pre-Merge Smoke Test\n\nBefore submitting a translation-quality review:\n\n- [ ] ETHGlossary `/filter` endpoint was called for at least one file per language (don't review from memory)\n- [ ] All critical issues listed (glossary deviations, MDX syntax errors, brand mistranslations, translated hrefs, semantic inversions)\n- [ ] Frontmatter `tags` distinction respected (brand tags English, concept tags translated)\n- [ ] Code-block policy applied (functional code English, comments may be translated)\n- [ ] Per-language quality score produced in the 5-category rubric\n- [ ] Per-language findings file updated at `.claude/translation-review/per-language/{lang}.md`\n- [ ] If new issue pattern surfaced, added to `references/known-patterns.md` (or `known-patterns.md` in the KB)\n- [ ] All review changes (fixes + knowledge-base updates) committed to the `intl/pending-*` branch under review and pushed — never left on `dev`\n- [ ] Build verification (`--build-local` or `--netlify-check`) run if the review touched MDX-syntax-related issues\n",".claude/skills/page-visual-tests/SKILL.md":"---\nname: page-visual-tests\ndescription: Playwright + Chromatic full-page visual tests for ethereum.org. Trigger on \"add a page to the visual suite\", \"the snapshot keeps changing\", \"chromatic pages\", \"chromatic playwright\", or edits to `tests/visual/`, `playwright.visual.config.ts`, or the `page-visual-tests` job in `.github/workflows/ci.yml`. Skip for Storybook Chromatic (`.github/workflows/chromatic.yml`), e2e (`tests/e2e/`), unit (`tests/unit/`).\n---\n\n# Page Visual Tests (Playwright + Chromatic)\n\nThis skill covers the **page visual tests** Chromatic project only (the `page-visual-tests` job in ci.yml publishes via `chromaui/action`; local publish is `pnpm chromatic:pages` with `CHROMATIC_PAGES_TOKEN`) — not Storybook Chromatic.\n\nThe Playwright suite captures DOM archives (not PNGs) per page × viewport; Chromatic re-renders them in the cloud to diff. A green local `pnpm test:visual` just means archives were produced — the diff happens after upload.\n\n## Files that matter\n\n- `playwright.visual.config.ts` — visual-only config (3 viewports + `webServer`)\n- `playwright.config.ts` — base (e2e + unit; **no `webServer`**)\n- `tests/visual/pages.spec.ts` — page list + readiness pattern\n- `.github/workflows/ci.yml` — CI (the `page-visual-tests` job)\n- `src/components/ui/skeleton.tsx`, `src/components/ui/spinner.tsx` — loading primitives\n- `package.json` scripts: `test:visual*`, `chromatic:pages`\n\n## Non-obvious constraints\n\n**Dual Playwright config.** `webServer` lives only in `playwright.visual.config.ts`. Moving it into the base config breaks `pnpm test:unit` and `pnpm test:e2e` in CI — they try to start Next against a missing `.next` build.\n\n**Desktop viewport is 1024, not 1280.** Chromatic caps snapshots at `width × height ≤ 25M` px. The tallest tested pages reach ~22.5k px; 1280 overflows, 1024 fits. Measure `document.documentElement.scrollHeight` before raising the viewport or adding a long page.\n\n**Loading contract: `data-slot=\"loading\"`.** The shared `Skeleton` and `Spinner` primitives carry this attribute. Each test waits until `document.querySelectorAll('[data-slot=\"loading\"]').length === 0` before snapshotting. Any bespoke loader — raw `animate-pulse-light`, a local Skeleton copy, a custom spinner — is invisible to the wait and will silently flake. Fix by routing through the shared primitive or adding `data-slot=\"loading\"` to the bespoke loader's root.\n\n**Imports come from `@chromatic-com/playwright`, not `@playwright/test`.** The two packages re-export `expect` with skewed types, so `expect(...).toHaveCount(0)` misbehaves — prefer `page.waitForFunction` for the loading wait.\n\n**Environment.** `USE_MOCK_DATA=true` and `NEXT_PUBLIC_BUILD_LOCALES` are required at build and test time. The local `test:visual:build` script sets `NEXT_PUBLIC_BUILD_LOCALES=en`; CI's shared build in ci.yml uses `NEXT_PUBLIC_BUILD_LOCALES: \"en,es,zh,ar\"` because one build artifact serves the e2e, lighthouse, and visual jobs. Paths in the spec are unprefixed (`/wallets/`, not `/en/wallets/`) because `localePrefix: \"as-needed\"` serves English at the root — adding `/en` would just trigger a redirect.\n\n**Random ordering: `safeShuffle`.** Lodash `shuffle` and `.sort(() => Math.random() - 0.5)` flake snapshots independently of loaders. Wrap them with `safeShuffle` from `src/lib/utils/random.ts` — it returns the list unchanged when `IS_VISUAL_TEST=true`. Current call sites: `src/lib/utils/wallets.ts`, `src/lib/utils/apps.ts` (Highlights/Discover/AppOfTheWeek), `src/components/Staking/StakingProductsCardGrid/index.tsx`. The env var is exposed to the client bundle via `next.config.js`'s `env` block; without that, `process.env.IS_VISUAL_TEST` evaluates to `undefined` in client components and the shuffle still runs.\n\n**Use `domcontentloaded`, not `networkidle`.** Analytics and background fetches keep the network perpetually busy.\n\n## Canonical test\n\n```ts\nimport { takeSnapshot, test } from \"@chromatic-com/playwright\"\n\nconst pages: Array<{ name: string; path: string }> = [\n  { name: \"Homepage\", path: \"/\" },\n  { name: \"Docs - Smart Contracts\", path: \"/developers/docs/smart-contracts/\" },\n  // ...\n]\n\ntest.describe(\"Page Visual Tests\", () => {\n  for (const { name, path } of pages) {\n    test(name, async ({ page }, testInfo) => {\n      await page.goto(path, { waitUntil: \"domcontentloaded\" })\n      await page.waitForFunction(\n        () => document.querySelectorAll('[data-slot=\"loading\"]').length === 0\n      )\n      // FeedbackWidget is dynamic({ ssr: false }); waiting for its button proves\n      // hydration finished and dynamic chunks landed — same pattern other\n      // ssr:false components (Emoji/Twemoji) rely on to render.\n      await page.waitForSelector('[data-testid=\"feedback-widget-button\"]')\n      await takeSnapshot(page, testInfo)\n    })\n  }\n})\n```\n\n## Common situations\n\n**Adding a page.** Each entry costs three snapshots (one per viewport) against Chromatic's budget, so check whether the page's layout (under `src/layouts/`) is already covered before adding. Scan the page subtree for bespoke loaders — they're the single biggest flake cause — and confirm full-page height stays under the 25M-pixel budget. Local loop: `pnpm test:visual:build` once, then `pnpm test:visual:desktop` for iteration, `pnpm test:visual` for the full sweep.\n\n**Flaky snapshot.** Two main causes. (1) A loader without `data-slot=\"loading\"` — run with `--trace=on` and inspect the `waitForFunction` step; ~0 ms duration means it isn't being waited on. (2) Random ordering — grep the page subtree for `shuffle(`, `Math.random()`, or `.sort(() =>` and route through `safeShuffle`. If dynamic content is drifting, double-check `USE_MOCK_DATA=true` is set in both build and test steps.\n\n**Local `pnpm dev` masks a regression.** `playwright.visual.config.ts` sets `reuseExistingServer: true`, which is correct for CI but means a `pnpm dev` already running on :3000 will be used silently in place of the production build the suite assumes. If a snapshot diff doesn't reproduce in CI, kill the dev server and run `pnpm test:visual:build` to rebuild against the production output before retrying.\n\n**Pixel-limit error.** Measure the page's full-page height at 1024 px; if it exceeds ~24,400 px, the page needs shortening or removal from the suite. Don't crop to viewport — below-the-fold coverage is the point of this suite.\n\n**Works locally, fails in CI.** Usually `HOME: /root` missing from the test step — GitHub Actions overrides `HOME` inside containers, and Playwright can no longer find the browsers baked into the `mcr.microsoft.com/playwright` image. Also check that the image tag matches `@playwright/test` in `package.json`.\n",".claude/skills/review-page/SKILL.md":"---\nname: review-page\ndescription: Adversarial review loop for a built ethereum.org page — spawns copy and design-system reviewer subagents against the Figma source, triages their findings, fixes, and re-reviews until a fresh round comes back clean. Invoke with /review-page.\ndisable-model-invocation: true\n---\n\n# Review Page\n\nAudit a built page by turning two **adversarial** reviewers loose on it, fixing what they find, and re-running them until a fresh round comes back **green**. Each reviewer is a subagent prompted to assume the page is *wrong* and hunt for proof — never to rubber-stamp it. You (the orchestrator) own the Figma pull, the triage, and the fixes; the reviewers only report. This is the QA half of building a page — pair it with `/new-page`, or run it standalone against any existing page.\n\nWork the steps in order. Each ends on a checkable condition — don't start the next until the current one is met.\n\n## Step 0 — Ground truth\n\nYou need two things before any review:\n\n1. **The live page.** Start `pnpm dev`, load the route (locale-stripped for `en`: `/<route>/`), confirm **HTTP 200**. A 500 is a build/asset error — fix that first; there is nothing to audit on a broken page.\n2. **The design source.** A Figma link/selection is the gold standard. A written spec serves the copy reviewer but not the pixel-level design checks. **No source at all → the copy reviewer has nothing to sync against; say so and confirm the user still wants a design-system-only pass.**\n\n**Done when:** the page serves 200 and the design source is known (or its absence is confirmed).\n\n## Step 1 — Capture both sides\n\nThe reviewers compare *the page as it renders* against *the design as it's specified*. Capture both once, to a scratch dir, and pass the paths to the subagents — don't make each reviewer re-pull Figma (reads drift between pulls, and it burns rate limits).\n\n- **Design.** Read the Figma **fresh** — never a read from an earlier turn: `get_metadata` for the frame tree, `get_screenshot` per region, `get_design_context` for the text, `get_variable_defs` for tokens. Save the region screenshots; keep the text/token dump.\n- **Live page.** Full-page screenshot with `playwright-cli`, plus per-section shots for a long page. Save them beside the design shots.\n\n**Done when:** design and live-page artifacts are saved and their paths are known.\n\n## Step 2 — Spawn the adversarial reviewers\n\nRead both briefs in `reviewers/` and spawn **one subagent per brief, in parallel** — a single message with two `Agent` calls. Give each subagent its brief verbatim, the route, and the artifact paths from Step 1. Each returns a structured findings list — `{location, claim, evidence, fix, severity}` — and nothing else. **Reviewers report; they never edit** (two agents editing in parallel would collide).\n\n- `reviewers/copy.md` — every string on the page matches the Figma/source copy.\n- `reviewers/design-system.md` — components, spacing, heading sizes, and colors are the design system's, with no custom CSS bolted onto primitives.\n\n**Done when:** both subagents have returned their findings lists.\n\n## Step 3 — Triage\n\nAdversarial reviewers over-report by design. **You** judge each finding before touching code — keep the real ones, drop the noise. Two calls need judgment:\n\n- **Intentional copy divergence.** A string differs because the *design is stale*, or because it's a templated/localized value — not a copy bug. Confirm against the source, not the reviewer's assumption.\n- **Justified custom CSS.** Custom CSS *chasing pixel parity* on a primitive is a finding; custom CSS the design *genuinely requires* (a one-off the system can't express) is acceptable per \"match, don't mimic.\" Don't strip styling the design needs.\n\nShow the user the **confirmed** findings as a table — `Region · Finding · Fix · Severity` — and flag anything ambiguous for their call. This is their window into what the reviewers saw.\n\n**Done when:** every finding is confirmed or dropped, and the confirmed table has been shown to the user.\n\n## Step 4 — Fix\n\nApply the confirmed findings yourself, reuse-first — a variant, not custom CSS; the right primitive, not a re-invention. Load the `design-system` skill if you haven't, for the component and token calls.\n\n**Done when:** every confirmed finding is fixed in the working tree.\n\n## Step 5 — Re-review until green\n\nRe-capture the **live page** (it changed; the Figma side didn't — reuse Step 1's design artifacts) and re-spawn the same reviewers. The loop is **green** when a fresh adversarial round returns **zero** confirmed findings. Until then: triage → fix → re-review.\n\nCap it: if the loop isn't green after **3 rounds**, stop and hand the user the stubborn findings — a finding that survives three fixes usually needs a human design call, not another pass.\n\n**Done when:** a fresh round is green, or the 3-round cap is hit and the remaining findings are handed off.\n","AGENTS.md":"# CLAUDE.md - Ethereum.org Website\n\n## Project Overview\n\nThis is the official Ethereum.org website - a Next.js application that serves as the primary educational and community hub for Ethereum. The site is built with modern web technologies and focuses on accessibility, internationalization, and performance.\n\n## Technology Stack\n\n### Core Framework\n\n- **Next.js 16+** - React framework with App Router (Turbopack is the default bundler; webpack available via `pnpm dev:webpack` / `pnpm build:webpack`)\n- **React 19** - UI library\n- **TypeScript 5.5+** - Type safety and development experience\n- **Tailwind CSS 4+** - Utility-first CSS framework (CSS-first config in `src/styles/global.css`)\n\n### Key Dependencies\n\n- **next-intl 4+** - Internationalization (i18n) with 25 languages\n- **next-mdx-remote 5.0+** - MDX content processing\n- **Motion 12+** (`motion` package, formerly Framer Motion) - Animations and transitions; import from `motion/react`\n- **Radix UI** - Accessible component primitives\n- **shadcn/ui** - Component library built on Radix UI\n- **Recharts** - Data visualization\n- **Viem/Wagmi** - Ethereum blockchain integration\n\n### Development & Testing\n\n- **Storybook 10+** - Component development and testing\n- **Chromatic** - Visual regression testing\n- **ESLint** - Code linting with custom rules\n- **Prettier** - Code formatting\n- **Husky** - Git hooks\n- **PNPM** - Package manager\n\n## Project Structure\n\n- **app/** - Next.js App Router pages\n  - **[locale]/** - Internationalized routes\n- **src/**\n  - **components/** - React components\n    - **ui/** - Design system components\n    - **icons/** - SVG icon components\n  - **data/** - Static data and configurations\n  - **hooks/** - Custom React hooks\n  - **i18n/** - Internationalization config\n  - **intl/** - Translation files (25 languages)\n  - **layouts/** - Page layout components\n  - **lib/** - Utility functions and types\n    - **constants.ts** - App constants\n    - **types.ts** - TypeScript type definitions\n    - **utils/** - Utility functions\n  - **styles/** - Global styles and design tokens\n- **public/** - Static assets\n  - **content/** - Markdown content files\n  - **images/** - Image assets\n- **docs/** - Development documentation\n  - **solutions/** - Documented solutions to past problems, organized by category with YAML frontmatter (module, tags, problem_type)\n\n## Code Conventions\n\n### File Naming\n\n- **Components**: kebab-case (e.g., `button-group.tsx`)\n- **Utilities**: camelCase (e.g., `cn.ts`, `relativePath.ts`)\n- **Pages**: kebab-case following Next.js conventions\n- **Assets**: kebab-case (e.g., `eth-logo.png`)\n\n### TypeScript Patterns\n\n- Use `interface` for object shapes, `type` for unions/intersections\n- Prefer explicit typing over `any` (ESLint enforces `fixToUnknown`)\n- **NEVER leave unused variables or parameters** - ESLint `unused-imports/no-unused-vars` will fail the Netlify build. The only allowed unused arg pattern is a single underscore `_`. Do NOT use `_prefixedNames` (e.g., `_foo`) - either use the variable or remove it from the signature entirely.\n- Use generic constraints for reusable components\n- Export types from dedicated files in `@/lib/types`\n\n### Styling Conventions\n\n- **Primary approach**: Tailwind CSS utility classes\n- **Component variants**: Use `tailwind-variants` (`tv`) for new and refactored work. Existing `class-variance-authority` (`cva`) components don't need bulk migration -- swap to `tv` opportunistically when you're already touching the component for another reason.\n- **Dynamic classes**: Use `cn()` utility (clsx + tailwind-merge)\n- **Custom properties**: CSS variables in `src/styles/` for theme values\n- **Responsive design**: Mobile-first approach\n\nFor UI work, see the **`design-system` skill** at `.claude/skills/design-system/`. It's the canonical knowledge base for component choices, design tokens, RTL/i18n, server/client boundaries, and the \"use a variant, not a new component\" pattern.\n\n## Development Workflows\n\n### Available Scripts\n\n```bash\n# Development\npnpm dev                    # Start development server (Turbopack)\npnpm dev:webpack            # Start development server with webpack\npnpm build                  # Build for production (Turbopack)\npnpm build:webpack          # Build for production with webpack\npnpm start                  # Start production server\n\n# Code Quality\npnpm lint                   # Run ESLint\npnpm lint:fix              # Fix ESLint issues\npnpm type-check            # TypeScript type checking\npnpm format                # Format with Prettier\n\n# Testing\npnpm test:unit             # Playwright unit tests (unit project)\npnpm test:e2e              # Playwright end-to-end tests\npnpm test:visual           # Playwright + Chromatic full-page visual tests\n\n# Storybook\npnpm storybook             # Start Storybook dev server\npnpm build-storybook       # Build Storybook\npnpm chromatic             # Storybook Chromatic (needs CHROMATIC_STORYBOOK_TOKEN)\n\n# Content Management\npnpm lint:md               # Lint English markdown content\npnpm lint:md:fix           # Auto-fix header IDs and duplicates\n```\n\n### Testing Strategy\n\n- **Unit Testing**: Playwright `unit` project (`pnpm test:unit`) — heaviest coverage is the intl-pipeline sanitizer\n- **E2E Testing**: Playwright (`pnpm test:e2e`), see `docs/e2e-testing.md`\n- **Visual Testing**: Storybook + Chromatic for component regression; Playwright + Chromatic for full pages (see the `page-visual-tests` skill)\n- **Type Safety**: TypeScript strict mode enabled\n- **Linting**: ESLint with custom rules for imports and TypeScript\n\n## Content Management\n\n### Internationalization\n\n- **25 languages** supported (canonical list: `i18n.config.json`); **RTL support** for Arabic, Urdu\n- JSON UI strings in `src/intl/[locale]/`; translated markdown content in `public/content/translations/[locale]/`\n- Non-English markdown is propagated by the **intl-pipeline** (`src/scripts/intl-pipeline/`, entry `main.ts`). **Do not hand-propagate English changes into non-English files** -- let the pipeline run, or trigger `intl-pipeline.yml` with `stamp_only: true` if manifests must catch up urgently (e.g. unblocking a build). Hand-fixing a translation error is fine when the English side hasn't moved, since the manifest mapping stays valid. Spec: `tests/specs/PIPELINE-SPEC.md`.\n- Glossary: base URL from `GLOSSARY_API_URL` env var; default in `src/scripts/intl-pipeline/config.ts`. ETHGlossary is authoritative for Ethereum term translations.\n\nFor pipeline mechanics, recovery, manifests, ETHGlossary integration, and the `intl/pending-{base}` orchestration model, see the **`intl-pipeline` skill** at `.claude/skills/intl-pipeline/`. For translation-quality review (scoring rubric, language-group rules, ETHGlossary-as-authority policy, multi-agent role split), see the **`intl-review` skill** at `.claude/skills/intl-review/`.\n\n### Markdown Content\n\n- Educational content stored in `public/content/`\n- Processed with `next-mdx-remote`\n- Custom MDX components for rich content\n- Automatic table of contents generation\n- **All h1-h4 headings require a custom `{#lower-kebab-id}`** -- enforced by markdownlint via pre-commit hook. Run `pnpm lint:md:fix` to auto-add missing IDs. Config: `.markdownlint-cli2.jsonc`, custom rules: `.markdownlint-rules/`\n\n### Asset Management\n\n- Images optimized with Next.js Image component\n- SVGs loaded as React components via `@svgr/webpack`\n- Static assets served from `public/`\n- Placeholder generation for images\n\n## SEO & Meta\n\n- Sitemap generation in `app/sitemaps/sitemap.ts` (one sitemap per locale, indexed by `app/sitemap.xml/route.ts`)\n- Meta tags and Open Graph optimization\n- Structured data for search engines\n- Security headers (X-Frame-Options: DENY)\n\n## Development Guidelines\n\n### When Working on Features\n\n1. **Check existing patterns** - Look at similar components first\n2. **Prioritize Server Components** - Use App Router and Server Components when possible\n3. **Follow import order** - ESLint will enforce, but be proactive\n4. **Use TypeScript strictly** - No `any` types, prefer `unknown`\n5. **Test in Storybook** - Create stories for new components (filename pattern: `.stories.tsx`)\n6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations(\"common\")`) rather than reaching across namespaces\n7. **Mobile-first** - Design for mobile, enhance for desktop\n8. **Accessibility** - Use Radix primitives, semantic HTML\n9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales.\n\n### Component Development\n\n1. Create component in appropriate `src/components/` subdirectory\n   - Use `src/components/ui` for shadcn components or pure UI components\n2. Add TypeScript types and proper props interface\n3. Accept `ref` as a regular prop when needed (React 19); `forwardRef` only survives in legacy components\n4. Add a Storybook story (see below)\n5. Export from appropriate index file\n6. Update documentation if adding new patterns\n\n### Storybook stories\n\n**Location**: `__stories__/` for flat directories (`ui/`, `styles/`), co-located for foldered components. Story globs are recursive, so nesting works. **Filenames are kebab-case** (`app-card.stories.tsx`) even inside a PascalCase component directory -- enforced by the test below.\n\n**Title**: `Section / Group / Name`. The section is derived from the file path -- not a judgment call:\n\n| Path                                  | Section         |\n| ------------------------------------- | --------------- |\n| `src/styles/**`                       | `Design System` |\n| `src/components/ui/**`                | `UI`            |\n| `src/components/**` (everything else) | `Components`    |\n| `src/layouts/**`                      | `Layouts`       |\n| `app/**`                              | `Pages`         |\n\nUse a second-level group only when an established one fits (Actions, Forms, Layout, Navigation, Overlays, Data Display, Cards, Heroes, Site Chrome, Content, Data Viz, Features). **Don't invent a group for a single component** -- a flat `Components / Morpher` is correct and preferred. `tests/unit/storybook/story-titles.spec.ts` enforces the section prefix, explicit titles, and title uniqueness (Storybook silently merges duplicate titles into one sidebar entry).\n\n**Don't add a story for a deprecated component or variant.** A rendered example reads as an endorsement whatever the caption says, so the showcase should only contain things that are available to use -- note the deprecation in prose instead (see `HR`). A component whose future is merely undecided can keep its story with a status blurb (see `Carousel`).\n\nAdd `tags: [\"autodocs\"]`. Write the `parameters.docs.description.component` blurb for someone deciding whether to use the thing -- constraints and gotchas, not a restatement of the props table.\n\n**A green build does not mean a story renders.** `pnpm build-storybook` compiles stories without executing them, and every `UI /` story sets `chromatic: { disableSnapshot: true }`, so neither CI signal catches a story that throws. Open new stories in `pnpm storybook` before pushing.\n\nAsync server components render via a `next-intl/server` shim (`.storybook/next-intl-server.tsx`). Components calling `usePathname` need `parameters.nextjs.navigation.pathname`. New i18n namespaces must be added to the `ns` array in `.storybook/next-intl.ts`, or strings render as raw key tails.\n\n### Content Updates\n\n1. Markdown files go in `public/content/`\n2. Images in `public/images/` with descriptive names\n3. Translation strings in appropriate `src/intl/` JSON files\n4. Data files in `src/data/` with TypeScript types\n\n### Type-Safe Chain Names\n\nThis project enforces type-safe chain names via TypeScript. When working with layer 2 networks or wallet data:\n\n**Critical Files:**\n\n- `src/data/chains.ts` - Canonical source of all chain names (auto-updated weekly)\n- `src/lib/types.ts` - Defines `ChainName` type derived from chains.ts\n- `src/data/networks/networks.ts` - Uses `chainName: ChainName`\n- `src/data/wallets/wallet-data.ts` - Uses `supported_chains: ChainName[]`\n\n**Rules:**\n\n1. **Always look up exact names** - Before adding `chainName` or `supported_chains`, search `chains.ts` for the exact `name` value\n2. **Names are case-sensitive and exact** - e.g., use `\"Zircuit Mainnet\"` not `\"Zircuit\"`, use `\"OP Mainnet\"` not `\"Optimism\"`\n3. **Run type checking** - Use `pnpm type-check` to verify chain names are valid before committing\n4. **Non-EVM chains** - For Starknet and other non-EVM chains, use `NonEVMChainName` type\n\n**Common Mistakes:**\n\n- Using informal names: `\"Optimism\"` should be `\"OP Mainnet\"`\n- Missing \"Mainnet\" suffix: `\"Zircuit\"` should be `\"Zircuit Mainnet\"`\n- Wrong casing: `\"zksync Mainnet\"` should be `\"zkSync Mainnet\"`\n\n## Key Dependencies to Know\n\n### UI & Styling\n\n- `@radix-ui/*` - Accessible component primitives\n- `tailwind-variants` - Component variant patterns\n- `motion` - Animation library (formerly framer-motion; import from `motion/react`)\n- `lucide-react` - Icon library\n\n### Content & Data\n\n- `gray-matter` - Frontmatter parsing\n- `recharts` - Data visualization\n\n### Ethereum Integration\n\n- `viem` - Ethereum library\n- `wagmi` - React hooks for Ethereum\n- `@rainbow-me/rainbowkit` - Wallet connection\n\n## A/B Testing\n\nThe site uses a GDPR-compliant, cookie-less A/B testing system built on the Flags SDK precompute pattern, integrated with Matomo. The proxy assigns variants at the edge (header fingerprinting) and rewrites to signed, statically prerendered variant pages under `ab-code/[code]/` — experiments never force dynamic rendering. Experiment status, weights, and scheduling live in the Matomo dashboard (no deploys needed). Only the default locale is tested; other locales always get the original.\n\nKey gotcha: **variants are matched by array index, not names** — the `variants` array order must match the Matomo experiment order exactly, and the `testKey` must match the Matomo experiment name exactly.\n\nFull guide (architecture, step-by-step recipe, env vars): `docs/ab-testing.md`. Code: `proxy.ts`, `src/lib/ab-testing/`, `src/components/AB/`, `app/[locale]/ab-code/`.\n\n## Site Search\n\nSearch runs on a self-hosted Typesense instance, one collection per locale, crawled by DocSearch from production ethereum.org. Publishing is gated: a crawl goes to a staging collection and the live alias only moves if the new index passes size, field and relevance checks.\n\nTwo things bite most often. **Ranking hints reach the index as `docsearch:*` meta tags** from `src/lib/utils/metadata.ts`, so a ranking change does nothing until the site is deployed and re-crawled. And **the networks offered as block explorer results come from `src/data/networks/networks.ts`** -- the same list `/layer-2/networks` renders -- so adding an L2 there is the only edit needed.\n\nOverview: `docs/site-search.md`. Runbook, secrets and recovery: `typesense/README.md`. Code: `src/components/Search/`, `src/scripts/typesense/`, `typesense/`.\n\n## Deployment\n\n- **Platform**: Netlify (config in `netlify.toml`)\n- **Next.js Integration**: Uses `@netlify/plugin-nextjs` for seamless Netlify and Next.js compatibility\n- **Monitoring**: Matomo analytics integration\n\n### Runtime constraints\n\n- **`public/content` is not in the Netlify server function** (CDN-only). A page that reads it at render time must stay static (`revalidate: false` getters like `getStaticAppsData`) or have an `outputFileTracingIncludes` entry in `next.config.js`. A finite-`revalidate` getter (e.g. `getEventsData`, 24h) turns the whole route into ISR. Details: `docs/solutions/integration-issues/netlify-isr-404-async-server-components.md`.\n\n## Internal Infrastructure\n\nThe following external-looking services are managed by the ethereum.org team:\n\n- **`s3-dcl1.ethquokkaops.io`** — S3-compatible object storage for app screenshots and media. Used by the data layer to serve images for the `/dapps` and app listing pages. Downtime here means broken images on the live site.\n- **Netlify Blobs** (`@netlify/blobs`) — Key-value store used by the data layer to cache API responses. Accessed via `src/data-layer/storage.ts`.\n",".claude/skills/performance/SKILL.md":"---\nname: performance\ndescription: Performance patterns and anti-patterns for ethereum.org covering TTFB, LCP, INP, CLS, bundle size, build memory, and RSC payload. Use when diagnosing a perf regression or reviewing proposed code changes for perf landmines.\nuser-invocable: false\n---\n\n# Performance Knowledge\n\nKnowledge base. No runtime actions; authoring conventions for maintainers below. Read the relevant `references/*.md` files directly via the Read tool.\n\n## Two entry points\n\n- **Diagnosing** an existing regression → `references/diagnose-table.md` (symptom → bucket).\n- **Reviewing** a proposed change before merge → `references/review-checklist.md` (diff pattern → rules to load).\n\n## Cross-cutting anti-patterns\n\nThese look right but aren't. Check every review or diagnose pass against this table.\n\n| Looks like…                                                  | Actually…                                                                                                     |\n| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |\n| Adding `CDN-Cache-Control` to fix TTFB                       | Netlify strips them; CF ignores due to `max-age=0`.                                                           |\n| Using `unstable_cache` in MDX routes                         | Triggers ISR → runtime 404s. See `edge-caching.md`.                                                           |\n| ``await import(`.../${locale}/${ns}.json`)``                 | Webpack enumerates all combinations → OOM. Use `fs.readFile`.                                                 |\n| Adding a `useSession` / `cookies()` in root layout           | Every route becomes dynamic. Edge caching dies silently.                                                      |\n| `priority: true` via `getImageProps()` on manual `<picture>` | `fetchPriority` attribute is not set; add it manually.                                                        |\n| Bumping `revalidate` to hourly to \"fix staleness\"            | Increases edge miss rate. Keep daily; add client `/api/*` for freshness.                                      |\n| Mocking DB/content in perf tests                             | Perf wins must be measured against real prod build (`pnpm build && start`).                                   |\n| Removing `common.json` keys without audit                    | Lookups of removed keys silently render the bare key (fallback). Grep the key across `src/` and `app/` first. |\n\n## File map\n\n| Topic                           | File                             |\n| ------------------------------- | -------------------------------- |\n| Symptom → bucket routing        | `references/diagnose-table.md`   |\n| Diff pattern → rules routing    | `references/review-checklist.md` |\n| TTFB, edge caching, CDN         | `references/edge-caching.md`     |\n| Bundle size, code splitting, RSC payload | `references/bundle.md`  |\n| Build OOM, ENOSPC, file tracing | `references/build.md`            |\n| INP, main-thread work           | `references/inp.md`              |\n| LCP, images                     | `references/images.md`           |\n\nFor data-fetching patterns (Trigger.dev tasks, Netlify Blobs, `src/lib/data` caching, internal `/api/*` routes, fetcher retries), use the `data-layer` skill instead.\n\n## Conventions for adding rules\n\n- One topic per file; no content duplication across files.\n- Cite a PR (`PR #N`) when a rule comes from a specific shipped change. Add a SHA only when no PR exists.\n- Forward-looking (\"do this, avoid that\"). Keep entries prescriptive — no postmortems or incident narratives.\n- New review check? Add a row to `review-checklist.md` AND the matching bucket file. Don't restate the rule inline.\n",".claude/commands/fix-sanitizer-bug.md":"---\ndescription: Guided workflow for fixing translation sanitizer bugs — triage, test, fix, verify\nallowed-tools: Bash, Read, Glob, Grep, Task, Edit, Write, AskUserQuestion\nargument-hint: [--language=CODE] [--issue=\"description\"] [--file=PATH] [--skip-build]\n---\n\n# Fix Sanitizer Bug\n\nIterative workflow for fixing bugs in the post-import translation sanitizer (`src/scripts/intl-pipeline/intl-sanitizer.ts`). Follows a test-first approach: triage the issue, write a failing test, implement the fix, verify across languages.\n\n## Context\n- Current branch: !`git branch --show-current`\n- Arguments: $ARGUMENTS\n- Sanitizer: `src/scripts/intl-pipeline/intl-sanitizer.ts`\n- Test files: `tests/unit/intl-pipeline/sanitizer/*.spec.ts`\n- Research docs: `docs/solutions/integration-issues/`\n\n## Phase 0: Gather Context\n\n### Parse Flags\n\nExtract from $ARGUMENTS:\n- `LANGUAGE`: from `--language=CODE` (e.g., `ja`, `zh-tw`, `es`)\n- `ISSUE_DESC`: from `--issue=\"...\"` (brief description of the bug)\n- `FILE_PATH`: from `--file=PATH` (specific file where issue was spotted)\n- `SKIP_BUILD`: from `--skip-build` (skip build verification)\n\n### Collect Information\n\nIf flags are missing, use AskUserQuestion to gather:\n\n1. **What language?** — Which locale has the issue (e.g., `ja`, `tr`, `zh-tw`)\n2. **What's the artifact?** — Exact text of the translation bug (copy-paste the broken string)\n3. **Where?** — File path or general area (markdown content, JSON translations, frontmatter)\n4. **English source?** — What does the correct English look like\n\nRead the affected file and the English source to confirm the issue:\n```\nTranslated: {FILE_PATH}\nEnglish:    {ENGLISH_EQUIVALENT_PATH}\n```\n\n**IMPORTANT:** Capture the exact broken pattern NOW before any processing. Copy the raw artifact verbatim — you'll need it for the test.\n\n## Phase 1: Document the Problem\n\nAppend the new pattern to the research doc:\n\n**File:** `docs/solutions/integration-issues/sanitizer-test-research.md`\n\nAdd a row to the \"New Patterns\" table:\n\n```markdown\n| N+1 | {PATTERN_DESCRIPTION} | {SOURCE_PR_OR_REVIEW} | `{EXACT_EXAMPLE}` | {SEVERITY} |\n```\n\nSeverity guide:\n- **Critical** — Breaks rendering, navigation, or MDX compilation\n- **High** — Breaks links, images, or loses content\n- **Medium** — Wrong text displayed, semantic errors\n- **Low** — Cosmetic, formatting-only\n\n## Phase 2: Triage — Fix, Warn, or Document Only\n\nThis is the most important decision. Use AskUserQuestion:\n\n**\"What type of fix does this need?\"**\n\n### Option A: Deterministic Fix (auto-correct)\nUse when the pattern is:\n- Regex-matchable with no false positives\n- The correct output is always the same (no judgment needed)\n- Safe to apply across all languages\n\nExamples: escaped bold `\\*\\*text\\*\\*`, ticker typos `EHT→ETH`, date format `DD/MM/YYYY→YYYY-MM-DD`\n\n→ Proceed to Phase 3A (write fix function + test)\n\n### Option B: Warning Only (detect + report)\nUse when:\n- The pattern is detectable but the fix requires context/judgment\n- Auto-fixing could cause collateral damage (see Bug #1: href substitution)\n- Different files may need different resolutions\n\nExamples: translated hrefs, missing brand names, code fence drift\n\n→ Proceed to Phase 3B (write warn function + test)\n\n### Option C: Document Only (not automatable)\nUse when:\n- The issue is semantic (wrong word choice, not a pattern)\n- No reliable regex can detect it\n- It needs human/AI review judgment\n\nExamples: \"Gas\" → \"Sprit\" (gasoline) in German, tone inconsistency\n\n→ Skip to Phase 6 (update docs only)\n\n## Phase 3A: Write Failing Test (Fix Function)\n\n### Determine which test file\n\n- Pure function (no English source needed) → `tests/unit/intl-pipeline/sanitizer/standalone-fixes.spec.ts`\n- Needs English comparison → `tests/unit/intl-pipeline/sanitizer/english-comparison.spec.ts`\n- End-to-end through processMarkdownFile/processJsonFile → `tests/unit/intl-pipeline/sanitizer/integration.spec.ts`\n\n### Write the test FIRST\n\nAdd a `test.describe` block for the new function. Include at minimum:\n\n```typescript\ntest.describe(\"fixNewIssue\", () => {\n  test(\"fixes the broken pattern\", () => {\n    const input = \"{EXACT_BROKEN_PATTERN_FROM_PHASE_0}\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(\"{EXPECTED_CORRECT_OUTPUT}\")\n    expect(fixCount).toBe(1)\n  })\n\n  test(\"leaves correct content unchanged\", () => {\n    const input = \"{ALREADY_CORRECT_CONTENT}\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(input)\n    expect(fixCount).toBe(0)\n  })\n\n  test(\"skips code blocks\", () => {\n    // If the fix operates on prose, it MUST skip code blocks\n    const input = \"```\\n{PATTERN_INSIDE_CODE}\\n```\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(input)\n    expect(fixCount).toBe(0)\n  })\n})\n```\n\n### Add import to test file\n\nAdd the new function name to the destructured import from `_testOnly` at the top of the test file.\n\n### Verify test fails\n\n```bash\nnpx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/{FILE}.spec.ts\n```\n\nThe new test MUST fail (function doesn't exist yet). Existing tests should still pass.\n\n## Phase 3B: Write Failing Test (Warn Function)\n\nSame as 3A but assert warnings instead of content changes:\n\n```typescript\ntest.describe(\"warnNewIssue\", () => {\n  test(\"warns on broken pattern\", () => {\n    const warnings = warnNewIssue(\"{BROKEN_INPUT}\", \"{ENGLISH_INPUT}\")\n    expect(warnings.length).toBeGreaterThan(0)\n    expect(warnings[0]).toContain(\"{EXPECTED_WARNING_SUBSTRING}\")\n  })\n\n  test(\"no warning on clean content\", () => {\n    const warnings = warnNewIssue(\"{CLEAN_INPUT}\", \"{ENGLISH_INPUT}\")\n    expect(warnings).toHaveLength(0)\n  })\n})\n```\n\nUse `tests/unit/intl-pipeline/sanitizer/warnings.spec.ts` for warn-only functions.\n\n## Phase 4: Implement the Fix\n\n### Write the function in the sanitizer\n\n**File:** `src/scripts/intl-pipeline/intl-sanitizer.ts`\n\n**For fix functions** — follow the established pattern:\n\n```typescript\nfunction fixNewIssue(content: string): {\n  content: string\n  fixCount: number\n} {\n  let fixCount = 0\n\n  // MANDATORY: Split to preserve code blocks\n  const codeBlockPattern = /(```[\\s\\S]*?```|~~~[\\s\\S]*?~~~|`[^`]+`)/g\n  const parts = content.split(codeBlockPattern)\n\n  for (let i = 0; i < parts.length; i++) {\n    if (i % 2 === 1) continue // Skip code blocks\n\n    // Your fix logic here\n    parts[i] = parts[i].replace(/{PATTERN}/g, () => {\n      fixCount++\n      return \"{REPLACEMENT}\"\n    })\n  }\n\n  return { content: parts.join(\"\"), fixCount }\n}\n```\n\n**Critical rules:**\n- ALWAYS split on code blocks first (fenced + inline)\n- ALWAYS return `{ content, fixCount }` for fix functions\n- ALWAYS return `string[]` for warn functions\n- Use word boundaries `\\b` for brand names to avoid partial matches\n- Use `escapeRegex()` when building regex from dynamic strings\n\n### Add to _testOnly export\n\nAdd the function name to the `_testOnly` export object near the bottom of the file.\n\n### Wire into processMarkdownFile or processJsonFile\n\nAdd the function call using the `applyFix` helper pattern:\n\n```typescript\napplyFix(\n  () => fixNewIssue(content),\n  (n) => `Fixed ${n} new issues`\n)\n```\n\n**Placement matters:** Consider whether the fix should run before or after existing fixes. Some fixes depend on others having run first.\n\nFor warn functions, add directly:\n```typescript\nconst newWarnings = warnNewIssue(content, englishMd)\nissues.push(...newWarnings)\n```\n\n## Phase 5: Run Tests and Verify\n\n### Step 1: Unit tests\n\n```bash\nnpx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/\n```\n\n**All tests must pass** — both the new test and all existing 600+ tests.\n\nIf a test fails:\n- New test fails → fix the implementation, not the test\n- Existing test fails → your fix has a regression, investigate the interaction\n\n### Step 2: Run sanitizer against ONLY the affected files\n\n**CRITICAL: NEVER run the sanitizer against an entire language. It processes thousands of files and will hang for 30+ minutes. Always scope to the specific files from the PR.**\n\nDetermine which files to test from the PR context (e.g., `gaming/index.md`). The sanitizer's only env-var scoping is `TARGET_LANGUAGES` (per-language, not per-file) — there is no `TARGET_FILES`. For single files, write an inline script that calls `processMarkdownFile` directly:\n\n```bash\n# Language-level scoping (still processes every file for that language):\nTARGET_LANGUAGES={LANGUAGE} \\\n  pnpm exec tsx ./src/scripts/intl-pipeline/intl-sanitizer.ts\n\n# Preferred for single files — inline script calling processMarkdownFile directly:\npnpm exec tsx -e '\nconst { _testOnly } = require(\"./src/scripts/intl-pipeline/intl-sanitizer\");\nconst fs = require(\"fs\");\nconst file = \"public/content/translations/{LANGUAGE}/{PAGE_PATH}\";\nconst content = fs.readFileSync(file, \"utf8\");\nconst result = _testOnly.processMarkdownFile(file, content);\nconsole.log(result.issues.join(\"\\n\"));\nif (result.fixed) fs.writeFileSync(file, result.content);\n'\n```\n\nCheck the output for:\n- Your fix being applied (look for the issue label in the log)\n- No unexpected fixes in other areas\n- Fix count looks reasonable (not 0, not thousands)\n\n### Step 3: Inspect the actual changes\n\n```bash\ngit diff public/content/translations/{LANGUAGE}/{PAGE_PATH}\n```\n\nVerify:\n- The broken pattern is corrected\n- No collateral damage to surrounding content\n- Changes look correct to a human reader\n\n### Step 4: Build verification (conditional)\n\n**Only run this if the fix touches MDX syntax** — angle brackets, tags, components, backticks.\n\n**Skip if** `--skip-build` flag is set, or fix is purely textual (ticker corrections, brand tags, date normalization, guillemets, bold/italic escaping).\n\n```bash\nNEXT_PUBLIC_BUILD_LOCALES=en,{LANGUAGE} pnpm build\n```\n\n**NOTE:** This step requires `dangerouslyDisableSandbox: true` and significant RAM. Only use when the fix could affect MDX compilation.\n\n### Step 5: Cross-language spot check on the SAME file only\n\nTest the same page in 2-3 other languages to check for false positives. **NEVER run across all files for a language.**\n\n```bash\n# Test the same page path in a few other languages\nfor lang in es tr ja; do\n  pnpm exec tsx -e \"\n    const { _testOnly } = require('./src/scripts/intl-pipeline/intl-sanitizer');\n    const fs = require('fs');\n    const file = 'public/content/translations/$lang/{PAGE_PATH}';\n    if (!fs.existsSync(file)) { console.log('$lang: file not found, skipping'); process.exit(0); }\n    const content = fs.readFileSync(file, 'utf8');\n    const result = _testOnly.processMarkdownFile(file, content);\n    console.log('$lang:', result.issues.length ? result.issues.join('; ') : 'clean');\n  \"\ndone\n```\n\nCheck that your fix doesn't trigger unexpectedly in other languages.\n\n## Phase 6: If Not Resolved\n\nIf the fix doesn't resolve the issue after Phase 5:\n\n### Diagnose the root cause\n\nUse AskUserQuestion:\n\n**\"What went wrong?\"**\n\n1. **Pattern mismatch** — regex doesn't match the real-world variant\n   - Get more examples of the broken pattern\n   - Broaden the regex\n   - Add another test case for the variant\n   - Go back to Phase 4\n\n2. **Interaction effect** — another fix runs first and changes content\n   - Identify which fix runs first and transforms the input\n   - Reorder the fix in `processMarkdownFile` (earlier or later)\n   - Add an interaction test in `integration.spec.ts`\n   - Go back to Phase 4\n\n3. **False positives in other languages** — fix breaks something elsewhere\n   - Add language-specific exclusions\n   - Add a cross-language test case\n   - Consider making it warn-only instead\n   - Go back to Phase 3\n\n4. **Not actually automatable** — needs more context than regex can provide\n   - Convert to warn function or document-only\n   - Go back to Phase 2 and re-triage\n\n## Phase 7: Update Documentation\n\n### Update research doc\n\n**File:** `docs/solutions/integration-issues/sanitizer-test-research.md`\n\nIf the pattern was new (not already in the table), ensure it was added in Phase 1.\n\nIf the fix worked, move the pattern from \"New Patterns Not Yet Covered\" to \"Patterns Already Handled by Sanitizer\" with the function name.\n\n### Update existing bug docs if relevant\n\nCheck if this relates to previously documented bugs:\n- `docs/solutions/integration-issues/intl-pipeline-bugs-from-pr-18041-review.md`\n\n### Report summary\n\nDisplay to user:\n```\n## Fix Complete\n\n**Issue:** {ISSUE_DESCRIPTION}\n**Type:** {fix | warn | document-only}\n**Function:** {FUNCTION_NAME}\n**Test file:** {TEST_FILE}\n**Tests:** {N} new tests added, {TOTAL} total passing\n**Languages verified:** {LANGUAGES_CHECKED}\n**Files changed:**\n  - src/scripts/intl-pipeline/intl-sanitizer.ts (fix + export)\n  - tests/unit/intl-pipeline/sanitizer/{FILE}.spec.ts (new tests)\n  - docs/solutions/integration-issues/sanitizer-test-research.md (documentation)\n```\n\n","CLAUDE.md":"# CLAUDE.md - Ethereum.org Website\n\n## Project Overview\n\nThis is the official Ethereum.org website - a Next.js application that serves as the primary educational and community hub for Ethereum. The site is built with modern web technologies and focuses on accessibility, internationalization, and performance.\n\n## Technology Stack\n\n### Core Framework\n\n- **Next.js 16+** - React framework with App Router (Turbopack is the default bundler; webpack available via `pnpm dev:webpack` / `pnpm build:webpack`)\n- **React 19** - UI library\n- **TypeScript 5.5+** - Type safety and development experience\n- **Tailwind CSS 4+** - Utility-first CSS framework (CSS-first config in `src/styles/global.css`)\n\n### Key Dependencies\n\n- **next-intl 4+** - Internationalization (i18n) with 25 languages\n- **next-mdx-remote 5.0+** - MDX content processing\n- **Motion 12+** (`motion` package, formerly Framer Motion) - Animations and transitions; import from `motion/react`\n- **Radix UI** - Accessible component primitives\n- **shadcn/ui** - Component library built on Radix UI\n- **Recharts** - Data visualization\n- **Viem/Wagmi** - Ethereum blockchain integration\n\n### Development & Testing\n\n- **Storybook 10+** - Component development and testing\n- **Chromatic** - Visual regression testing\n- **ESLint** - Code linting with custom rules\n- **Prettier** - Code formatting\n- **Husky** - Git hooks\n- **PNPM** - Package manager\n\n## Project Structure\n\n- **app/** - Next.js App Router pages\n  - **[locale]/** - Internationalized routes\n- **src/**\n  - **components/** - React components\n    - **ui/** - Design system components\n    - **icons/** - SVG icon components\n  - **data/** - Static data and configurations\n  - **hooks/** - Custom React hooks\n  - **i18n/** - Internationalization config\n  - **intl/** - Translation files (25 languages)\n  - **layouts/** - Page layout components\n  - **lib/** - Utility functions and types\n    - **constants.ts** - App constants\n    - **types.ts** - TypeScript type definitions\n    - **utils/** - Utility functions\n  - **styles/** - Global styles and design tokens\n- **public/** - Static assets\n  - **content/** - Markdown content files\n  - **images/** - Image assets\n- **docs/** - Development documentation\n  - **solutions/** - Documented solutions to past problems, organized by category with YAML frontmatter (module, tags, problem_type)\n\n## Code Conventions\n\n### File Naming\n\n- **Components**: kebab-case (e.g., `button-group.tsx`)\n- **Utilities**: camelCase (e.g., `cn.ts`, `relativePath.ts`)\n- **Pages**: kebab-case following Next.js conventions\n- **Assets**: kebab-case (e.g., `eth-logo.png`)\n\n### TypeScript Patterns\n\n- Use `interface` for object shapes, `type` for unions/intersections\n- Prefer explicit typing over `any` (ESLint enforces `fixToUnknown`)\n- **NEVER leave unused variables or parameters** - ESLint `unused-imports/no-unused-vars` will fail the Netlify build. The only allowed unused arg pattern is a single underscore `_`. Do NOT use `_prefixedNames` (e.g., `_foo`) - either use the variable or remove it from the signature entirely.\n- Use generic constraints for reusable components\n- Export types from dedicated files in `@/lib/types`\n\n### Styling Conventions\n\n- **Primary approach**: Tailwind CSS utility classes\n- **Component variants**: Use `tailwind-variants` (`tv`) for new and refactored work. Existing `class-variance-authority` (`cva`) components don't need bulk migration -- swap to `tv` opportunistically when you're already touching the component for another reason.\n- **Dynamic classes**: Use `cn()` utility (clsx + tailwind-merge)\n- **Custom properties**: CSS variables in `src/styles/` for theme values\n- **Responsive design**: Mobile-first approach\n\nFor UI work, see the **`design-system` skill** at `.claude/skills/design-system/`. It's the canonical knowledge base for component choices, design tokens, RTL/i18n, server/client boundaries, and the \"use a variant, not a new component\" pattern.\n\n## Development Workflows\n\n### Available Scripts\n\n```bash\n# Development\npnpm dev                    # Start development server (Turbopack)\npnpm dev:webpack            # Start development server with webpack\npnpm build                  # Build for production (Turbopack)\npnpm build:webpack          # Build for production with webpack\npnpm start                  # Start production server\n\n# Code Quality\npnpm lint                   # Run ESLint\npnpm lint:fix              # Fix ESLint issues\npnpm type-check            # TypeScript type checking\npnpm format                # Format with Prettier\n\n# Testing\npnpm test:unit             # Playwright unit tests (unit project)\npnpm test:e2e              # Playwright end-to-end tests\npnpm test:visual           # Playwright + Chromatic full-page visual tests\n\n# Storybook\npnpm storybook             # Start Storybook dev server\npnpm build-storybook       # Build Storybook\npnpm chromatic             # Storybook Chromatic (needs CHROMATIC_STORYBOOK_TOKEN)\n\n# Content Management\npnpm lint:md               # Lint English markdown content\npnpm lint:md:fix           # Auto-fix header IDs and duplicates\n```\n\n### Testing Strategy\n\n- **Unit Testing**: Playwright `unit` project (`pnpm test:unit`) — heaviest coverage is the intl-pipeline sanitizer\n- **E2E Testing**: Playwright (`pnpm test:e2e`), see `docs/e2e-testing.md`\n- **Visual Testing**: Storybook + Chromatic for component regression; Playwright + Chromatic for full pages (see the `page-visual-tests` skill)\n- **Type Safety**: TypeScript strict mode enabled\n- **Linting**: ESLint with custom rules for imports and TypeScript\n\n## Content Management\n\n### Internationalization\n\n- **25 languages** supported (canonical list: `i18n.config.json`); **RTL support** for Arabic, Urdu\n- JSON UI strings in `src/intl/[locale]/`; translated markdown content in `public/content/translations/[locale]/`\n- Non-English markdown is propagated by the **intl-pipeline** (`src/scripts/intl-pipeline/`, entry `main.ts`). **Do not hand-propagate English changes into non-English files** -- let the pipeline run, or trigger `intl-pipeline.yml` with `stamp_only: true` if manifests must catch up urgently (e.g. unblocking a build). Hand-fixing a translation error is fine when the English side hasn't moved, since the manifest mapping stays valid. Spec: `tests/specs/PIPELINE-SPEC.md`.\n- Glossary: base URL from `GLOSSARY_API_URL` env var; default in `src/scripts/intl-pipeline/config.ts`. ETHGlossary is authoritative for Ethereum term translations.\n\nFor pipeline mechanics, recovery, manifests, ETHGlossary integration, and the `intl/pending-{base}` orchestration model, see the **`intl-pipeline` skill** at `.claude/skills/intl-pipeline/`. For translation-quality review (scoring rubric, language-group rules, ETHGlossary-as-authority policy, multi-agent role split), see the **`intl-review` skill** at `.claude/skills/intl-review/`.\n\n### Markdown Content\n\n- Educational content stored in `public/content/`\n- Processed with `next-mdx-remote`\n- Custom MDX components for rich content\n- Automatic table of contents generation\n- **All h1-h4 headings require a custom `{#lower-kebab-id}`** -- enforced by markdownlint via pre-commit hook. Run `pnpm lint:md:fix` to auto-add missing IDs. Config: `.markdownlint-cli2.jsonc`, custom rules: `.markdownlint-rules/`\n\n### Asset Management\n\n- Images optimized with Next.js Image component\n- SVGs loaded as React components via `@svgr/webpack`\n- Static assets served from `public/`\n- Placeholder generation for images\n\n## SEO & Meta\n\n- Sitemap generation in `app/sitemaps/sitemap.ts` (one sitemap per locale, indexed by `app/sitemap.xml/route.ts`)\n- Meta tags and Open Graph optimization\n- Structured data for search engines\n- Security headers (X-Frame-Options: DENY)\n\n## Development Guidelines\n\n### When Working on Features\n\n1. **Check existing patterns** - Look at similar components first\n2. **Prioritize Server Components** - Use App Router and Server Components when possible\n3. **Follow import order** - ESLint will enforce, but be proactive\n4. **Use TypeScript strictly** - No `any` types, prefer `unknown`\n5. **Test in Storybook** - Create stories for new components (filename pattern: `.stories.tsx`)\n6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations(\"common\")`) rather than reaching across namespaces\n7. **Mobile-first** - Design for mobile, enhance for desktop\n8. **Accessibility** - Use Radix primitives, semantic HTML\n9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales.\n\n### Component Development\n\n1. Create component in appropriate `src/components/` subdirectory\n   - Use `src/components/ui` for shadcn components or pure UI components\n2. Add TypeScript types and proper props interface\n3. Accept `ref` as a regular prop when needed (React 19); `forwardRef` only survives in legacy components\n4. Add a Storybook story (see below)\n5. Export from appropriate index file\n6. Update documentation if adding new patterns\n\n### Storybook stories\n\n**Location**: `__stories__/` for flat directories (`ui/`, `styles/`), co-located for foldered components. Story globs are recursive, so nesting works. **Filenames are kebab-case** (`app-card.stories.tsx`) even inside a PascalCase component directory -- enforced by the test below.\n\n**Title**: `Section / Group / Name`. The section is derived from the file path -- not a judgment call:\n\n| Path                                  | Section         |\n| ------------------------------------- | --------------- |\n| `src/styles/**`                       | `Design System` |\n| `src/components/ui/**`                | `UI`            |\n| `src/components/**` (everything else) | `Components`    |\n| `src/layouts/**`                      | `Layouts`       |\n| `app/**`                              | `Pages`         |\n\nUse a second-level group only when an established one fits (Actions, Forms, Layout, Navigation, Overlays, Data Display, Cards, Heroes, Site Chrome, Content, Data Viz, Features). **Don't invent a group for a single component** -- a flat `Components / Morpher` is correct and preferred. `tests/unit/storybook/story-titles.spec.ts` enforces the section prefix, explicit titles, and title uniqueness (Storybook silently merges duplicate titles into one sidebar entry).\n\n**Don't add a story for a deprecated component or variant.** A rendered example reads as an endorsement whatever the caption says, so the showcase should only contain things that are available to use -- note the deprecation in prose instead (see `HR`). A component whose future is merely undecided can keep its story with a status blurb (see `Carousel`).\n\nAdd `tags: [\"autodocs\"]`. Write the `parameters.docs.description.component` blurb for someone deciding whether to use the thing -- constraints and gotchas, not a restatement of the props table.\n\n**A green build does not mean a story renders.** `pnpm build-storybook` compiles stories without executing them, and every `UI /` story sets `chromatic: { disableSnapshot: true }`, so neither CI signal catches a story that throws. Open new stories in `pnpm storybook` before pushing.\n\nAsync server components render via a `next-intl/server` shim (`.storybook/next-intl-server.tsx`). Components calling `usePathname` need `parameters.nextjs.navigation.pathname`. New i18n namespaces must be added to the `ns` array in `.storybook/next-intl.ts`, or strings render as raw key tails.\n\n### Content Updates\n\n1. Markdown files go in `public/content/`\n2. Images in `public/images/` with descriptive names\n3. Translation strings in appropriate `src/intl/` JSON files\n4. Data files in `src/data/` with TypeScript types\n\n### Type-Safe Chain Names\n\nThis project enforces type-safe chain names via TypeScript. When working with layer 2 networks or wallet data:\n\n**Critical Files:**\n\n- `src/data/chains.ts` - Canonical source of all chain names (auto-updated weekly)\n- `src/lib/types.ts` - Defines `ChainName` type derived from chains.ts\n- `src/data/networks/networks.ts` - Uses `chainName: ChainName`\n- `src/data/wallets/wallet-data.ts` - Uses `supported_chains: ChainName[]`\n\n**Rules:**\n\n1. **Always look up exact names** - Before adding `chainName` or `supported_chains`, search `chains.ts` for the exact `name` value\n2. **Names are case-sensitive and exact** - e.g., use `\"Zircuit Mainnet\"` not `\"Zircuit\"`, use `\"OP Mainnet\"` not `\"Optimism\"`\n3. **Run type checking** - Use `pnpm type-check` to verify chain names are valid before committing\n4. **Non-EVM chains** - For Starknet and other non-EVM chains, use `NonEVMChainName` type\n\n**Common Mistakes:**\n\n- Using informal names: `\"Optimism\"` should be `\"OP Mainnet\"`\n- Missing \"Mainnet\" suffix: `\"Zircuit\"` should be `\"Zircuit Mainnet\"`\n- Wrong casing: `\"zksync Mainnet\"` should be `\"zkSync Mainnet\"`\n\n## Key Dependencies to Know\n\n### UI & Styling\n\n- `@radix-ui/*` - Accessible component primitives\n- `tailwind-variants` - Component variant patterns\n- `motion` - Animation library (formerly framer-motion; import from `motion/react`)\n- `lucide-react` - Icon library\n\n### Content & Data\n\n- `gray-matter` - Frontmatter parsing\n- `recharts` - Data visualization\n\n### Ethereum Integration\n\n- `viem` - Ethereum library\n- `wagmi` - React hooks for Ethereum\n- `@rainbow-me/rainbowkit` - Wallet connection\n\n## A/B Testing\n\nThe site uses a GDPR-compliant, cookie-less A/B testing system built on the Flags SDK precompute pattern, integrated with Matomo. The proxy assigns variants at the edge (header fingerprinting) and rewrites to signed, statically prerendered variant pages under `ab-code/[code]/` — experiments never force dynamic rendering. Experiment status, weights, and scheduling live in the Matomo dashboard (no deploys needed). Only the default locale is tested; other locales always get the original.\n\nKey gotcha: **variants are matched by array index, not names** — the `variants` array order must match the Matomo experiment order exactly, and the `testKey` must match the Matomo experiment name exactly.\n\nFull guide (architecture, step-by-step recipe, env vars): `docs/ab-testing.md`. Code: `proxy.ts`, `src/lib/ab-testing/`, `src/components/AB/`, `app/[locale]/ab-code/`.\n\n## Site Search\n\nSearch runs on a self-hosted Typesense instance, one collection per locale, crawled by DocSearch from production ethereum.org. Publishing is gated: a crawl goes to a staging collection and the live alias only moves if the new index passes size, field and relevance checks.\n\nTwo things bite most often. **Ranking hints reach the index as `docsearch:*` meta tags** from `src/lib/utils/metadata.ts`, so a ranking change does nothing until the site is deployed and re-crawled. And **the networks offered as block explorer results come from `src/data/networks/networks.ts`** -- the same list `/layer-2/networks` renders -- so adding an L2 there is the only edit needed.\n\nOverview: `docs/site-search.md`. Runbook, secrets and recovery: `typesense/README.md`. Code: `src/components/Search/`, `src/scripts/typesense/`, `typesense/`.\n\n## Deployment\n\n- **Platform**: Netlify (config in `netlify.toml`)\n- **Next.js Integration**: Uses `@netlify/plugin-nextjs` for seamless Netlify and Next.js compatibility\n- **Monitoring**: Matomo analytics integration\n\n### Runtime constraints\n\n- **`public/content` is not in the Netlify server function** (CDN-only). A page that reads it at render time must stay static (`revalidate: false` getters like `getStaticAppsData`) or have an `outputFileTracingIncludes` entry in `next.config.js`. A finite-`revalidate` getter (e.g. `getEventsData`, 24h) turns the whole route into ISR. Details: `docs/solutions/integration-issues/netlify-isr-404-async-server-components.md`.\n\n## Internal Infrastructure\n\nThe following external-looking services are managed by the ethereum.org team:\n\n- **`s3-dcl1.ethquokkaops.io`** — S3-compatible object storage for app screenshots and media. Used by the data layer to serve images for the `/dapps` and app listing pages. Downtime here means broken images on the live site.\n- **Netlify Blobs** (`@netlify/blobs`) — Key-value store used by the data layer to cache API responses. Accessed via `src/data-layer/storage.ts`.\n",".claude/commands/netlify-build-check.md":"# Command: /netlify-build-check\n\nPath: `.claude/commands/netlify-build-check.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/netlify-build-check.md)",".claude/commands/prepare-release.md":"# Command: /prepare-release\n\nPath: `.claude/commands/prepare-release.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/prepare-release.md)",".claude/commands/review-release.md":"# Command: /review-release\n\nPath: `.claude/commands/review-release.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/review-release.md)",".claude/commands/review-translations.md":"# Command: /review-translations\n\nPath: `.claude/commands/review-translations.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/review-translations.md)",".claude/skills/design-system/evals/evals.json":"# Evals Skill\n\nPath: `.claude/skills/design-system/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/evals/evals.json)",".claude/skills/design-system/references/a11y.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/a11y.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/a11y.md)",".claude/skills/design-system/references/callout-walkthrough.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/callout-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/callout-walkthrough.md)",".claude/skills/design-system/references/canonical-imports.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/canonical-imports.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/canonical-imports.md)",".claude/skills/design-system/references/card-walkthrough.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/card-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/card-walkthrough.md)",".claude/skills/design-system/references/cleanup-playbook.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/cleanup-playbook.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/cleanup-playbook.md)",".claude/skills/design-system/references/components.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/components.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/components.md)",".claude/skills/design-system/references/gotchas.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/gotchas.md)",".claude/skills/design-system/references/i18n-rtl.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/i18n-rtl.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/i18n-rtl.md)",".claude/skills/design-system/references/layouts.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/layouts.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/layouts.md)",".claude/skills/design-system/references/new-component-checklist.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/new-component-checklist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/new-component-checklist.md)",".claude/skills/design-system/references/page-hero-walkthrough.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/page-hero-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/page-hero-walkthrough.md)",".claude/skills/design-system/references/server-vs-client.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/server-vs-client.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/server-vs-client.md)",".claude/skills/design-system/references/spacing-typography.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/spacing-typography.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/spacing-typography.md)",".claude/skills/design-system/references/tokens.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/tokens.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/tokens.md)",".claude/skills/design-system/references/variant-vs-new.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/variant-vs-new.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/variant-vs-new.md)",".claude/skills/intl-pipeline/evals/evals.json":"# Evals Skill\n\nPath: `.claude/skills/intl-pipeline/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/evals/evals.json)",".claude/skills/intl-pipeline/references/architecture.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/architecture.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/architecture.md)",".claude/skills/intl-pipeline/references/ethglossary.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/ethglossary.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/ethglossary.md)",".claude/skills/intl-pipeline/references/gotchas.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/gotchas.md)",".claude/skills/intl-pipeline/references/manifests.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/manifests.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/manifests.md)",".claude/skills/intl-pipeline/references/non-english-edits.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/non-english-edits.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/non-english-edits.md)",".claude/skills/intl-pipeline/references/orchestration.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/orchestration.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/orchestration.md)",".claude/skills/intl-pipeline/references/recovery.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/recovery.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/recovery.md)",".claude/skills/intl-pipeline/references/sanitizer.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/sanitizer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/sanitizer.md)",".claude/skills/intl-review/evals/evals.json":"# Evals Skill\n\nPath: `.claude/skills/intl-review/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/evals/evals.json)",".claude/skills/intl-review/references/agent-roles.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/agent-roles.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/agent-roles.md)",".claude/skills/intl-review/references/critical-vs-warning.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/critical-vs-warning.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/critical-vs-warning.md)",".claude/skills/intl-review/references/ethglossary-usage.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/ethglossary-usage.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/ethglossary-usage.md)",".claude/skills/intl-review/references/gotchas.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/gotchas.md)",".claude/skills/intl-review/references/known-patterns.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/known-patterns.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/known-patterns.md)",".claude/skills/intl-review/references/language-rules.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/language-rules.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/language-rules.md)",".claude/skills/intl-review/references/per-language-tracking.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/per-language-tracking.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/per-language-tracking.md)",".claude/skills/intl-review/references/scoring-rubric.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/scoring-rubric.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/scoring-rubric.md)",".claude/skills/performance/references/build.md":"# References Skill\n\nPath: `.claude/skills/performance/references/build.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/build.md)",".claude/skills/performance/references/bundle.md":"# References Skill\n\nPath: `.claude/skills/performance/references/bundle.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/bundle.md)",".claude/skills/performance/references/diagnose-table.md":"# References Skill\n\nPath: `.claude/skills/performance/references/diagnose-table.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/diagnose-table.md)",".claude/skills/performance/references/edge-caching.md":"# References Skill\n\nPath: `.claude/skills/performance/references/edge-caching.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/edge-caching.md)",".claude/skills/performance/references/images.md":"# References Skill\n\nPath: `.claude/skills/performance/references/images.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/images.md)",".claude/skills/performance/references/inp.md":"# References Skill\n\nPath: `.claude/skills/performance/references/inp.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/inp.md)",".claude/skills/performance/references/review-checklist.md":"# References Skill\n\nPath: `.claude/skills/performance/references/review-checklist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/review-checklist.md)",".claude/skills/review-page/reviewers/copy.md":"# Reviewers Skill\n\nPath: `.claude/skills/review-page/reviewers/copy.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/reviewers/copy.md)",".claude/skills/review-page/reviewers/design-system.md":"# Reviewers Skill\n\nPath: `.claude/skills/review-page/reviewers/design-system.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/reviewers/design-system.md)",".claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md":"# Runbooks Skill\n\nPath: `.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md)",".claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md":"# Runbooks Skill\n\nPath: `.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md)"},"files":{".claude/skills/new-page/SKILL.md":"---\nname: new-page\ndescription: Interview-then-scaffold guide for adding a new page to ethereum.org. Forks first on markdown-backed vs App Router page, then picks the layout and hero, maps the Figma design to existing components, and builds it reuse-first. Invoke with /new-page.\ndisable-model-invocation: true\n---\n\n# New Page\n\nA new ethereum.org page is **reuse-first**: every region maps to a layout, hero, primitive, or variant that already exists — you *configure* what's there, you don't build a parallel version of it. This skill interviews you for the missing context, then scaffolds the page against the design system.\n\nWork the steps in order. Each ends on a checkable condition — don't start the next until the current one is met.\n\n## Step 0 — Load the design system\n\nInvoke the `design-system` skill and read its `SKILL.md` fully before asking anything else. It is the source of truth for component choices, tokens, layouts, RTL/i18n, and the server/client boundary. **Do not restate its catalog here** — when you need the layout inventory read `design-system/references/layouts.md`, and verify it against `src/layouts/index.ts` (layouts get refactored).\n\n**Done when:** `design-system` SKILL.md has been read this session.\n\n## Step 1 — Interview\n\nThe primary fork is **markdown-backed or not**. It decides the layout system, the build, and how the page is translated — so settle it first. It is an *authoring* decision (how the content is written and maintained), **not** a visual one: a hero-topped prose page can be either, because `ContentLayout` gives markdown `TopicLayout` and App Router pages the same hero + card-TOC chrome. So **ask it; never infer it from the design.**\n\nUse `AskUserQuestion`:\n\n1. **Markdown page or App Router page?**\n   - *Markdown page* — content authored in `public/content/<route>/index.md`, rendered through `[...slug]`, translated by the intl-pipeline, built from a fixed set of MDX shortcodes. Choose for editorial/educational content a writer maintains in markdown.\n   - *App Router page* — a hand-built `app/[locale]/<route>/page.tsx` that composes `ContentLayout`. Choose when the page is interactive, data-backed, or needs layout/components the markdown shortcodes can't express. Translated via `getTranslations` JSON keys.\n2. **Route** — the URL path (e.g. `/stablecoins/`). The folder name *is* the route.\n3. **Design source** — a Figma link/selection, a written description, or none.\n4. **Content** (markdown only) — does the user have the body ready, or should you draft it?\n\nThen **check the route is free**: no existing `public/content/<route>/index.md` or `app/[locale]/<route>/page.tsx`. If one exists you're *editing*, not creating — surface it and confirm before any write; never silently overwrite a page you didn't author.\n\n**Done when:** markdown-vs-App-Router, route, and design source are known, and the route is confirmed free (or editing is okayed).\n\n## Step 2 — Pull the design (only if Figma was provided)\n\nRead the design **fresh** with the Figma MCP (designs change between runs — never rely on a read from an earlier turn): `get_metadata` for the frame tree, then `get_design_context` / `get_screenshot` on each region; `search_design_system` / `get_code_connect_map` for existing code mappings. Load the `figma-use` skill before any `use_figma` call.\n\nProduce a **component map** and **always show it to the user as a table** — three columns: **Region** (each design region, top to bottom) · **Maps to** (the existing primitive + variant that renders it — `PageHero`, `Card`, `BigNumber`, …) · **Status** (`reuse`, `missing asset`, `placeholder copy`, `missing data`, …). Walk the frame top to bottom and map **every** region. The table is the user's only window into what you saw in the frame — render it every run; never just reason about the mapping internally.\n\n**Node names lie.** A node called `Screenshot 2026-…` or `Frame 1789` is routinely real content — a stat band (`BigNumber`), a video embed (`YouTube`), a callout (`Alert`) — not a throwaway. Never omit a region because its name *looks* like a draft; `get_screenshot` it and see what it actually is before deciding.\n\n**Missing data → ask, don't guess.** When a region needs a value the design doesn't carry — a YouTube ID behind a thumbnail, a real destination URL, a live data source — you can't extract it from the frame. Flag it and ask the user; a guessed embed or invented link is worse than an open question.\n\n**Match, don't mimic.** Reproduce the design with existing components and their variants. Do **not** layer custom Tailwind/CSS on a primitive to chase pixel parity. If a region has no fitting primitive or variant, the answer is *add a variant* (`design-system/references/variant-vs-new.md`) — never a one-off component or bespoke styles. Flag any such gap and confirm before inventing.\n\n**Pressure-test the authoring fork.** The map can reveal a region markdown shortcodes can't express — an interactive widget, a data-backed list, a bespoke multi-column grid, or cards with **lucide** icons (`MarkdownCard` accepts `emoji` *or* `icon: ReactNode`, but a plain `.md` file can't author a ReactNode, so from markdown the `<Card>` shortcode is effectively emoji-only). Any of these means the page wants to be App Router. If Step 1 said markdown, surface the conflict and re-confirm before scaffolding — flipping it later is a rewrite.\n\n**Done when:** the component-map table has been shown to the user and they've confirmed or corrected it; every region is accounted for — mapped, flagged for an approved variant, or raised as missing data — and the authoring fork still holds.\n\n## Step 3 — Pick the layout and hero\n\nBranches on Step 1.\n\n### Markdown page — pick the `template:`\n\nThe layout is selected by the `template:` frontmatter value (default `static`). Full inventory in `design-system/references/layouts.md`; live set in `src/layouts/index.ts`.\n\n| `template:` | Layout | Use for | Hero it renders |\n|---|---|---|---|\n| `static` (default) | `StaticLayout` | one-off editorial prose, no sub-nav | title + breadcrumbs only (`PageHero variant=\"no-divider\"`, **no image**) |\n| `use-cases` / `staking` / `roadmap` / `upgrade` | `TopicLayout` | topic hub: full side-image hero + sibling sub-nav | image `PageHero` from frontmatter `image` / `summary` / `buttons` |\n| `docs` | `DocsLayout` | developer docs with the docs sidebar | — |\n| `tutorial` | `TutorialLayout` | developer tutorial with author/date/skill metadata | — |\n\n- Each Topic template carries a distinct **MDX component list** (`componentsMapping`, `src/layouts/index.ts`) — pick the one whose shortcodes match the content.\n- **Side-image hero in the design → a Topic template** (`static` renders only a title hero). `showDropdown`, new-topic wiring, and the never-add-a-new-layout rule: `design-system/references/layouts.md`.\n\n### App Router page — compose `ContentLayout`, pass the hero that fits\n\nHand-build `app/[locale]/<route>/page.tsx` composing `ContentLayout`, and choose the hero from the design:\n\n- `PageHero` — breadcrumbs + title + optional `heroImg` + description + buttons. The workhorse; covers both image and title-only heroes.\n- `HubHero` — large hub hero (exemplar: `/learn/`).\n\nBuild the `tocItems` array by hand and wrap each prose section in `<Section id>`. `ContentLayout` owns the card TOC, contributors block, and feedback. Exemplars: `/learn/`, `/what-is-ethereum/`.\n\n**Done when:** markdown → `template:` (and `showDropdown` if relevant) chosen; App Router → hero component chosen. Confirmed with the user.\n\n## Step 4 — Scaffold\n\nBuild from the Step 2 component map. Reuse-first throughout — no raw `<a>`/`<button>`, no hex colors, logical CSS props (`ms-`/`me-`/…), locale-aware `numberFormat()`/`dateTimeFormat()`. Icons: **lucide in `.tsx`, emoji in markdown** (markdown can't pass the `icon` ReactNode prop).\n\n**Markdown page:**\n- Create `public/content/<route>/index.md` with frontmatter (`title`, `description`, `lang: en`, plus `template:` unless static). English only — never hand-write translated copies; the intl-pipeline propagates them.\n- **Inline images co-locate** next to `index.md`, referenced relatively — a `/images/...` inline path 500s the MDX render; the frontmatter `image:` hero is the exception. Details: `design-system/references/gotchas.md`.\n- Every h1–h4 needs a `{#kebab-id}`; run `pnpm lint:md:fix`.\n- If using `TopicLayout`: add `src/data/topics/<key>.ts`, wire `layoutMapping`/`componentsMapping` in `src/layouts/index.ts`, and add `src/intl/en/page-<key>.json`.\n\n**App Router page:**\n- Create `app/[locale]/<route>/page.tsx` composing `ContentLayout` (mirror `/learn/` or `/what-is-ethereum/`): pass `heroSection`, the hand-built `tocItems`, and `contributors`; wrap prose in `<Section id>`. Server Component unless it needs state/effects/handlers.\n- All user-facing strings via `getTranslations`; add keys to `src/intl/en/`.\n\n**Done when:** the page renders through its layout with content/strings in place and no inlined reinventions of existing primitives.\n\n## Step 5 — Verify (static checks)\n\n- `pnpm type-check` (catches invalid chain names and type errors).\n- Walk the design-system **Pre-Merge Smoke Test** checklist.\n- Add a `.stories.tsx` for any genuinely new UI primitive.\n\n**Done when:** type-check passes and the smoke checklist is clean.\n\n## Step 6 — QA via the adversarial review loop\n\nStatic checks pass on pages that are visibly broken — a wrong image path, a failed MDX compile, a section that silently didn't render. So the page must be run and audited via the **adversarial reviewer loop**, not an eyeball comparison.\n\n1. **Run it.** Start `pnpm dev`, wait for \"Ready\", load the page (locale-stripped for `en`: `/<route>/`). Confirm **HTTP 200** — a 500 is almost always an MDX or asset error; read the dev log (a non-co-located inline image is the classic one). There is nothing to review on a broken page.\n2. **Run the review loop.** Read `review-page/SKILL.md` and execute its Steps 1–5 against this route until green. You already hold the design source and route from Steps 1–2, so its Step 0 (running page + known design source) is already satisfied. Read and run that procedure; don't try to invoke `/review-page` as a skill from here.\n   - **A hero finding is a template bug, not a CSS gap.** When the design-system reviewer flags a title-only hero against a side-image design: on a markdown page that's a wrong-`template:` (use a Topic template, not `static`); on an App Router page, pass `heroImg` to `PageHero`. Never rebuild a hero with custom markup.\n3. **Hand off.** Leave the dev server running and give the user the local URL the server printed (e.g. `http://localhost:3000/<route>/`).\n\n**Done when:** the review loop is green (or hits its round cap with the remaining findings handed off), and the user has the live URL.\n",".claude/skills/design-system/SKILL.md":"---\nname: design-system\ndescription: Use when building, refactoring, or styling any UI in the ethereum.org Next.js site (`src/components/`, `app/`, `src/styles/`, `public/content/`, or any `.tsx`/`.mdx`/`.css` change that affects the rendered UI). Provides canonical component choices, design tokens, RTL/i18n rules, server/client guidance, and the \"use a variant, not a new component\" pattern for the project's Tailwind v4 + Radix + shadcn-style design system.\n---\n\n# ethereum.org Design System\n\nTailwind v4 (CSS-first config, no `tailwind.config.ts`) + React 19 / Next.js App Router + Radix UI primitives + shadcn-style component layer. Tokens live in CSS. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Habit: Reuse Over Reinvent\n\nThe single highest-leverage habit for keeping this codebase consistent: **when you need new UI, look for a primitive or variant first, only invent if nothing fits.** Most \"new component\" instincts are actually \"new variant\" instincts in disguise.\n\nBefore you write any UI code, ask:\n- Is there a primitive that already does this? (`Card`, `Button`, `Alert`, `Tag`, `Hero/*`)\n- Is the difference small enough to express as a *variant* on an existing primitive?\n- Can I compose existing primitives instead of inlining a long Tailwind class chain?\n\nIf you find yourself writing `flex items-center gap-X rounded-Y border bg-... p-Z` for a card-like thing, you're reinventing `<Card>`. If you write `<p className=\"text-4xl font-bold\">N</p>` for a stat, you're reinventing `<BigNumber>`. If you write `<div className=\"text-5xl font-bold\">Title</div>`, you're reinventing `<h1>` (which is already styled by `base.css`). **Compose, don't inline.**\n\nWhen the existing primitive doesn't quite fit, the answer is usually \"add a variant,\" not \"create a new file.\" See `references/variant-vs-new.md`.\n\n## Top Rules\n\n1. **No raw `<a>` or `<button>`.** Use `<Button>`/`<ButtonLink>` from `@/components/ui/buttons/Button` and `InlineLink`/`BaseLink`/`LinkWithArrow` from `@/components/ui/Link`. These primitives handle event tracking, external-link safety, locale routing, and focus rings.\n2. **No raw color values.** Use semantic tokens (`text-body`, `bg-background`, `border-border`, `text-primary`). Hex literals and `rgb()` calls bypass dark mode.\n3. **Prefer adding a variant** to an existing primitive over creating a new component. Card, Button, Alert, Tag are the most common targets.\n4. **Server Components by default.** Only `\"use client\"` when you need state, effects, browser APIs, or inline event handlers.\n5. **All text is translatable.** `getTranslations` from `next-intl/server` (server) or `useTranslations` from `next-intl` (client). One namespace-bound `t` per namespace -- bind a second function (e.g. `const tCommon = useTranslations(\"common\")`) to access another namespace. The legacy `@/hooks/useTranslation` wrapper is deprecated for new code. Never hard-code user-facing English. **In an `app/[locale]/` page or `generateMetadata`, call `setRequestLocale(locale)` before any next-intl API** or on-demand renders throw `static to dynamic ... reason: headers` -- see `references/i18n-rtl.md`.\n6. **Logical CSS for direction.** Use `ms-`/`me-`/`ps-`/`pe-`/`inset-s-`/`inset-e-`/`border-s`/`border-e`/`text-start`/`text-end`. The site supports Arabic and Urdu (RTL). Hard-coded `left-`/`right-`/`ml-`/`mr-`/`pl-`/`pr-` breaks RTL.\n7. **Locale-aware formatters.** `numberFormat()` from `@/lib/utils/numbers`, `dateTimeFormat()` from `@/lib/utils/date`. Never `toLocaleString` / `Intl.NumberFormat` directly.\n8. **`useRtlFlip()` for directional icons** (right-pointing arrows/chevrons). Or use `ChevronNext`/`ChevronPrev` from `@/components/Chevron`.\n9. **Markdown content goes through `MdComponents`.** The `<Card>` markdown shortcode is backed by `@/components/MarkdownCard` (a thin wrapper around the `ui/card` primitives with an MDX-friendly prop shape). For app code, compose the primitives directly from `@/components/ui/card`.\n10. **Storybook stories ship with new UI components.** No automated unit tests; Storybook + Chromatic + types are the verification layer.\n11. **Don't add new layouts.** There are six canonical layouts (`TopicLayout`, `StaticLayout`, `DocsLayout`, `TutorialLayout`, `ContentLayout`, `BaseLayout`). New sectioned content goes in `src/data/topics/<key>.ts` as a `TopicLayout` config -- not a new layout component. See `references/layouts.md`.\n\n## Highest-Value Gotchas (index)\n\nLandmines where the code looks reasonable but the pattern is wrong. One line each; the canonical write-ups live in the named reference.\n\n- **Look-alike imports** -- Tooltip (`@/components/Tooltip`, not `ui/tooltip`), Modal (`ui/dialog-modal` vs `ui/dialog`; don't mix sources within a feature), Card (`ui/card` for app code, not `MarkdownCard`), Heroes (named exports of `@/components/Hero`; `title` is always the `<h1>`, no `header` prop). Decision trees: `references/canonical-imports.md`.\n- **Stale shadcn token names** (`bg-popover`, `bg-muted`, `text-muted-foreground`, ...) linger in a few `ui/` files but don't resolve in this project's tokens. Don't add new uses. List: `references/tokens.md`; replacements: `references/cleanup-playbook.md`.\n- **`useColorModeValue` is a deprecated Chakra leftover** -- use `dark:` + semantic tokens.\n- **`Card` is variant-driven, not `className`-driven.** Padding/spacing/background/radius/text color go through `variant`/`size`; a `className` override means a missing variant case. Full system: `references/card-walkthrough.md`. Surprise behaviors (`isSecondary` no-op on `solid`, `CardBanner fit=\"contain\"` auto-backdrop, `LinkBox` needs `LinkOverlay`, `commonControlClasses` shared with `Switch`): `references/gotchas.md`.\n- **No `Heading` primitive** -- `base.css` styles `<h1>`-`<h6>`; just write the tag. To match a heading level's size anywhere, use `text-h1`-`text-h6` (size + line-height only) -- never the raw `text-3xl lg:text-4xl` pair, and never re-apply a weight on a real heading. Canonical write-up: `references/spacing-typography.md`.\n- **Spacing is `.flow` + the `page`/`space`/`hero` tokens**, not hand-rolled margins. App pages follow a `<main className=\"p-page\"> > <MainArticle className=\"flow\"> > <Section id>` skeleton. Details: `references/spacing-typography.md`; token table: `references/tokens.md`.\n- **Locale formatting** -- one stray `toLocaleString` remains in `ui/chart.tsx`; don't add more. Use `numberFormat()`.\n\n### Shadows: default to the Tailwind scale\n\nPick by surface: dropdowns/tooltips `shadow-md`, cards/popovers/modals `shadow-lg`, large framed boxes/sheets `shadow-xl`. Three rules:\n\n1. Only two custom shadows exist (`shadow-primary-xl`, `shadow-primary-no-blur-*`, both in `utilities.css`) -- anything new needs a brand-tint justification.\n2. Hover elevation is the `hover-lift-*` utility or `Card hoverLift` (both `motion-safe`-gated), never a per-component resting/hover shadow pair.\n3. A genuinely-needed custom shadow is a raw `box-shadow` `@utility`, never an arbitrary `shadow-[...]` -- arbitrary shadows route color through `--tw-shadow-color`, which the global `* { dark:shadow-body }` rule grays out in dark mode.\n\nOld-shadow-token mapping table: `references/cleanup-playbook.md`.\n\n## Quick \"Where Do I Import From?\" Cheatsheet\n\n| I need... | Import |\n|---|---|\n| Card | `import { Card, CardBanner, CardContent, CardTitle, CardParagraph } from \"@/components/ui/card\"` |\n| Modal/Dialog (typical) | `import Modal from \"@/components/ui/dialog-modal\"` (default export) |\n| Side sheet | `import { Sheet, ... } from \"@/components/ui/sheet\"` |\n| Tooltip | `import Tooltip from \"@/components/Tooltip\"` (NOT `@/components/ui/tooltip`) |\n| Button | `import { Button, ButtonLink } from \"@/components/ui/buttons/Button\"` |\n| Anchor (in prose) | `import InlineLink from \"@/components/ui/Link\"` (default) |\n| Anchor (CTA with arrow) | `import { LinkWithArrow } from \"@/components/ui/Link\"` |\n| Page hero | `import { PageHero, HubHero } from \"@/components/Hero\"` |\n| Inline alert | `import { Alert, AlertContent, AlertDescription } from \"@/components/ui/alert\"` |\n| Top-of-page banner | `import { Alert } from \"@/components/ui/alert\"` then `<Alert variant=\"banner\">` |\n| Big numeric display | `import BigNumber from \"@/components/BigNumber\"` |\n| Layout | `import { Stack, HStack, VStack, Flex, Center } from \"@/components/ui/flex\"` |\n| Number formatting | `import { numberFormat } from \"@/lib/utils/numbers\"` |\n| Date formatting | `import { dateTimeFormat } from \"@/lib/utils/date\"` |\n| RTL flip helper | `import { useRtlFlip } from \"@/hooks/useRtlFlip\"` |\n\nFor full decision trees with all the look-alike landmines, see `references/canonical-imports.md`.\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies. Don't read them all upfront.\n\n- **`references/canonical-imports.md`** -- unsure which of several look-alike imports is canonical.\n- **`references/components.md`** -- the full component inventory with variants and usage.\n- **`references/tokens.md`** -- adding a token, gradients, z-index, semantic-token choices, work in `src/styles/`.\n- **`references/spacing-typography.md`** -- page/section layout, heading sizes, spacing rhythm, the `.flow` system, citation and footnote markers.\n- **`references/gotchas.md`** -- unexpected primitive behavior; the long-tail confusion patterns.\n- **`references/variant-vs-new.md`** -- before creating any new component file.\n- **`references/cleanup-playbook.md`** -- refactoring existing anti-patterns; the \"old pattern -> new pattern\" map.\n- **`references/i18n-rtl.md`** -- user-facing text, number/date formatting, RTL, translation keys, `setRequestLocale`.\n- **`references/server-vs-client.md`** -- `\"use client\"` decisions and SSR-boundary structure.\n- **`references/a11y.md`** -- interactive elements, forms, images, heading hierarchy.\n- **`references/card-walkthrough.md`** -- any card-shaped UI work.\n- **`references/callout-walkthrough.md`** -- adding or modifying an in-content `Callout`.\n- **`references/page-hero-walkthrough.md`** -- a new page that needs a hero.\n- **`references/layouts.md`** -- layout selection, new topic hubs, one-off layout refactors.\n- **`references/new-component-checklist.md`** -- before opening a PR for a new component.\n\n## Other Project Skills That May Apply\n\n- **`data-layer`** -- For data fetching/sources. UI work that needs data should compose with this.\n\n## Pre-Merge Smoke Test\n\nBefore opening a PR for any UI work:\n\n- [ ] No raw `<a>` or `<button>`\n- [ ] No hard-coded colors (`#hex`, `rgb()`, `hsla()`); semantic tokens only\n- [ ] No `left-`/`right-`/`ml-`/`mr-`/`pl-`/`pr-` (use logical equivalents)\n- [ ] All user-facing strings translatable\n- [ ] `numberFormat()`/`dateTimeFormat()` for formatting (not native APIs)\n- [ ] Server Components wherever possible\n- [ ] New UI primitives have a `.stories.tsx`\n- [ ] Headings use `<h1>`-`<h6>` (not `<div className=\"text-5xl font-bold\">`)\n- [ ] If introducing a new component, justify why it isn't a variant of an existing one\n",".claude/skills/image-gen/SKILL.md":"---\nname: image-gen\ndescription: Generate illustration images (heroes, decorations, section art) for ethereum.org via the OpenRouter image API, then key/trim/check and place them in the repo. Trigger on \"make an image for\", \"generate a hero\", \"new illustration for <page>\", \"fix the glyph\", \"redraw this image\", \"chroma key this\", or edits to `src/scripts/image-gen/`. Not for screenshots, logos, or brand assets.\n---\n\n# Image generation\n\nThe user asks in chat (\"make a hero for /privacy\"); Claude drives the flow below. The script is Claude's tool for the parts that need the key or pixel math: it wraps OpenRouter's `POST /api/v1/images` and the post-processing. Three subcommands:\n\n```bash\npnpm tsx src/scripts/image-gen/main.ts models [--filter gemini]          # live params per model (aspect, resolution, background, n, ref limit)\npnpm tsx src/scripts/image-gen/main.ts generate --type cutout|full-bleed --prompt-file <f> [--ref <img>]... [--model <id>] [--aspect 16:9] [--quality <q>] [--eth-glyph auto|on|off] [--n 1] [--name <slug>] [--out-dir <dir>] [--dry-run]\npnpm tsx src/scripts/image-gen/main.ts process <img> --type cutout|full-bleed --out <file> [--key-color green|magenta] [--pad 0] [--max-size <px>]\n```\n\n## The key\n\n`OPENROUTER_API_KEY` lives in `.env.local`, loaded by the script itself. **Never read `.env.local`** (`.claude/settings.json` denies it; `.env.example` is fine to read). Never echo, log, or pass the key on a command line. Each person uses their own OpenRouter key with a spend limit set.\n\n**If a key ever appears in your context** (tool output, an error body, a file you read), tell the user right away that it was exposed and should be revoked and replaced at openrouter.ai/settings/keys, without repeating the key. Then carry on with the task; rotation happens separately.\n\n## Workflow\n\n1. **Context.** Read the target page and component, then decide the type. If the page doesn't settle it, ask with AskUserQuestion; never guess.\n   - **cutout** -- subject on transparency, nothing touching any edge (`PageHero` `heroImg`, section decorations).\n   - **full-bleed** -- scene fills the frame edge to edge (`HubHero` backgrounds, banners).\n2. **References.** Pick 2-6 existing images that match the target style (look at them first); see **House style** below. More refs pull harder toward house style; mismatched refs muddy it.\n3. **Prompt.** Write the subject/scene and style only. The script appends the cutout or full-bleed constraints (key color, margins, no borders) and, when the prompt mentions Ethereum, the ETH glyph spec and reference (see **The ETH glyph**), so don't restate them.\n4. **Confirm before spending.** Show the prompt, refs, model, aspect, and `--dry-run` output. Generate only after the user agrees, and get a fresh go-ahead for every further call; agreement to one generation isn't agreement to the next. A call costs about $0.11-0.26 and the price follows output pixels (21:9 is the cheapest, 1:1 and 3:2 the most).\n5. **Generate** one image per call: `--n 1`, never higher unless the user asks for a batch. Use a stable kebab `--name` describing the subject (see **Naming** below); reuse it for every iteration on that target, even if the subject drifts. Don't pass `--out-dir`: the default archive, `/tmp/image-gen/<name>/`, sits outside the repo and the session scratchpad, so candidates survive across iterations and sessions. Numbering continues (`-1`, `-2`, ...) and nothing is ever overwritten. Each candidate gets `<name>-<i>-raw.*`, a processed `<name>-<i>.png`, and `<name>-<i>.json` (prompt, refs, params, cost, check results).\n6. **Review.** Look at every output. `CHECK` lines are automatic failures (subject touching an edge, flat border strip, background not a key color); regenerate those rather than placing them. Then zoom in (crop at 3-4x) on every ETH glyph and every structure the user has flagged before, and check them against **The ETH glyph** and **Compositions that fail**. Report defects you find rather than letting the user discover them.\n7. **Place in situ, automatically.** Don't wait for a pick: put the best passing candidate at its target location right away so the user can judge it on the real page in their running dev server.\n   - `process` the archived raw (or `cp` the archived `.png`) to its final path, `public/images/<area>/<descriptive-name>.png`, sized to roughly 2x its largest rendered width with `--max-size`. Cutouts are trimmed to zero margin (`--pad 0`, the default); the page sets the spacing. Run a lossless PNG optimizer (oxipng) on the result.\n   - If the target also uses a portrait version on small viewports (a `*-portrait.png` beside it, e.g. some `HubHero` backgrounds), crop it from the landscape image rather than generating it separately, so both stay identical.\n   - Wire it into the target component (camelCase raster import, see the design-system skill's `page-hero-walkthrough.md`). If it replaces an existing image, repoint the import and leave the old file alone, so reverting is just `git checkout` on the component.\n   - Tell the user the URL to check (e.g. `/organizations/`) and list the other candidates by path.\n   - Never commit, and never start a dev server; the user runs their own.\n8. **Iterate in place.** Each new trial is copied from the archive over the same in-repo file, so the page updates without touching the import again. Static imports pick up the change through their content hash; for a plain `/images/...` string `src`, tell the user to hard-refresh. Revise by adding a candidate as a `--ref` with an updated prompt. To go back to an earlier candidate (\"I prefer the first one\"), copy that archived file in; never regenerate it. Always say which archive number is currently in situ.\n9. **Never delete the archive.** Leave `/tmp/image-gen/<name>/` alone, even after the image is placed. The user decides when it's done. When the user is happy, show `git status` so they can see exactly which files changed.\n\n## Editing an existing image\n\nMost work after the first generation is fixing details. Doing it well:\n\n- **Fix only what the user flagged.** Starting over or \"improving\" unflagged areas changes sizes, colors and objects the user had already accepted, and is usually worse.\n- **Redraw faithfully.** Pass the approved image as the sole (first) `--ref` and open with \"Redraw the reference image faithfully: identical camera, framing, <list every element>, same colors, same linework. Change only <X>: <exact description of the fix>. Everything else stays exactly as it is.\" OpenAI reproduces the rest closely.\n- **Take the whole redraw.** Patching just the fixed region back into the old image fails: a redraw shifts edges by around 10px. If everything else matches, place the full redraw.\n- **Check the neighbors.** Removing or changing one element can break an adjacent one (removing an elevator shaft truncated the next one). Compare the whole image against the previous version before placing.\n- **If the user supplies an edited version, it becomes the source.** Archive it in `/tmp/image-gen/<name>/`, place it, and use it as ref 1 for any later redraw. Never assume the user can edit images; for a detail the model keeps getting wrong, a manual edit can be suggested, not expected.\n\n## House style\n\nethereum.org illustrations are hand-drawn, airy and utopian. Good references: `public/images/home/hero.png` (plus a full-resolution crop of its palms and strokes, which shows the line texture better than the whole image), `public/images/heroes/developers-hub-hero.png`, `learn-hub-hero.png` and `guides-hub-hero.jpg`. Avoid the roadmap, garden and quizzes heroes as references; they lean purple and pink.\n\nCues that work in prompts:\n\n- Lines: hand-drawn in cobalt and royal blue, never black, lighter periwinkle for distant details. Gentle crayon quality: \"lines waver a little in weight and have slightly soft, lightly textured edges, fills carry a faint paper grain; subtle and clean, mostly crisp, not scratchy\". Full crayon wording overshoots into grain the user doesn't want (aim for about 75% clean, 25% texture).\n- Color: mostly white and pale blue with peach, orange and cyan accents; red awnings are fine. Flat fills, no gradients, glow or halos.\n- Architecture: sci-fi, white and rounded, slender columns, curved balconies, glass as pale blue panels with thin white grid lines, thin peach accent bands, rooftop plants. Classical forms (columns, pediments, domes) stay but in that white style. No brick or boxy present-day buildings.\n- Ground: white or pale-blue tiles; floating slab edges in pale periwinkle, not brown earth.\n- Plants: palms with feathery fronds of thin cobalt and periwinkle strokes, in white planters. No green, no round deciduous trees.\n- People: simple outlined figures, flat-colored clothes, simple hair, low detail.\n- Always add \"No text, letters, numbers or currency symbols anywhere, no logos other than the Ethereum diamond, no UI labels.\" Clock faces get no numbers.\n\n## The ETH glyph\n\nImage models get the Ethereum diamond wrong more than anything else: one solid rhombus, a white fill or a line bridging the gap, an extra lattice of facets, a glyph rotated off the surface it sits on, or a smudge on a small coin. The **shape** must match `src/components/icons/eth-home-icon.svg` exactly. Its **colors don't matter**: they can change to suit the design (a coin imprint in coin tones, a white glyph on a pediment).\n\nThe shape, for checking outputs and describing fixes (the script's reference renders it as unfilled outlines for this reason):\n\n- Two separate pieces stacked vertically, with an open gap between them where the background shows through.\n- Upper piece: a tall kite with exactly four facets. Its side corners sit about halfway down the whole glyph and its bottom point about two-thirds down. A center line runs top to bottom, and a line from each side corner rises to meet it about a third of the way down.\n- Lower piece: a downward chevron with exactly two facets. Its top edge is a shallow V parallel to the upper piece's bottom edges, and its tips sit just below the upper piece's corners.\n- About 1.6 times as tall as wide, symmetric, the same width top and bottom. On a tilted or foreshortened surface it follows that surface's perspective and keeps the same structure.\n\nHow the script helps: with `--eth-glyph auto` (the default), any prompt that mentions Ethereum, ETH, a glyph or a diamond gets the detailed shape spec (`src/scripts/image-gen/eth-glyph.ts`) appended and a rendering of the canonical glyph attached as the **last** reference image. User refs keep their order, so \"the first reference image\" still means yours; never call the glyph anything but \"the last reference image\" in your own prompt. Use `--eth-glyph on` to force it, `off` when the image has no glyph and the prompt only names Ethereum in passing, or when the reference slot is needed.\n\nIn prompts, also:\n\n- Ask for few glyphs, each drawn large. A glyph below about 15px in the output, or lying on foreshortened paper, is unreadable at any prompt fidelity; leave it off small coins and badges.\n- Say where each glyph sits and which way it faces (\"upright and centered at the top of the certificate\", \"lying flat in the center of the floor square, aligned with its edges\").\n- Don't ask for glows, highlights or \"dimensional\" facets on it; they turn into extra lines inside the shape.\n- To fix a wrong glyph on an otherwise approved image, redraw faithfully (see **Editing an existing image**) with \"Change only the Ethereum diamond on <where>\" and its position, size and facing; the spec and reference supply the shape.\n\n## Compositions that fail\n\nThese cost several failed generations each. Design them out in the prompt instead of fighting them:\n\n- **Merging or branching rail tracks:** the model can't draw a clean switch, and \"hide it under a train\" produced two misaligned trains. Use a single continuous track.\n- **Hard occlusion:** legs under tables, columns behind foreground objects, a bridge running behind a floating object. Use a solid-front desk or counter that reaches the floor, a single colonnade, fewer overlapping layers.\n- **Repeated structure:** column rows, colonnades and dome drums come out unevenly spaced. Spell it out (\"exactly four identical columns at exactly equal spacing on the same base line\") and check spacing at zoom.\n- **Impossible layering:** floors that don't continue behind objects, staircases that loop. Keep depth simple: one island, one level change.\n\n## Naming\n\nName images for what they depict, never for where they're used: a town square with people used as the `/some-page/` hero is `town-square-buildings-with-people.png`, not `some-page-hero.png`. Placement changes; content doesn't. Kebab-case, specific enough to tell it apart from neighbors in the same folder. The `--name` passed to `generate` follows the same rule, so the archive folder and the final file match. If the final image ends up depicting something different from the original name, rename the in-repo file to match what's in the image (the archive folder keeps its name).\n\n## Models\n\nDefault `openai/gpt-image-2.5-sunburst` at the highest `quality` it offers. It matches the house style (clothing, palms, architecture, ground, people, flat color) where `google/gemini-3-pro-image` (Nano Banana Pro) kept drawing black vector lines, purple and red casts and ordinary buildings despite explicit prompts. Switch with `--model` only when the user asks; run `models` to see what a model accepts -- the script validates flags against that list and drops options a model lacks.\n\n- **Cutouts:** OpenAI supports `background: transparent`, so the script requests real alpha and skips keying. Models without it get a flat key background that the script removes.\n- **Size:** OpenRouter exposes no size parameter; output tops out around 1536px on the long side (21:9 comes back 1536x656). For a wide hero that needs more, a local Real-ESRGAN upscale reads better on the page than the raw 1536px, though it flattens some crayon texture.\n- `n` is 1 per call on Gemini; `--n 3` makes three calls. Models that accept a higher `n` batch it.\n\n## Chroma keying\n\nOnly for models without native alpha, or for chroma images made elsewhere.\n\n- Green (`#00FF00`) by default. Use `--key-color magenta` when the subject itself is green or teal; a green subject keyed on green turns partly transparent.\n- Alpha comes from how strongly a pixel leans toward the sampled border color, then the background is unmixed from partially transparent pixels. Despill applies only to a 2px rim near transparency, so interior colors are untouched.\n- Prefer PNG sources. JPEG compression smears the key into dark outlines, which leaves a faint tinted rim.\n- `process` works on any existing chroma image, e.g. one generated by hand on the OpenRouter site.\n",".claude/skills/intl-pipeline/SKILL.md":"---\nname: intl-pipeline\ndescription: Use when working on the translation pipeline (`src/scripts/intl-pipeline/`), the post-import sanitizer (`intl-sanitizer.ts`), the `intl/pending-*` branches, the `intl-pipeline.yml` GitHub Actions workflow, ETHGlossary integration, or any translation-related issue (broken translations, missing locales, manifest drift, sanitizer false positives, ETHGlossary term lookups, language-group transliteration questions). Provides the manifest-driven incremental pipeline mental model, the \"don't hand-propagate\" rule, recovery procedures, the ETHGlossary-as-canonical-source policy, and the `intl/pending-{base}` orchestration contract.\n---\n\n# intl-pipeline\n\nLLM-based, manifest-driven, incremental translation pipeline for ethereum.org. Translates English content (markdown in `public/content/`, JSON UI strings in `src/intl/en/`) into 24 target languages via Gemini — reached either directly or through OpenRouter (`LLM_PROVIDER`), whose keys carry a server-side spend limit — with a post-import sanitizer normalizing common artifacts. **The workflow is manual-dispatch only**; the daily cron was removed after a run billed $1,108 unattended (`references/runbooks/cost-guard-tripped.md`). ETHGlossary is the authoritative source for all Ethereum-ecosystem term translations consumed by the pipeline. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Rule: Don't Hand-Propagate English Changes\n\nThe single highest-leverage habit for keeping translations correct: **never hand-edit translated content to reflect an English change.** The pipeline tracks state via manifests (`.manifests/{destPath}/source.json` + `translation.json` per file+locale); hand-edits that follow an English change desynchronize the manifest from reality, and the next pipeline run either re-translates over your edit or produces merge conflicts.\n\nThe rule is NOT \"never edit locales.\" It IS \"don't hand-propagate English updates\":\n\n- **Allowed:** Fixing a translation error when the English side hasn't moved (review-time corrections, sanitizer follow-ups). The manifest's English-to-locale map stays valid.\n- **Not allowed:** Editing a locale to reflect a new English value (URL change, attribute change, restructured paragraph). The manifest map becomes wrong.\n- **If English-to-locale sync is genuinely urgent** (build-breaking structural change with no pending PR open): make the English edit, then trigger `intl-pipeline.yml` with `stamp_only: true` to refresh manifests without translation. Safe only when no `intl/pending-{base}` branch exists for that base.\n\n**Deletions go through English too.** To remove a string — including an orphaned key code no longer references — delete it from the English source only; the pipeline propagates the removal on its next run. Hand-deleting the locale copies is hand-propagation.\n\n## Top Rules\n\n1. **ETHGlossary is canonical for term translations.** Brand names, people's names, programming languages, OS/platform names, concept terms — all live in ETHGlossary. Don't maintain parallel term banks. The pipeline queries the API via `GLOSSARY_API_URL` (default in `src/scripts/intl-pipeline/config.ts`).\n2. **One translation PR at a time per base branch.** The pipeline commits to `intl/pending-{base}` (e.g., `intl/pending-dev`). Subsequent runs merge `{base}` into pending first, then translate the delta. Parallel translation PRs against the same base will conflict.\n3. **The pipeline only targets `dev` in production.** Hot fixes to `staging` / `master` go out English-only and catch up via prepare-release. Don't translate against `staging` / `master` unless you have a specific reason and use a custom `target_branch`.\n4. **Manifests are inseparable from their locale file.** Each translated file has two manifests, centralized under `.manifests/{destPath}/`: `source.json` and `translation.json`. Delete one, you must regenerate both — easiest via the pipeline in `full` mode for that file+locale. Never hand-edit a manifest.\n5. **The sanitizer runs post-translation, not pre.** Its job is to fix Gemini-introduced artifacts (BiDi mistakes, code-fence drift, brand-name mistranslations). It receives translation outputs, never English source.\n6. **Don't add transliteration data here.** All term/brand/person transliteration policy lives in ETHGlossary's `docs/translation-policy.md` and per-language entries. The intl-pipeline consumes; it does not author.\n7. **Sanitizer fixes must split on code blocks first.** Every text transformation in `intl-sanitizer.ts` MUST start with the code-block split pattern. Modifying code-fence contents breaks Solidity / Python / TypeScript examples in tutorials.\n8. **An untagged code fence is code.** Fence bodies never reach the model; only a prose-tagged fence (```` ```text ````, `txt`, `md`, `markdown`, `mdx` -- `PROSE_FENCE_TAGS` in `lib/shared-patterns.ts`) is translatable. To make a fence's prose translatable, tag it ```` ```text ```` in the English source. `code-fence-content` fails any other fence whose body drifts from English, comments excepted.\n9. **Pipeline failures are not always pipeline bugs.** A \"translation looks wrong\" report may be: bad Gemini output (file upstream), missing ETHGlossary term (add there), correct per language-group policy (read translation-policy.md), or an actual pipeline bug. Triage before patching.\n\n## Highest-Value Gotchas\n\nThese are landmines where the obvious-looking action is wrong. The full set is in `references/gotchas.md`; these come up most often.\n\n### Naming legacy\n\nOld names are dead — `src/scripts/i18n/`, `gemini-translations.yml`, `main-incremental.ts`, \"i18n pipeline\"; the canonical name everywhere is `intl-pipeline`. Update old names on sight.\n\n### Manifests are centralized under `.manifests/`, not next to the locale file\n\nTwo manifests per file+locale at `.manifests/{destPath}/source.json` + `translation.json` (e.g. `.manifests/src/intl/ja/common.json/source.json`). Structure, lifecycle, and debugging: `references/manifests.md`.\n\n### Spend bounds are input-side, and the tell is input tokens per call\n\nHealthy runs average 4k–9k input tokens per call; the input:output ratio is NOT a signal. Before merging anything that touches batching, prompt assembly, or context selection, run `pnpm intl:estimate` and compare per-request input tokens against that range. Bounds, triage, and the incident that motivated them: `references/runbooks/cost-guard-tripped.md`.\n\n### `intl/pending-{base}` branch lifecycle\n\nThe pipeline creates `intl/pending-{base}` on first run, commits translations to it, opens a PR against `{base}`. Subsequent runs MERGE `{base}` into pending first, then translate the delta. After the pending PR is merged, the branch is deleted; the next run creates a fresh one.\n\nDo NOT rebase, squash, or force-push `intl/pending-{base}`. The pipeline depends on its history.\n\n### SOV-language inline element reordering\n\nFor Korean, Urdu, and other SOV (subject-object-verb) languages, inline elements (links, inline code, JSX components) often appear in REVERSE order vs. English. The pipeline's inert-propagation pass matches elements by **value**, not by position. Any logic that assumes positional order will silently corrupt those languages.\n\n### JSX attribute translation is a separate pass\n\nJSX attribute values are NOT translated in the main Phase 4 LLM call. Phase 4b is a dedicated pass with an allow-list of translatable attribute names (`title`, `description`, `alt`, `label`, `aria-label`, `placeholder`, etc.) defined in `src/scripts/intl-pipeline/lib/shared-patterns.ts`. Touching attribute translation means touching that pass, not Phase 4.\n\n### The sanitizer runs inside each task, before the gates\n\nSince the gates landed, `runSanitizer` is called per task on that task's own output (`sanitizeAndGate` in `main.ts`), then the gates judge the sanitized bytes, then content and manifest are recorded together. There is no post-squash sanitize commit any more; a sanitizer change is exercised on the next run's tasks, not on a branch sweep.\n\n### Sanitizer test scope is per-file, never per-language sweep\n\nNEVER run the sanitizer against an entire language. It processes thousands of files and hangs for 30+ minutes. There is no per-file env var — the only env-based scoping is per-language via `TARGET_LANGUAGES` (e.g. `TARGET_LANGUAGES=ja`), which still sweeps that whole language. Scope to specific files programmatically by calling the exported `runSanitizer(filesWithContent)` from `intl-sanitizer.ts` with just the affected file(s). The slash command `/fix-sanitizer-bug` enforces this; if you script around it, preserve the constraint.\n\n## Quick \"Where Do I Look?\" Cheatsheet\n\n| I need...                               | Path                                                                                  |\n| --------------------------------------- | ------------------------------------------------------------------------------------- |\n| Pipeline entry                          | `src/scripts/intl-pipeline/main.ts`                                                   |\n| Sanitizer                               | `src/scripts/intl-pipeline/intl-sanitizer.ts`                                         |\n| Gemini adapter                          | `src/scripts/intl-pipeline/lib/llm/gemini.ts`                                         |\n| OpenRouter transport                    | `src/scripts/intl-pipeline/lib/llm/openrouter.ts`                                     |\n| Adapter registry / provider selection   | `src/scripts/intl-pipeline/lib/llm/adapters.ts`, `constants.ts`                       |\n| Spend bounds (budget + run fuse)        | `src/scripts/intl-pipeline/lib/llm/cost-meter.ts`                                     |\n| Pre-commit gates (structure, MDX, JSON) | `src/scripts/intl-pipeline/lib/gates.ts`                                              |\n| Deterministic-failure quarantine        | `src/scripts/intl-pipeline/lib/quarantine.ts`, `.manifests/quarantine.json`           |\n| Work planner (shared with estimate)     | `src/scripts/intl-pipeline/lib/llm/plan.ts`                                           |\n| Cost estimate, no LLM calls             | `pnpm intl:estimate` (`src/scripts/intl-pipeline/estimate.ts`)                        |\n| Structural gate, standalone CLI         | `src/scripts/intl-pipeline/verify-structure.ts` (checks in `lib/verify-structure.ts`) |\n| One-off repairs (never run by a run)    | `migrate-manifests.ts`, `repair-anchors.ts` -- see \"One-off repair scripts\"           |\n| Prompt builder                          | `src/scripts/intl-pipeline/lib/llm/prompt-builder.ts`                                 |\n| Content normalizer                      | `src/scripts/intl-pipeline/lib/llm/content-normalizer.ts`                             |\n| Shared patterns (JSX attrs allow-list)  | `src/scripts/intl-pipeline/lib/shared-patterns.ts`                                    |\n| Glossary config                         | `src/scripts/intl-pipeline/config.ts`                                                 |\n| Workflow file                           | `.github/workflows/intl-pipeline.yml`                                                 |\n| Per-file pipeline spec (canonical)      | `tests/specs/PIPELINE-SPEC.md`                                                        |\n| Concurrency / chunking spec             | `tests/specs/CONCURRENCY-SPEC.md`                                                     |\n| Test fixture mutation table             | `tests/specs/SPEC.md`                                                                 |\n| Sanitizer test suite                    | `tests/unit/intl-pipeline/sanitizer/`                                                 |\n| Pipeline test suite                     | `tests/unit/intl-pipeline/`                                                           |\n| Future-work backlog                     | `src/scripts/intl-pipeline/FUTURE.md`                                                 |\n| Language config (canonical list)        | `i18n.config.json`                                                                    |\n| ETHGlossary repo                        | https://github.com/ethereum/ethglossary                                               |\n| ETHGlossary API root                    | https://glossary.ethereum.org                                                         |\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies. Don't read them all upfront.\n\n- **`references/architecture.md`** — debugging pipeline behavior; the phase-by-phase walkthrough.\n- **`references/manifests.md`** — manifests misbehave, or changing what the pipeline tracks.\n- **`references/orchestration.md`** — working with `intl/pending-{base}` branches.\n- **`references/recovery.md`** — translations broken, triaging \"the pipeline did something wrong.\"\n- **`references/sanitizer.md`** — sanitizer behavior, false positives/negatives, fix-function catalog.\n- **`references/runbooks/fix-sanitizer-bug.md`** — confirmed sanitizer bug; when (not) to run `/fix-sanitizer-bug`.\n- **`references/runbooks/cost-guard-tripped.md`** — `[cost-guard]` aborts, budget skips, cost-bound design, provider selection.\n- **`references/ethglossary.md`** — term lookups, `script_rule`/`term_role` semantics, missing terms.\n- **`references/non-english-edits.md`** — about to hand-edit a translated file.\n- **`references/gotchas.md`** — something feels off and it isn't inline above.\n\n## Other Project Skills That May Apply\n\n- **`intl-review`** — For translation quality review (scoring rubric, language-group rules, brand-name policies, per-language findings). The review side of the same coin; the pipeline produces, intl-review evaluates.\n- **`data-layer`** — For data fetching, if pipeline changes touch externally-sourced content.\n\n## Pre-Merge Smoke Test\n\nBefore opening a PR that touches the pipeline:\n\n- [ ] Sanitizer test suite passes (`npx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/`)\n- [ ] Pipeline test suite passes (`npx playwright test --project=unit tests/unit/intl-pipeline/`)\n- [ ] No hand-edits to `public/content/translations/` or `src/intl/{non-en}/` files\n- [ ] No manual changes to manifest files under `.manifests/`\n- [ ] If renaming workflow / config paths, all doc references updated (search for old names)\n- [ ] If adding pipeline-affecting code, `src/scripts/intl-pipeline/FUTURE.md` updated (or item removed if completed)\n- [ ] If new sanitizer fix function, the code-block-split pattern is the first operation inside it\n- [ ] If touching term policy or transliteration, change goes in ETHGlossary first — never duplicate term data here\n- [ ] If touching the orchestration model (pending branch, temp branch, stamp_only), `references/orchestration.md` updated alongside\n- [ ] If touching batching, prompt assembly, or context selection: `pnpm intl:estimate` run and per-request input tokens sane (4k–9k), plus `tests/unit/intl-pipeline/cost-incident.spec.ts` passing\n- [ ] No new LLM call site that bypasses `callGeminiRaw` — it is the single choke point where the per-call ceiling and run fuse are enforced\n",".claude/skills/data-layer/SKILL.md":"---\nname: data-layer\ndescription: This skill provides patterns for working with the data-layer module. Use when creating/editing files in src/data-layer/, src/lib/data/, or adding new data sources.\n---\n\n# Data Layer\n\n## Architecture\n\n```\nsrc/data-layer/\n├── fetchers/         # Fetch functions (one per data source)\n│   └── developer-tools/  # Multi-file fetcher (builder resources, GitHub/npm stats, ranking)\n├── index.ts          # Public API - typed getter functions\n├── tasks.ts          # Trigger.dev scheduled tasks\n├── storage.ts        # KEYS constant + get/set (Netlify Blobs or mock files)\n├── s3.ts             # S3 image upload utility for external images\n├── docs.md           # Module documentation\n├── mocks/            # Mock data files for local development\n└── .env.example      # Environment variables for data-layer/Trigger.dev\n\nsrc/lib/data/\n└── index.ts          # Next.js caching adapter (createCachedGetter)\n```\n\n## Environment Variables\n\nThe data-layer uses a **dedicated `.env.local`** at `src/data-layer/.env.local`, separate from the root `.env.local`: `cp src/data-layer/.env.example src/data-layer/.env.local`, fill in the keys (see `.env.example` for all options), then `pnpm trigger:dev` runs tasks locally. `GITHUB_TOKEN_READ_ONLY` and Sentry vars are shared with the main app (configure in both files); everything else (API keys, Netlify Blobs tokens, S3 credentials, Trigger.dev config) is data-layer only. In production, configure vars in the Trigger.dev project dashboard — the app and data-layer run in separate environments.\n\n## Key Files\n\n`storage.ts` defines the `KEYS` constant, `tasks.ts` the `WEEKLY`/`DAILY`/`HOURLY` task tuples; `index.ts` holds the one-liner getters — snippets for both under \"Adding a New Data Source\" below.\n\n### storage.ts - Storage Abstraction\n\n`get<T>(key)` / `set(key, data)` switch between Netlify Blobs (prod) and local mock JSON files (`USE_MOCK_DATA=true` for local development).\n\n### s3.ts - Image Upload Utility\n\nCentralized S3 upload for external images. Fetchers use this to upload external images to a single S3 bucket, reducing Next.js `remotePatterns` complexity.\n\n```typescript\n// Upload single image\nconst s3Url = await uploadToS3(sourceUrl, \"events/logos\")\n\n// Batch upload (parallel)\nconst s3Urls = await uploadManyToS3(urls, \"apps/banners\")\n```\n\nKey features:\n\n- **SSRF protection** - Blocks private/internal network addresses\n- **Deduplication** - SHA256 hash of source URL as key\n- **Existence check** - Skips if already uploaded\n- **5MB size limit** - Returns `null` for large images\n- **Content-Type detection** - From header or URL extension fallback\n\n## Rules\n\n### 1. Getters must be pure passthrough\n\nNo transformations in `index.ts` - just `get<T>(KEYS.X)`:\n\n```typescript\n// Correct\nexport const getEventsData = () => get<EventItem[]>(KEYS.EVENTS)\n\n// Wrong - no transformations in getters\nexport const getEventsData = () => {\n  const data = await get<EventItem[]>(KEYS.EVENTS)\n  return data?.map(transform) ?? null\n}\n```\n\nAll transformations belong in the fetcher (`src/data-layer/fetchers/`).\n\n### 2. KEYS is the single source of truth\n\nAll task IDs are defined in `KEYS` in `storage.ts`. The getter in `index.ts` and the task tuple in `WEEKLY`/`DAILY`/`HOURLY` must use the same key.\n\n### 3. Expose via lib/data for caching\n\nAdd cached wrapper in `src/lib/data/index.ts`:\n\n```typescript\nexport const getEventsData = createCachedGetter(\n  dataLayer.getEventsData,\n  [\"events-data\"],\n  CACHE_REVALIDATE_DAY // or CACHE_REVALIDATE_HOUR\n)\n```\n\nThe `revalidate` parameter is `number | false`. Passing `false` is a deliberate pattern to keep a route fully static — a finite revalidate opts the page into ISR, which fails on Netlify for pages reading `public/content/` files. Example: `getStaticAppsData` in `src/lib/data/index.ts`, used by components embedded in MDX pages (data refreshes only on deploy).\n\n### 4. Use S3 for external images\n\nExternal images should be uploaded to S3 in the fetcher to centralize image domains:\n\n```typescript\n// In fetcher - correct\nimport { uploadToS3 } from \"../s3\"\n\nconst logoUrl = await uploadToS3(event.logoImage, \"events/logos\")\nreturn { ...event, logoImage: logoUrl ?? \"\" }\n```\n\nAlways handle `null` returns (upload failures) with fallback/empty string.\n\n### 5. Keep fetchers isolated from the app\n\nFetchers run on Trigger.dev — a separate runtime, deployment, and bundle from the Next.js app. They cannot assume the app's filesystem, environment, or modules are available.\n\nAny import or runtime dependency reaching outside `src/data-layer/` is a warning sign. Allowed: types (`@/lib/types`, `@/lib/interfaces`), pure constants (`@/lib/constants`), and pure utility functions with no app-runtime dependencies. Not allowed: anything that reads `process.cwd()`, anything from `app/` or `public/`, anything from `src/components/`, or `src/lib/data/` (which wraps the data layer and would create a cycle).\n\nIf a fetcher needs data that lives in the app — content files, frontmatter, etc. — fetch it over the network via the GitHub API and treat the repo as an external system. See `fetchGitHubContributors.ts` for the pattern. Don't work around this with `additionalFiles` in `trigger.config.ts`; bundling app files into the data-layer deployment re-creates the coupling.\n\n## Adding a New Data Source\n\n1. **Create fetcher** in `src/data-layer/fetchers/fetchNewData.ts`:\n\n   ```typescript\n   export async function fetchNewData(): Promise<YourDataType> {\n     // Fetch and transform data here\n   }\n   ```\n\n2. **Add key** to `KEYS` in `src/data-layer/storage.ts`:\n\n   ```typescript\n   export const KEYS = {\n     // ...existing keys\n     NEW_DATA: \"fetch-new-data\",\n   } as const\n   ```\n\n3. **Add task tuple** to `WEEKLY`, `DAILY`, or `HOURLY` in `tasks.ts` (getter and tuple must use the same `KEYS` entry — Rule 2):\n\n   ```typescript\n   const DAILY: TaskDef[] = [\n     // ...existing tasks\n     [KEYS.NEW_DATA, fetchNewData],\n   ]\n   ```\n\n4. **Add getter** in `src/data-layer/index.ts`:\n\n   ```typescript\n   export const getNewData = () => get<YourDataType>(KEYS.NEW_DATA)\n   ```\n\n5. **Add mock file** at `src/data-layer/mocks/fetch-new-data.json` (read when `USE_MOCK_DATA=true`)\n\n6. **Add cached wrapper** in `src/lib/data/index.ts`:\n   ```typescript\n   export const getNewData = createCachedGetter(\n     dataLayer.getNewData,\n     [\"new-data\"],\n     CACHE_REVALIDATE_HOUR\n   )\n   ```\n",".claude/skills/intl-review/SKILL.md":"---\nname: intl-review\ndescription: Use when reviewing translation imports/PRs (especially against `intl/pending-*` branches), evaluating translation quality across any of the 24 target languages, triaging issues in `.claude/translation-review/`, scoring a locale's translation, running the `/review-translations` slash command, debugging brand-name mistranslations or glossary deviations, or working with the per-language findings tracking files. Covers the scoring rubric, ETHGlossary-as-authority policy, language-group rules, agent-role split (structural / terminology / semantic), the concept-tag-vs-brand-tag distinction in frontmatter, and the critical-vs-warning severity policy.\n---\n\n# intl-review\n\nTranslation-quality review for ethereum.org's 24-language pipeline output. Reviews target the LLM (Gemini)-produced translations on `intl/pending-{base}` branches plus any historical Crowdin imports still in flight. ETHGlossary (https://glossary.ethereum.org) is the authoritative source for terminology — deviations are **critical issues**, not warnings. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Rule: ETHGlossary Is Authority\n\nWhen evaluating a translated brand name, person name, programming language, OS name, or any Ethereum-ecosystem term, **the ETHGlossary entry for that term in that language is the truth**. Deviations are **critical** issues that must be flagged (and auto-fixed when running `/review-translations` without `--no-fix`).\n\nThis is not a stylistic preference — it's a determinism guarantee. The translation pipeline queries ETHGlossary; reviewers verify the output matches. If you think the glossary is wrong, the fix is to update ETHGlossary (https://github.com/ethereum/ethglossary), not to leave the translation as-is.\n\nUse the `/filter` endpoint to get the subset of glossary terms that actually appear in a given English source file. Don't reason from memory about brand names; look them up.\n\n## Top Rules\n\n1. **Glossary deviations are CRITICAL, not warnings.** Any time a translated term differs from the ETHGlossary entry for that language, that's a critical issue. The review report must list them so Phase 5 can auto-fix.\n2. **Internal hrefs must stay English.** `/governance` in Spanish content must be `/governance`, not `/gobernanza`. Translated URLs break navigation. Same for anchor IDs (`#section-id`).\n3. **Concept tags translate; brand-name tags don't.** Tutorial frontmatter `tags:` arrays contain a mix. Brand names (`\"solidity\"`, `\"hardhat\"`, `\"alchemy\"`) stay English. Concept/category tags (`\"smart contracts\"`, `\"testing\"`, `\"security\"`) are intentionally translated by the pipeline and must NOT be reverted to English.\n4. **Code blocks: functional code stays English; comments may be translated.** Identifiers, strings, config keys, console output — English. Code comments (`//`, `/* */`, `#`) may be translated to aid reader comprehension.\n5. **Brand-name policy is per-language, per-term.** Some brands stay Latin in CJK (Solidity, Hardhat per ETHGlossary's `script_rule: always_latin`); some transliterate (Ethereum → イーサリアム per `transliterate`); some have native calques in `zh`/`zh-tw`. Use the glossary; don't pattern-match.\n6. **MDX syntax errors are CRITICAL (build-breaking).** Raw `<` before numbers, unclosed backticks, orphaned closing tags, JSX attributes with embedded unescaped quotes — any of these breaks the build for that locale. Flag and fix.\n7. **Reporting zero critical issues is a valid outcome.** Don't invent issues to \"show your work.\" If a thorough review surfaces no criticals, report `0 critical, N warnings` (or `0/0`) and that's a valid result.\n8. **Auto-fix is opt-out, not opt-in.** `/review-translations` applies fixes by default. Use `--no-fix` for review-only runs (GitHub Actions context, no commit auth). The review report still lists what would have been fixed.\n9. **All review changes go on the `intl/pending-*` branch being reviewed — NEVER `dev`.** Translation fixes AND knowledge-base updates (`known-patterns.md`, `per-language/{lang}.md`) are committed to that review's own `intl/pending-{base}` branch (the `/review-translations` worktree) so they ride with the PR. If you catch yourself editing `.claude/translation-review/` in the `dev` checkout, move the changes to the review branch and restore `dev`. Sequence: commit → push → submit review (`--approve` once the fix is pushed and no criticals remain, else `--comment`).\n\n## Highest-Value Gotchas\n\nThese are landmines where the obvious-looking call is wrong. The full set is in `references/gotchas.md`.\n\n### Frontmatter `tags` arrays mix two policies\n\nTutorial frontmatter `tags:` mix brand-name tags (stay English) with concept/category tags (intentionally translated — do NOT revert). Rule of thumb: proper noun / product name → English; generic descriptive term → translated form is right. Details and examples: `references/known-patterns.md`.\n\n### Semantic inversions on consensus terms\n\nHigh-frequency real-world failure mode: Gemini occasionally swaps proof-of-stake and proof-of-work, or mistranslates \"mainnet\" as \"market\" or \"main network.\" These are critical semantic errors, not warnings. Spot-check consensus, mainnet, testnet, validator, miner, client when reviewing.\n\n### Cross-script contamination\n\nOld Crowdin imports sometimes have characters from one script leaking into another (Devanagari in Turkish, CJK in Arabic). Less common with the new pipeline but still possible. Flag any unexpected-script characters; they indicate translation memory leaks.\n\n### Lorem ipsum / placeholder text in JSON\n\nReal translation values sometimes contain \"Lorem ipsum dolor sit amet\" or similar placeholder text. Treat as a critical user-visible issue.\n\n### Translated GitHub `@username` handles\n\n`@axic` becoming `@kwaaxic` (Swahili) or similar — broken attribution. Flag.\n\n### Asymmetric backticks and orphaned HTML tags\n\nSingle-open / double-close backticks (`` `text`` ``), missing `</em>`before`</li>`, raw `</a>` without opener — all break MDX compilation. Critical.\n\n## Quick \"Where Do I Look?\" Cheatsheet\n\n| I need...                              | Path                                                                                                               |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |\n| Slash command (full pipeline)          | `/review-translations` (`.claude/commands/review-translations.md`)                                                 |\n| Issue-pattern catalog                  | `.claude/translation-review/known-patterns.md` + `references/known-patterns.md`                                    |\n| Per-language rules                     | `references/language-rules.md` (sourced from `.claude/translation-review/localization-rules-by-language-group.md`) |\n| Scoring rubric                         | `references/scoring-rubric.md`                                                                                     |\n| Per-language findings                  | `.claude/translation-review/per-language/{lang}.md`                                                                |\n| ETHGlossary terms (filtered to source) | `POST /api/v1/filter`                                                                                              |\n| ETHGlossary terms (full per-language)  | `GET /api/v1/translations/{lang}`                                                                                  |\n| ETHGlossary policy                     | https://github.com/ethereum/ethglossary/blob/main/docs/translation-policy.md                                       |\n| Sanitizer source                       | `src/scripts/intl-pipeline/intl-sanitizer.ts`                                                                      |\n| Sanitizer test research                | `docs/solutions/integration-issues/sanitizer-test-research.md`                                                     |\n| Past review post-mortems               | `docs/solutions/integration-issues/` + `docs/solutions/logic-errors/`                                              |\n| Language config                        | `i18n.config.json`                                                                                                 |\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies.\n\n- **`references/known-patterns.md`** — Load when starting any review. The catalog of recurring issue patterns (brand mistranslations, MDX syntax errors, semantic inversions, cross-script contamination, etc.).\n- **`references/language-rules.md`** — Load when working on a specific non-Latin-script language (ar, bn, hi, ja, ko, mr, ru, ta, te, uk, ur, zh, zh-tw). Per-language-group rules for prose, UI tags, numerals, code, people's names.\n- **`references/scoring-rubric.md`** — Load when scoring a PR or producing a quality report. The 5-category rubric (Brand Name Preservation, Technical Accuracy, Semantic Fidelity, Terminology Consistency, Tone/Register) with weighting.\n- **`references/ethglossary-usage.md`** — Load when integrating ETHGlossary lookups into a review (which endpoint, when to use `/filter` vs `/translations/{lang}`, how to handle missing terms, what `script_rule` and `term_role` mean for reviewers).\n- **`references/per-language-tracking.md`** — Load when writing to or reading from `.claude/translation-review/per-language/{lang}.md`. The convention for accumulating findings across reviews.\n- **`references/gotchas.md`** — Load when something feels off and you can't find it inline. The long tail of confusion patterns.\n- **`references/critical-vs-warning.md`** — Load when deciding whether to flag an issue as critical (auto-fix) or warning (manual review). The severity rubric.\n- **`references/agent-roles.md`** — Load when planning a multi-agent review (structural / terminology / semantic). The split and what each role focuses on.\n\n## Other Project Skills That May Apply\n\n- **`intl-pipeline`** — For the pipeline side: how translations are produced, manifest invariants, sanitizer behavior, the `intl/pending-{base}` model. When a review surfaces a pipeline-level bug (not a translation-level one), reach for this skill.\n- **`design-system`** — When a translation issue is rendering-related (BiDi flip, RTL spacing, layout overflow from longer non-English strings).\n\n## Pre-Merge Smoke Test\n\nBefore submitting a translation-quality review:\n\n- [ ] ETHGlossary `/filter` endpoint was called for at least one file per language (don't review from memory)\n- [ ] All critical issues listed (glossary deviations, MDX syntax errors, brand mistranslations, translated hrefs, semantic inversions)\n- [ ] Frontmatter `tags` distinction respected (brand tags English, concept tags translated)\n- [ ] Code-block policy applied (functional code English, comments may be translated)\n- [ ] Per-language quality score produced in the 5-category rubric\n- [ ] Per-language findings file updated at `.claude/translation-review/per-language/{lang}.md`\n- [ ] If new issue pattern surfaced, added to `references/known-patterns.md` (or `known-patterns.md` in the KB)\n- [ ] All review changes (fixes + knowledge-base updates) committed to the `intl/pending-*` branch under review and pushed — never left on `dev`\n- [ ] Build verification (`--build-local` or `--netlify-check`) run if the review touched MDX-syntax-related issues\n",".claude/skills/page-visual-tests/SKILL.md":"---\nname: page-visual-tests\ndescription: Playwright + Chromatic full-page visual tests for ethereum.org. Trigger on \"add a page to the visual suite\", \"the snapshot keeps changing\", \"chromatic pages\", \"chromatic playwright\", or edits to `tests/visual/`, `playwright.visual.config.ts`, or the `page-visual-tests` job in `.github/workflows/ci.yml`. Skip for Storybook Chromatic (`.github/workflows/chromatic.yml`), e2e (`tests/e2e/`), unit (`tests/unit/`).\n---\n\n# Page Visual Tests (Playwright + Chromatic)\n\nThis skill covers the **page visual tests** Chromatic project only (the `page-visual-tests` job in ci.yml publishes via `chromaui/action`; local publish is `pnpm chromatic:pages` with `CHROMATIC_PAGES_TOKEN`) — not Storybook Chromatic.\n\nThe Playwright suite captures DOM archives (not PNGs) per page × viewport; Chromatic re-renders them in the cloud to diff. A green local `pnpm test:visual` just means archives were produced — the diff happens after upload.\n\n## Files that matter\n\n- `playwright.visual.config.ts` — visual-only config (3 viewports + `webServer`)\n- `playwright.config.ts` — base (e2e + unit; **no `webServer`**)\n- `tests/visual/pages.spec.ts` — page list + readiness pattern\n- `.github/workflows/ci.yml` — CI (the `page-visual-tests` job)\n- `src/components/ui/skeleton.tsx`, `src/components/ui/spinner.tsx` — loading primitives\n- `package.json` scripts: `test:visual*`, `chromatic:pages`\n\n## Non-obvious constraints\n\n**Dual Playwright config.** `webServer` lives only in `playwright.visual.config.ts`. Moving it into the base config breaks `pnpm test:unit` and `pnpm test:e2e` in CI — they try to start Next against a missing `.next` build.\n\n**Desktop viewport is 1024, not 1280.** Chromatic caps snapshots at `width × height ≤ 25M` px. The tallest tested pages reach ~22.5k px; 1280 overflows, 1024 fits. Measure `document.documentElement.scrollHeight` before raising the viewport or adding a long page.\n\n**Loading contract: `data-slot=\"loading\"`.** The shared `Skeleton` and `Spinner` primitives carry this attribute. Each test waits until `document.querySelectorAll('[data-slot=\"loading\"]').length === 0` before snapshotting. Any bespoke loader — raw `animate-pulse-light`, a local Skeleton copy, a custom spinner — is invisible to the wait and will silently flake. Fix by routing through the shared primitive or adding `data-slot=\"loading\"` to the bespoke loader's root.\n\n**Imports come from `@chromatic-com/playwright`, not `@playwright/test`.** The two packages re-export `expect` with skewed types, so `expect(...).toHaveCount(0)` misbehaves — prefer `page.waitForFunction` for the loading wait.\n\n**Environment.** `USE_MOCK_DATA=true` and `NEXT_PUBLIC_BUILD_LOCALES` are required at build and test time. The local `test:visual:build` script sets `NEXT_PUBLIC_BUILD_LOCALES=en`; CI's shared build in ci.yml uses `NEXT_PUBLIC_BUILD_LOCALES: \"en,es,zh,ar\"` because one build artifact serves the e2e, lighthouse, and visual jobs. Paths in the spec are unprefixed (`/wallets/`, not `/en/wallets/`) because `localePrefix: \"as-needed\"` serves English at the root — adding `/en` would just trigger a redirect.\n\n**Random ordering: `safeShuffle`.** Lodash `shuffle` and `.sort(() => Math.random() - 0.5)` flake snapshots independently of loaders. Wrap them with `safeShuffle` from `src/lib/utils/random.ts` — it returns the list unchanged when `IS_VISUAL_TEST=true`. Current call sites: `src/lib/utils/wallets.ts`, `src/lib/utils/apps.ts` (Highlights/Discover/AppOfTheWeek), `src/components/Staking/StakingProductsCardGrid/index.tsx`. The env var is exposed to the client bundle via `next.config.js`'s `env` block; without that, `process.env.IS_VISUAL_TEST` evaluates to `undefined` in client components and the shuffle still runs.\n\n**Use `domcontentloaded`, not `networkidle`.** Analytics and background fetches keep the network perpetually busy.\n\n## Canonical test\n\n```ts\nimport { takeSnapshot, test } from \"@chromatic-com/playwright\"\n\nconst pages: Array<{ name: string; path: string }> = [\n  { name: \"Homepage\", path: \"/\" },\n  { name: \"Docs - Smart Contracts\", path: \"/developers/docs/smart-contracts/\" },\n  // ...\n]\n\ntest.describe(\"Page Visual Tests\", () => {\n  for (const { name, path } of pages) {\n    test(name, async ({ page }, testInfo) => {\n      await page.goto(path, { waitUntil: \"domcontentloaded\" })\n      await page.waitForFunction(\n        () => document.querySelectorAll('[data-slot=\"loading\"]').length === 0\n      )\n      // FeedbackWidget is dynamic({ ssr: false }); waiting for its button proves\n      // hydration finished and dynamic chunks landed — same pattern other\n      // ssr:false components (Emoji/Twemoji) rely on to render.\n      await page.waitForSelector('[data-testid=\"feedback-widget-button\"]')\n      await takeSnapshot(page, testInfo)\n    })\n  }\n})\n```\n\n## Common situations\n\n**Adding a page.** Each entry costs three snapshots (one per viewport) against Chromatic's budget, so check whether the page's layout (under `src/layouts/`) is already covered before adding. Scan the page subtree for bespoke loaders — they're the single biggest flake cause — and confirm full-page height stays under the 25M-pixel budget. Local loop: `pnpm test:visual:build` once, then `pnpm test:visual:desktop` for iteration, `pnpm test:visual` for the full sweep.\n\n**Flaky snapshot.** Two main causes. (1) A loader without `data-slot=\"loading\"` — run with `--trace=on` and inspect the `waitForFunction` step; ~0 ms duration means it isn't being waited on. (2) Random ordering — grep the page subtree for `shuffle(`, `Math.random()`, or `.sort(() =>` and route through `safeShuffle`. If dynamic content is drifting, double-check `USE_MOCK_DATA=true` is set in both build and test steps.\n\n**Local `pnpm dev` masks a regression.** `playwright.visual.config.ts` sets `reuseExistingServer: true`, which is correct for CI but means a `pnpm dev` already running on :3000 will be used silently in place of the production build the suite assumes. If a snapshot diff doesn't reproduce in CI, kill the dev server and run `pnpm test:visual:build` to rebuild against the production output before retrying.\n\n**Pixel-limit error.** Measure the page's full-page height at 1024 px; if it exceeds ~24,400 px, the page needs shortening or removal from the suite. Don't crop to viewport — below-the-fold coverage is the point of this suite.\n\n**Works locally, fails in CI.** Usually `HOME: /root` missing from the test step — GitHub Actions overrides `HOME` inside containers, and Playwright can no longer find the browsers baked into the `mcr.microsoft.com/playwright` image. Also check that the image tag matches `@playwright/test` in `package.json`.\n",".claude/skills/review-page/SKILL.md":"---\nname: review-page\ndescription: Adversarial review loop for a built ethereum.org page — spawns copy and design-system reviewer subagents against the Figma source, triages their findings, fixes, and re-reviews until a fresh round comes back clean. Invoke with /review-page.\ndisable-model-invocation: true\n---\n\n# Review Page\n\nAudit a built page by turning two **adversarial** reviewers loose on it, fixing what they find, and re-running them until a fresh round comes back **green**. Each reviewer is a subagent prompted to assume the page is *wrong* and hunt for proof — never to rubber-stamp it. You (the orchestrator) own the Figma pull, the triage, and the fixes; the reviewers only report. This is the QA half of building a page — pair it with `/new-page`, or run it standalone against any existing page.\n\nWork the steps in order. Each ends on a checkable condition — don't start the next until the current one is met.\n\n## Step 0 — Ground truth\n\nYou need two things before any review:\n\n1. **The live page.** Start `pnpm dev`, load the route (locale-stripped for `en`: `/<route>/`), confirm **HTTP 200**. A 500 is a build/asset error — fix that first; there is nothing to audit on a broken page.\n2. **The design source.** A Figma link/selection is the gold standard. A written spec serves the copy reviewer but not the pixel-level design checks. **No source at all → the copy reviewer has nothing to sync against; say so and confirm the user still wants a design-system-only pass.**\n\n**Done when:** the page serves 200 and the design source is known (or its absence is confirmed).\n\n## Step 1 — Capture both sides\n\nThe reviewers compare *the page as it renders* against *the design as it's specified*. Capture both once, to a scratch dir, and pass the paths to the subagents — don't make each reviewer re-pull Figma (reads drift between pulls, and it burns rate limits).\n\n- **Design.** Read the Figma **fresh** — never a read from an earlier turn: `get_metadata` for the frame tree, `get_screenshot` per region, `get_design_context` for the text, `get_variable_defs` for tokens. Save the region screenshots; keep the text/token dump.\n- **Live page.** Full-page screenshot with `playwright-cli`, plus per-section shots for a long page. Save them beside the design shots.\n\n**Done when:** design and live-page artifacts are saved and their paths are known.\n\n## Step 2 — Spawn the adversarial reviewers\n\nRead both briefs in `reviewers/` and spawn **one subagent per brief, in parallel** — a single message with two `Agent` calls. Give each subagent its brief verbatim, the route, and the artifact paths from Step 1. Each returns a structured findings list — `{location, claim, evidence, fix, severity}` — and nothing else. **Reviewers report; they never edit** (two agents editing in parallel would collide).\n\n- `reviewers/copy.md` — every string on the page matches the Figma/source copy.\n- `reviewers/design-system.md` — components, spacing, heading sizes, and colors are the design system's, with no custom CSS bolted onto primitives.\n\n**Done when:** both subagents have returned their findings lists.\n\n## Step 3 — Triage\n\nAdversarial reviewers over-report by design. **You** judge each finding before touching code — keep the real ones, drop the noise. Two calls need judgment:\n\n- **Intentional copy divergence.** A string differs because the *design is stale*, or because it's a templated/localized value — not a copy bug. Confirm against the source, not the reviewer's assumption.\n- **Justified custom CSS.** Custom CSS *chasing pixel parity* on a primitive is a finding; custom CSS the design *genuinely requires* (a one-off the system can't express) is acceptable per \"match, don't mimic.\" Don't strip styling the design needs.\n\nShow the user the **confirmed** findings as a table — `Region · Finding · Fix · Severity` — and flag anything ambiguous for their call. This is their window into what the reviewers saw.\n\n**Done when:** every finding is confirmed or dropped, and the confirmed table has been shown to the user.\n\n## Step 4 — Fix\n\nApply the confirmed findings yourself, reuse-first — a variant, not custom CSS; the right primitive, not a re-invention. Load the `design-system` skill if you haven't, for the component and token calls.\n\n**Done when:** every confirmed finding is fixed in the working tree.\n\n## Step 5 — Re-review until green\n\nRe-capture the **live page** (it changed; the Figma side didn't — reuse Step 1's design artifacts) and re-spawn the same reviewers. The loop is **green** when a fresh adversarial round returns **zero** confirmed findings. Until then: triage → fix → re-review.\n\nCap it: if the loop isn't green after **3 rounds**, stop and hand the user the stubborn findings — a finding that survives three fixes usually needs a human design call, not another pass.\n\n**Done when:** a fresh round is green, or the 3-round cap is hit and the remaining findings are handed off.\n","AGENTS.md":"# CLAUDE.md - Ethereum.org Website\n\n## Project Overview\n\nThis is the official Ethereum.org website - a Next.js application that serves as the primary educational and community hub for Ethereum. The site is built with modern web technologies and focuses on accessibility, internationalization, and performance.\n\n## Technology Stack\n\n### Core Framework\n\n- **Next.js 16+** - React framework with App Router (Turbopack is the default bundler; webpack available via `pnpm dev:webpack` / `pnpm build:webpack`)\n- **React 19** - UI library\n- **TypeScript 5.5+** - Type safety and development experience\n- **Tailwind CSS 4+** - Utility-first CSS framework (CSS-first config in `src/styles/global.css`)\n\n### Key Dependencies\n\n- **next-intl 4+** - Internationalization (i18n) with 25 languages\n- **next-mdx-remote 5.0+** - MDX content processing\n- **Motion 12+** (`motion` package, formerly Framer Motion) - Animations and transitions; import from `motion/react`\n- **Radix UI** - Accessible component primitives\n- **shadcn/ui** - Component library built on Radix UI\n- **Recharts** - Data visualization\n- **Viem/Wagmi** - Ethereum blockchain integration\n\n### Development & Testing\n\n- **Storybook 10+** - Component development and testing\n- **Chromatic** - Visual regression testing\n- **ESLint** - Code linting with custom rules\n- **Prettier** - Code formatting\n- **Husky** - Git hooks\n- **PNPM** - Package manager\n\n## Project Structure\n\n- **app/** - Next.js App Router pages\n  - **[locale]/** - Internationalized routes\n- **src/**\n  - **components/** - React components\n    - **ui/** - Design system components\n    - **icons/** - SVG icon components\n  - **data/** - Static data and configurations\n  - **hooks/** - Custom React hooks\n  - **i18n/** - Internationalization config\n  - **intl/** - Translation files (25 languages)\n  - **layouts/** - Page layout components\n  - **lib/** - Utility functions and types\n    - **constants.ts** - App constants\n    - **types.ts** - TypeScript type definitions\n    - **utils/** - Utility functions\n  - **styles/** - Global styles and design tokens\n- **public/** - Static assets\n  - **content/** - Markdown content files\n  - **images/** - Image assets\n- **docs/** - Development documentation\n  - **solutions/** - Documented solutions to past problems, organized by category with YAML frontmatter (module, tags, problem_type)\n\n## Code Conventions\n\n### File Naming\n\n- **Components**: kebab-case (e.g., `button-group.tsx`)\n- **Utilities**: camelCase (e.g., `cn.ts`, `relativePath.ts`)\n- **Pages**: kebab-case following Next.js conventions\n- **Assets**: kebab-case (e.g., `eth-logo.png`)\n\n### TypeScript Patterns\n\n- Use `interface` for object shapes, `type` for unions/intersections\n- Prefer explicit typing over `any` (ESLint enforces `fixToUnknown`)\n- **NEVER leave unused variables or parameters** - ESLint `unused-imports/no-unused-vars` will fail the Netlify build. The only allowed unused arg pattern is a single underscore `_`. Do NOT use `_prefixedNames` (e.g., `_foo`) - either use the variable or remove it from the signature entirely.\n- Use generic constraints for reusable components\n- Export types from dedicated files in `@/lib/types`\n\n### Styling Conventions\n\n- **Primary approach**: Tailwind CSS utility classes\n- **Component variants**: Use `tailwind-variants` (`tv`) for new and refactored work. Existing `class-variance-authority` (`cva`) components don't need bulk migration -- swap to `tv` opportunistically when you're already touching the component for another reason.\n- **Dynamic classes**: Use `cn()` utility (clsx + tailwind-merge)\n- **Custom properties**: CSS variables in `src/styles/` for theme values\n- **Responsive design**: Mobile-first approach\n\nFor UI work, see the **`design-system` skill** at `.claude/skills/design-system/`. It's the canonical knowledge base for component choices, design tokens, RTL/i18n, server/client boundaries, and the \"use a variant, not a new component\" pattern.\n\n## Development Workflows\n\n### Available Scripts\n\n```bash\n# Development\npnpm dev                    # Start development server (Turbopack)\npnpm dev:webpack            # Start development server with webpack\npnpm build                  # Build for production (Turbopack)\npnpm build:webpack          # Build for production with webpack\npnpm start                  # Start production server\n\n# Code Quality\npnpm lint                   # Run ESLint\npnpm lint:fix              # Fix ESLint issues\npnpm type-check            # TypeScript type checking\npnpm format                # Format with Prettier\n\n# Testing\npnpm test:unit             # Playwright unit tests (unit project)\npnpm test:e2e              # Playwright end-to-end tests\npnpm test:visual           # Playwright + Chromatic full-page visual tests\n\n# Storybook\npnpm storybook             # Start Storybook dev server\npnpm build-storybook       # Build Storybook\npnpm chromatic             # Storybook Chromatic (needs CHROMATIC_STORYBOOK_TOKEN)\n\n# Content Management\npnpm lint:md               # Lint English markdown content\npnpm lint:md:fix           # Auto-fix header IDs and duplicates\n```\n\n### Testing Strategy\n\n- **Unit Testing**: Playwright `unit` project (`pnpm test:unit`) — heaviest coverage is the intl-pipeline sanitizer\n- **E2E Testing**: Playwright (`pnpm test:e2e`), see `docs/e2e-testing.md`\n- **Visual Testing**: Storybook + Chromatic for component regression; Playwright + Chromatic for full pages (see the `page-visual-tests` skill)\n- **Type Safety**: TypeScript strict mode enabled\n- **Linting**: ESLint with custom rules for imports and TypeScript\n\n## Content Management\n\n### Internationalization\n\n- **25 languages** supported (canonical list: `i18n.config.json`); **RTL support** for Arabic, Urdu\n- JSON UI strings in `src/intl/[locale]/`; translated markdown content in `public/content/translations/[locale]/`\n- Non-English markdown is propagated by the **intl-pipeline** (`src/scripts/intl-pipeline/`, entry `main.ts`). **Do not hand-propagate English changes into non-English files** -- let the pipeline run, or trigger `intl-pipeline.yml` with `stamp_only: true` if manifests must catch up urgently (e.g. unblocking a build). Hand-fixing a translation error is fine when the English side hasn't moved, since the manifest mapping stays valid. Spec: `tests/specs/PIPELINE-SPEC.md`.\n- Glossary: base URL from `GLOSSARY_API_URL` env var; default in `src/scripts/intl-pipeline/config.ts`. ETHGlossary is authoritative for Ethereum term translations.\n\nFor pipeline mechanics, recovery, manifests, ETHGlossary integration, and the `intl/pending-{base}` orchestration model, see the **`intl-pipeline` skill** at `.claude/skills/intl-pipeline/`. For translation-quality review (scoring rubric, language-group rules, ETHGlossary-as-authority policy, multi-agent role split), see the **`intl-review` skill** at `.claude/skills/intl-review/`.\n\n### Markdown Content\n\n- Educational content stored in `public/content/`\n- Processed with `next-mdx-remote`\n- Custom MDX components for rich content\n- Automatic table of contents generation\n- **All h1-h4 headings require a custom `{#lower-kebab-id}`** -- enforced by markdownlint via pre-commit hook. Run `pnpm lint:md:fix` to auto-add missing IDs. Config: `.markdownlint-cli2.jsonc`, custom rules: `.markdownlint-rules/`\n\n### Asset Management\n\n- Images optimized with Next.js Image component\n- SVGs loaded as React components via `@svgr/webpack`\n- Static assets served from `public/`\n- Placeholder generation for images\n\n## SEO & Meta\n\n- Sitemap generation in `app/sitemaps/sitemap.ts` (one sitemap per locale, indexed by `app/sitemap.xml/route.ts`)\n- Meta tags and Open Graph optimization\n- Structured data for search engines\n- Security headers (X-Frame-Options: DENY)\n\n## Development Guidelines\n\n### When Working on Features\n\n1. **Check existing patterns** - Look at similar components first\n2. **Prioritize Server Components** - Use App Router and Server Components when possible\n3. **Follow import order** - ESLint will enforce, but be proactive\n4. **Use TypeScript strictly** - No `any` types, prefer `unknown`\n5. **Test in Storybook** - Create stories for new components (filename pattern: `.stories.tsx`)\n6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations(\"common\")`) rather than reaching across namespaces\n7. **Mobile-first** - Design for mobile, enhance for desktop\n8. **Accessibility** - Use Radix primitives, semantic HTML\n9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales.\n\n### Component Development\n\n1. Create component in appropriate `src/components/` subdirectory\n   - Use `src/components/ui` for shadcn components or pure UI components\n2. Add TypeScript types and proper props interface\n3. Accept `ref` as a regular prop when needed (React 19); `forwardRef` only survives in legacy components\n4. Add a Storybook story (see below)\n5. Export from appropriate index file\n6. Update documentation if adding new patterns\n\n### Storybook stories\n\n**Location**: `__stories__/` for flat directories (`ui/`, `styles/`), co-located for foldered components. Story globs are recursive, so nesting works. **Filenames are kebab-case** (`app-card.stories.tsx`) even inside a PascalCase component directory -- enforced by the test below.\n\n**Title**: `Section / Group / Name`. The section is derived from the file path -- not a judgment call:\n\n| Path                                  | Section         |\n| ------------------------------------- | --------------- |\n| `src/styles/**`                       | `Design System` |\n| `src/components/ui/**`                | `UI`            |\n| `src/components/**` (everything else) | `Components`    |\n| `src/layouts/**`                      | `Layouts`       |\n| `app/**`                              | `Pages`         |\n\nUse a second-level group only when an established one fits (Actions, Forms, Layout, Navigation, Overlays, Data Display, Cards, Heroes, Site Chrome, Content, Data Viz, Features). **Don't invent a group for a single component** -- a flat `Components / Morpher` is correct and preferred. `tests/unit/storybook/story-titles.spec.ts` enforces the section prefix, explicit titles, and title uniqueness (Storybook silently merges duplicate titles into one sidebar entry).\n\n**Don't add a story for a deprecated component or variant.** A rendered example reads as an endorsement whatever the caption says, so the showcase should only contain things that are available to use -- note the deprecation in prose instead (see `HR`). A component whose future is merely undecided can keep its story with a status blurb (see `Carousel`).\n\nAdd `tags: [\"autodocs\"]`. Write the `parameters.docs.description.component` blurb for someone deciding whether to use the thing -- constraints and gotchas, not a restatement of the props table.\n\n**A green build does not mean a story renders.** `pnpm build-storybook` compiles stories without executing them, and every `UI /` story sets `chromatic: { disableSnapshot: true }`, so neither CI signal catches a story that throws. Open new stories in `pnpm storybook` before pushing.\n\nAsync server components render via a `next-intl/server` shim (`.storybook/next-intl-server.tsx`). Components calling `usePathname` need `parameters.nextjs.navigation.pathname`. New i18n namespaces must be added to the `ns` array in `.storybook/next-intl.ts`, or strings render as raw key tails.\n\n### Content Updates\n\n1. Markdown files go in `public/content/`\n2. Images in `public/images/` with descriptive names\n3. Translation strings in appropriate `src/intl/` JSON files\n4. Data files in `src/data/` with TypeScript types\n\n### Type-Safe Chain Names\n\nThis project enforces type-safe chain names via TypeScript. When working with layer 2 networks or wallet data:\n\n**Critical Files:**\n\n- `src/data/chains.ts` - Canonical source of all chain names (auto-updated weekly)\n- `src/lib/types.ts` - Defines `ChainName` type derived from chains.ts\n- `src/data/networks/networks.ts` - Uses `chainName: ChainName`\n- `src/data/wallets/wallet-data.ts` - Uses `supported_chains: ChainName[]`\n\n**Rules:**\n\n1. **Always look up exact names** - Before adding `chainName` or `supported_chains`, search `chains.ts` for the exact `name` value\n2. **Names are case-sensitive and exact** - e.g., use `\"Zircuit Mainnet\"` not `\"Zircuit\"`, use `\"OP Mainnet\"` not `\"Optimism\"`\n3. **Run type checking** - Use `pnpm type-check` to verify chain names are valid before committing\n4. **Non-EVM chains** - For Starknet and other non-EVM chains, use `NonEVMChainName` type\n\n**Common Mistakes:**\n\n- Using informal names: `\"Optimism\"` should be `\"OP Mainnet\"`\n- Missing \"Mainnet\" suffix: `\"Zircuit\"` should be `\"Zircuit Mainnet\"`\n- Wrong casing: `\"zksync Mainnet\"` should be `\"zkSync Mainnet\"`\n\n## Key Dependencies to Know\n\n### UI & Styling\n\n- `@radix-ui/*` - Accessible component primitives\n- `tailwind-variants` - Component variant patterns\n- `motion` - Animation library (formerly framer-motion; import from `motion/react`)\n- `lucide-react` - Icon library\n\n### Content & Data\n\n- `gray-matter` - Frontmatter parsing\n- `recharts` - Data visualization\n\n### Ethereum Integration\n\n- `viem` - Ethereum library\n- `wagmi` - React hooks for Ethereum\n- `@rainbow-me/rainbowkit` - Wallet connection\n\n## A/B Testing\n\nThe site uses a GDPR-compliant, cookie-less A/B testing system built on the Flags SDK precompute pattern, integrated with Matomo. The proxy assigns variants at the edge (header fingerprinting) and rewrites to signed, statically prerendered variant pages under `ab-code/[code]/` — experiments never force dynamic rendering. Experiment status, weights, and scheduling live in the Matomo dashboard (no deploys needed). Only the default locale is tested; other locales always get the original.\n\nKey gotcha: **variants are matched by array index, not names** — the `variants` array order must match the Matomo experiment order exactly, and the `testKey` must match the Matomo experiment name exactly.\n\nFull guide (architecture, step-by-step recipe, env vars): `docs/ab-testing.md`. Code: `proxy.ts`, `src/lib/ab-testing/`, `src/components/AB/`, `app/[locale]/ab-code/`.\n\n## Site Search\n\nSearch runs on a self-hosted Typesense instance, one collection per locale, crawled by DocSearch from production ethereum.org. Publishing is gated: a crawl goes to a staging collection and the live alias only moves if the new index passes size, field and relevance checks.\n\nTwo things bite most often. **Ranking hints reach the index as `docsearch:*` meta tags** from `src/lib/utils/metadata.ts`, so a ranking change does nothing until the site is deployed and re-crawled. And **the networks offered as block explorer results come from `src/data/networks/networks.ts`** -- the same list `/layer-2/networks` renders -- so adding an L2 there is the only edit needed.\n\nOverview: `docs/site-search.md`. Runbook, secrets and recovery: `typesense/README.md`. Code: `src/components/Search/`, `src/scripts/typesense/`, `typesense/`.\n\n## Deployment\n\n- **Platform**: Netlify (config in `netlify.toml`)\n- **Next.js Integration**: Uses `@netlify/plugin-nextjs` for seamless Netlify and Next.js compatibility\n- **Monitoring**: Matomo analytics integration\n\n### Runtime constraints\n\n- **`public/content` is not in the Netlify server function** (CDN-only). A page that reads it at render time must stay static (`revalidate: false` getters like `getStaticAppsData`) or have an `outputFileTracingIncludes` entry in `next.config.js`. A finite-`revalidate` getter (e.g. `getEventsData`, 24h) turns the whole route into ISR. Details: `docs/solutions/integration-issues/netlify-isr-404-async-server-components.md`.\n\n## Internal Infrastructure\n\nThe following external-looking services are managed by the ethereum.org team:\n\n- **`s3-dcl1.ethquokkaops.io`** — S3-compatible object storage for app screenshots and media. Used by the data layer to serve images for the `/dapps` and app listing pages. Downtime here means broken images on the live site.\n- **Netlify Blobs** (`@netlify/blobs`) — Key-value store used by the data layer to cache API responses. Accessed via `src/data-layer/storage.ts`.\n",".claude/skills/performance/SKILL.md":"---\nname: performance\ndescription: Performance patterns and anti-patterns for ethereum.org covering TTFB, LCP, INP, CLS, bundle size, build memory, and RSC payload. Use when diagnosing a perf regression or reviewing proposed code changes for perf landmines.\nuser-invocable: false\n---\n\n# Performance Knowledge\n\nKnowledge base. No runtime actions; authoring conventions for maintainers below. Read the relevant `references/*.md` files directly via the Read tool.\n\n## Two entry points\n\n- **Diagnosing** an existing regression → `references/diagnose-table.md` (symptom → bucket).\n- **Reviewing** a proposed change before merge → `references/review-checklist.md` (diff pattern → rules to load).\n\n## Cross-cutting anti-patterns\n\nThese look right but aren't. Check every review or diagnose pass against this table.\n\n| Looks like…                                                  | Actually…                                                                                                     |\n| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |\n| Adding `CDN-Cache-Control` to fix TTFB                       | Netlify strips them; CF ignores due to `max-age=0`.                                                           |\n| Using `unstable_cache` in MDX routes                         | Triggers ISR → runtime 404s. See `edge-caching.md`.                                                           |\n| ``await import(`.../${locale}/${ns}.json`)``                 | Webpack enumerates all combinations → OOM. Use `fs.readFile`.                                                 |\n| Adding a `useSession` / `cookies()` in root layout           | Every route becomes dynamic. Edge caching dies silently.                                                      |\n| `priority: true` via `getImageProps()` on manual `<picture>` | `fetchPriority` attribute is not set; add it manually.                                                        |\n| Bumping `revalidate` to hourly to \"fix staleness\"            | Increases edge miss rate. Keep daily; add client `/api/*` for freshness.                                      |\n| Mocking DB/content in perf tests                             | Perf wins must be measured against real prod build (`pnpm build && start`).                                   |\n| Removing `common.json` keys without audit                    | Lookups of removed keys silently render the bare key (fallback). Grep the key across `src/` and `app/` first. |\n\n## File map\n\n| Topic                           | File                             |\n| ------------------------------- | -------------------------------- |\n| Symptom → bucket routing        | `references/diagnose-table.md`   |\n| Diff pattern → rules routing    | `references/review-checklist.md` |\n| TTFB, edge caching, CDN         | `references/edge-caching.md`     |\n| Bundle size, code splitting, RSC payload | `references/bundle.md`  |\n| Build OOM, ENOSPC, file tracing | `references/build.md`            |\n| INP, main-thread work           | `references/inp.md`              |\n| LCP, images                     | `references/images.md`           |\n\nFor data-fetching patterns (Trigger.dev tasks, Netlify Blobs, `src/lib/data` caching, internal `/api/*` routes, fetcher retries), use the `data-layer` skill instead.\n\n## Conventions for adding rules\n\n- One topic per file; no content duplication across files.\n- Cite a PR (`PR #N`) when a rule comes from a specific shipped change. Add a SHA only when no PR exists.\n- Forward-looking (\"do this, avoid that\"). Keep entries prescriptive — no postmortems or incident narratives.\n- New review check? Add a row to `review-checklist.md` AND the matching bucket file. Don't restate the rule inline.\n",".claude/commands/fix-sanitizer-bug.md":"---\ndescription: Guided workflow for fixing translation sanitizer bugs — triage, test, fix, verify\nallowed-tools: Bash, Read, Glob, Grep, Task, Edit, Write, AskUserQuestion\nargument-hint: [--language=CODE] [--issue=\"description\"] [--file=PATH] [--skip-build]\n---\n\n# Fix Sanitizer Bug\n\nIterative workflow for fixing bugs in the post-import translation sanitizer (`src/scripts/intl-pipeline/intl-sanitizer.ts`). Follows a test-first approach: triage the issue, write a failing test, implement the fix, verify across languages.\n\n## Context\n- Current branch: !`git branch --show-current`\n- Arguments: $ARGUMENTS\n- Sanitizer: `src/scripts/intl-pipeline/intl-sanitizer.ts`\n- Test files: `tests/unit/intl-pipeline/sanitizer/*.spec.ts`\n- Research docs: `docs/solutions/integration-issues/`\n\n## Phase 0: Gather Context\n\n### Parse Flags\n\nExtract from $ARGUMENTS:\n- `LANGUAGE`: from `--language=CODE` (e.g., `ja`, `zh-tw`, `es`)\n- `ISSUE_DESC`: from `--issue=\"...\"` (brief description of the bug)\n- `FILE_PATH`: from `--file=PATH` (specific file where issue was spotted)\n- `SKIP_BUILD`: from `--skip-build` (skip build verification)\n\n### Collect Information\n\nIf flags are missing, use AskUserQuestion to gather:\n\n1. **What language?** — Which locale has the issue (e.g., `ja`, `tr`, `zh-tw`)\n2. **What's the artifact?** — Exact text of the translation bug (copy-paste the broken string)\n3. **Where?** — File path or general area (markdown content, JSON translations, frontmatter)\n4. **English source?** — What does the correct English look like\n\nRead the affected file and the English source to confirm the issue:\n```\nTranslated: {FILE_PATH}\nEnglish:    {ENGLISH_EQUIVALENT_PATH}\n```\n\n**IMPORTANT:** Capture the exact broken pattern NOW before any processing. Copy the raw artifact verbatim — you'll need it for the test.\n\n## Phase 1: Document the Problem\n\nAppend the new pattern to the research doc:\n\n**File:** `docs/solutions/integration-issues/sanitizer-test-research.md`\n\nAdd a row to the \"New Patterns\" table:\n\n```markdown\n| N+1 | {PATTERN_DESCRIPTION} | {SOURCE_PR_OR_REVIEW} | `{EXACT_EXAMPLE}` | {SEVERITY} |\n```\n\nSeverity guide:\n- **Critical** — Breaks rendering, navigation, or MDX compilation\n- **High** — Breaks links, images, or loses content\n- **Medium** — Wrong text displayed, semantic errors\n- **Low** — Cosmetic, formatting-only\n\n## Phase 2: Triage — Fix, Warn, or Document Only\n\nThis is the most important decision. Use AskUserQuestion:\n\n**\"What type of fix does this need?\"**\n\n### Option A: Deterministic Fix (auto-correct)\nUse when the pattern is:\n- Regex-matchable with no false positives\n- The correct output is always the same (no judgment needed)\n- Safe to apply across all languages\n\nExamples: escaped bold `\\*\\*text\\*\\*`, ticker typos `EHT→ETH`, date format `DD/MM/YYYY→YYYY-MM-DD`\n\n→ Proceed to Phase 3A (write fix function + test)\n\n### Option B: Warning Only (detect + report)\nUse when:\n- The pattern is detectable but the fix requires context/judgment\n- Auto-fixing could cause collateral damage (see Bug #1: href substitution)\n- Different files may need different resolutions\n\nExamples: translated hrefs, missing brand names, code fence drift\n\n→ Proceed to Phase 3B (write warn function + test)\n\n### Option C: Document Only (not automatable)\nUse when:\n- The issue is semantic (wrong word choice, not a pattern)\n- No reliable regex can detect it\n- It needs human/AI review judgment\n\nExamples: \"Gas\" → \"Sprit\" (gasoline) in German, tone inconsistency\n\n→ Skip to Phase 6 (update docs only)\n\n## Phase 3A: Write Failing Test (Fix Function)\n\n### Determine which test file\n\n- Pure function (no English source needed) → `tests/unit/intl-pipeline/sanitizer/standalone-fixes.spec.ts`\n- Needs English comparison → `tests/unit/intl-pipeline/sanitizer/english-comparison.spec.ts`\n- End-to-end through processMarkdownFile/processJsonFile → `tests/unit/intl-pipeline/sanitizer/integration.spec.ts`\n\n### Write the test FIRST\n\nAdd a `test.describe` block for the new function. Include at minimum:\n\n```typescript\ntest.describe(\"fixNewIssue\", () => {\n  test(\"fixes the broken pattern\", () => {\n    const input = \"{EXACT_BROKEN_PATTERN_FROM_PHASE_0}\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(\"{EXPECTED_CORRECT_OUTPUT}\")\n    expect(fixCount).toBe(1)\n  })\n\n  test(\"leaves correct content unchanged\", () => {\n    const input = \"{ALREADY_CORRECT_CONTENT}\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(input)\n    expect(fixCount).toBe(0)\n  })\n\n  test(\"skips code blocks\", () => {\n    // If the fix operates on prose, it MUST skip code blocks\n    const input = \"```\\n{PATTERN_INSIDE_CODE}\\n```\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(input)\n    expect(fixCount).toBe(0)\n  })\n})\n```\n\n### Add import to test file\n\nAdd the new function name to the destructured import from `_testOnly` at the top of the test file.\n\n### Verify test fails\n\n```bash\nnpx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/{FILE}.spec.ts\n```\n\nThe new test MUST fail (function doesn't exist yet). Existing tests should still pass.\n\n## Phase 3B: Write Failing Test (Warn Function)\n\nSame as 3A but assert warnings instead of content changes:\n\n```typescript\ntest.describe(\"warnNewIssue\", () => {\n  test(\"warns on broken pattern\", () => {\n    const warnings = warnNewIssue(\"{BROKEN_INPUT}\", \"{ENGLISH_INPUT}\")\n    expect(warnings.length).toBeGreaterThan(0)\n    expect(warnings[0]).toContain(\"{EXPECTED_WARNING_SUBSTRING}\")\n  })\n\n  test(\"no warning on clean content\", () => {\n    const warnings = warnNewIssue(\"{CLEAN_INPUT}\", \"{ENGLISH_INPUT}\")\n    expect(warnings).toHaveLength(0)\n  })\n})\n```\n\nUse `tests/unit/intl-pipeline/sanitizer/warnings.spec.ts` for warn-only functions.\n\n## Phase 4: Implement the Fix\n\n### Write the function in the sanitizer\n\n**File:** `src/scripts/intl-pipeline/intl-sanitizer.ts`\n\n**For fix functions** — follow the established pattern:\n\n```typescript\nfunction fixNewIssue(content: string): {\n  content: string\n  fixCount: number\n} {\n  let fixCount = 0\n\n  // MANDATORY: Split to preserve code blocks\n  const codeBlockPattern = /(```[\\s\\S]*?```|~~~[\\s\\S]*?~~~|`[^`]+`)/g\n  const parts = content.split(codeBlockPattern)\n\n  for (let i = 0; i < parts.length; i++) {\n    if (i % 2 === 1) continue // Skip code blocks\n\n    // Your fix logic here\n    parts[i] = parts[i].replace(/{PATTERN}/g, () => {\n      fixCount++\n      return \"{REPLACEMENT}\"\n    })\n  }\n\n  return { content: parts.join(\"\"), fixCount }\n}\n```\n\n**Critical rules:**\n- ALWAYS split on code blocks first (fenced + inline)\n- ALWAYS return `{ content, fixCount }` for fix functions\n- ALWAYS return `string[]` for warn functions\n- Use word boundaries `\\b` for brand names to avoid partial matches\n- Use `escapeRegex()` when building regex from dynamic strings\n\n### Add to _testOnly export\n\nAdd the function name to the `_testOnly` export object near the bottom of the file.\n\n### Wire into processMarkdownFile or processJsonFile\n\nAdd the function call using the `applyFix` helper pattern:\n\n```typescript\napplyFix(\n  () => fixNewIssue(content),\n  (n) => `Fixed ${n} new issues`\n)\n```\n\n**Placement matters:** Consider whether the fix should run before or after existing fixes. Some fixes depend on others having run first.\n\nFor warn functions, add directly:\n```typescript\nconst newWarnings = warnNewIssue(content, englishMd)\nissues.push(...newWarnings)\n```\n\n## Phase 5: Run Tests and Verify\n\n### Step 1: Unit tests\n\n```bash\nnpx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/\n```\n\n**All tests must pass** — both the new test and all existing 600+ tests.\n\nIf a test fails:\n- New test fails → fix the implementation, not the test\n- Existing test fails → your fix has a regression, investigate the interaction\n\n### Step 2: Run sanitizer against ONLY the affected files\n\n**CRITICAL: NEVER run the sanitizer against an entire language. It processes thousands of files and will hang for 30+ minutes. Always scope to the specific files from the PR.**\n\nDetermine which files to test from the PR context (e.g., `gaming/index.md`). The sanitizer's only env-var scoping is `TARGET_LANGUAGES` (per-language, not per-file) — there is no `TARGET_FILES`. For single files, write an inline script that calls `processMarkdownFile` directly:\n\n```bash\n# Language-level scoping (still processes every file for that language):\nTARGET_LANGUAGES={LANGUAGE} \\\n  pnpm exec tsx ./src/scripts/intl-pipeline/intl-sanitizer.ts\n\n# Preferred for single files — inline script calling processMarkdownFile directly:\npnpm exec tsx -e '\nconst { _testOnly } = require(\"./src/scripts/intl-pipeline/intl-sanitizer\");\nconst fs = require(\"fs\");\nconst file = \"public/content/translations/{LANGUAGE}/{PAGE_PATH}\";\nconst content = fs.readFileSync(file, \"utf8\");\nconst result = _testOnly.processMarkdownFile(file, content);\nconsole.log(result.issues.join(\"\\n\"));\nif (result.fixed) fs.writeFileSync(file, result.content);\n'\n```\n\nCheck the output for:\n- Your fix being applied (look for the issue label in the log)\n- No unexpected fixes in other areas\n- Fix count looks reasonable (not 0, not thousands)\n\n### Step 3: Inspect the actual changes\n\n```bash\ngit diff public/content/translations/{LANGUAGE}/{PAGE_PATH}\n```\n\nVerify:\n- The broken pattern is corrected\n- No collateral damage to surrounding content\n- Changes look correct to a human reader\n\n### Step 4: Build verification (conditional)\n\n**Only run this if the fix touches MDX syntax** — angle brackets, tags, components, backticks.\n\n**Skip if** `--skip-build` flag is set, or fix is purely textual (ticker corrections, brand tags, date normalization, guillemets, bold/italic escaping).\n\n```bash\nNEXT_PUBLIC_BUILD_LOCALES=en,{LANGUAGE} pnpm build\n```\n\n**NOTE:** This step requires `dangerouslyDisableSandbox: true` and significant RAM. Only use when the fix could affect MDX compilation.\n\n### Step 5: Cross-language spot check on the SAME file only\n\nTest the same page in 2-3 other languages to check for false positives. **NEVER run across all files for a language.**\n\n```bash\n# Test the same page path in a few other languages\nfor lang in es tr ja; do\n  pnpm exec tsx -e \"\n    const { _testOnly } = require('./src/scripts/intl-pipeline/intl-sanitizer');\n    const fs = require('fs');\n    const file = 'public/content/translations/$lang/{PAGE_PATH}';\n    if (!fs.existsSync(file)) { console.log('$lang: file not found, skipping'); process.exit(0); }\n    const content = fs.readFileSync(file, 'utf8');\n    const result = _testOnly.processMarkdownFile(file, content);\n    console.log('$lang:', result.issues.length ? result.issues.join('; ') : 'clean');\n  \"\ndone\n```\n\nCheck that your fix doesn't trigger unexpectedly in other languages.\n\n## Phase 6: If Not Resolved\n\nIf the fix doesn't resolve the issue after Phase 5:\n\n### Diagnose the root cause\n\nUse AskUserQuestion:\n\n**\"What went wrong?\"**\n\n1. **Pattern mismatch** — regex doesn't match the real-world variant\n   - Get more examples of the broken pattern\n   - Broaden the regex\n   - Add another test case for the variant\n   - Go back to Phase 4\n\n2. **Interaction effect** — another fix runs first and changes content\n   - Identify which fix runs first and transforms the input\n   - Reorder the fix in `processMarkdownFile` (earlier or later)\n   - Add an interaction test in `integration.spec.ts`\n   - Go back to Phase 4\n\n3. **False positives in other languages** — fix breaks something elsewhere\n   - Add language-specific exclusions\n   - Add a cross-language test case\n   - Consider making it warn-only instead\n   - Go back to Phase 3\n\n4. **Not actually automatable** — needs more context than regex can provide\n   - Convert to warn function or document-only\n   - Go back to Phase 2 and re-triage\n\n## Phase 7: Update Documentation\n\n### Update research doc\n\n**File:** `docs/solutions/integration-issues/sanitizer-test-research.md`\n\nIf the pattern was new (not already in the table), ensure it was added in Phase 1.\n\nIf the fix worked, move the pattern from \"New Patterns Not Yet Covered\" to \"Patterns Already Handled by Sanitizer\" with the function name.\n\n### Update existing bug docs if relevant\n\nCheck if this relates to previously documented bugs:\n- `docs/solutions/integration-issues/intl-pipeline-bugs-from-pr-18041-review.md`\n\n### Report summary\n\nDisplay to user:\n```\n## Fix Complete\n\n**Issue:** {ISSUE_DESCRIPTION}\n**Type:** {fix | warn | document-only}\n**Function:** {FUNCTION_NAME}\n**Test file:** {TEST_FILE}\n**Tests:** {N} new tests added, {TOTAL} total passing\n**Languages verified:** {LANGUAGES_CHECKED}\n**Files changed:**\n  - src/scripts/intl-pipeline/intl-sanitizer.ts (fix + export)\n  - tests/unit/intl-pipeline/sanitizer/{FILE}.spec.ts (new tests)\n  - docs/solutions/integration-issues/sanitizer-test-research.md (documentation)\n```\n\n","CLAUDE.md":"# CLAUDE.md - Ethereum.org Website\n\n## Project Overview\n\nThis is the official Ethereum.org website - a Next.js application that serves as the primary educational and community hub for Ethereum. The site is built with modern web technologies and focuses on accessibility, internationalization, and performance.\n\n## Technology Stack\n\n### Core Framework\n\n- **Next.js 16+** - React framework with App Router (Turbopack is the default bundler; webpack available via `pnpm dev:webpack` / `pnpm build:webpack`)\n- **React 19** - UI library\n- **TypeScript 5.5+** - Type safety and development experience\n- **Tailwind CSS 4+** - Utility-first CSS framework (CSS-first config in `src/styles/global.css`)\n\n### Key Dependencies\n\n- **next-intl 4+** - Internationalization (i18n) with 25 languages\n- **next-mdx-remote 5.0+** - MDX content processing\n- **Motion 12+** (`motion` package, formerly Framer Motion) - Animations and transitions; import from `motion/react`\n- **Radix UI** - Accessible component primitives\n- **shadcn/ui** - Component library built on Radix UI\n- **Recharts** - Data visualization\n- **Viem/Wagmi** - Ethereum blockchain integration\n\n### Development & Testing\n\n- **Storybook 10+** - Component development and testing\n- **Chromatic** - Visual regression testing\n- **ESLint** - Code linting with custom rules\n- **Prettier** - Code formatting\n- **Husky** - Git hooks\n- **PNPM** - Package manager\n\n## Project Structure\n\n- **app/** - Next.js App Router pages\n  - **[locale]/** - Internationalized routes\n- **src/**\n  - **components/** - React components\n    - **ui/** - Design system components\n    - **icons/** - SVG icon components\n  - **data/** - Static data and configurations\n  - **hooks/** - Custom React hooks\n  - **i18n/** - Internationalization config\n  - **intl/** - Translation files (25 languages)\n  - **layouts/** - Page layout components\n  - **lib/** - Utility functions and types\n    - **constants.ts** - App constants\n    - **types.ts** - TypeScript type definitions\n    - **utils/** - Utility functions\n  - **styles/** - Global styles and design tokens\n- **public/** - Static assets\n  - **content/** - Markdown content files\n  - **images/** - Image assets\n- **docs/** - Development documentation\n  - **solutions/** - Documented solutions to past problems, organized by category with YAML frontmatter (module, tags, problem_type)\n\n## Code Conventions\n\n### File Naming\n\n- **Components**: kebab-case (e.g., `button-group.tsx`)\n- **Utilities**: camelCase (e.g., `cn.ts`, `relativePath.ts`)\n- **Pages**: kebab-case following Next.js conventions\n- **Assets**: kebab-case (e.g., `eth-logo.png`)\n\n### TypeScript Patterns\n\n- Use `interface` for object shapes, `type` for unions/intersections\n- Prefer explicit typing over `any` (ESLint enforces `fixToUnknown`)\n- **NEVER leave unused variables or parameters** - ESLint `unused-imports/no-unused-vars` will fail the Netlify build. The only allowed unused arg pattern is a single underscore `_`. Do NOT use `_prefixedNames` (e.g., `_foo`) - either use the variable or remove it from the signature entirely.\n- Use generic constraints for reusable components\n- Export types from dedicated files in `@/lib/types`\n\n### Styling Conventions\n\n- **Primary approach**: Tailwind CSS utility classes\n- **Component variants**: Use `tailwind-variants` (`tv`) for new and refactored work. Existing `class-variance-authority` (`cva`) components don't need bulk migration -- swap to `tv` opportunistically when you're already touching the component for another reason.\n- **Dynamic classes**: Use `cn()` utility (clsx + tailwind-merge)\n- **Custom properties**: CSS variables in `src/styles/` for theme values\n- **Responsive design**: Mobile-first approach\n\nFor UI work, see the **`design-system` skill** at `.claude/skills/design-system/`. It's the canonical knowledge base for component choices, design tokens, RTL/i18n, server/client boundaries, and the \"use a variant, not a new component\" pattern.\n\n## Development Workflows\n\n### Available Scripts\n\n```bash\n# Development\npnpm dev                    # Start development server (Turbopack)\npnpm dev:webpack            # Start development server with webpack\npnpm build                  # Build for production (Turbopack)\npnpm build:webpack          # Build for production with webpack\npnpm start                  # Start production server\n\n# Code Quality\npnpm lint                   # Run ESLint\npnpm lint:fix              # Fix ESLint issues\npnpm type-check            # TypeScript type checking\npnpm format                # Format with Prettier\n\n# Testing\npnpm test:unit             # Playwright unit tests (unit project)\npnpm test:e2e              # Playwright end-to-end tests\npnpm test:visual           # Playwright + Chromatic full-page visual tests\n\n# Storybook\npnpm storybook             # Start Storybook dev server\npnpm build-storybook       # Build Storybook\npnpm chromatic             # Storybook Chromatic (needs CHROMATIC_STORYBOOK_TOKEN)\n\n# Content Management\npnpm lint:md               # Lint English markdown content\npnpm lint:md:fix           # Auto-fix header IDs and duplicates\n```\n\n### Testing Strategy\n\n- **Unit Testing**: Playwright `unit` project (`pnpm test:unit`) — heaviest coverage is the intl-pipeline sanitizer\n- **E2E Testing**: Playwright (`pnpm test:e2e`), see `docs/e2e-testing.md`\n- **Visual Testing**: Storybook + Chromatic for component regression; Playwright + Chromatic for full pages (see the `page-visual-tests` skill)\n- **Type Safety**: TypeScript strict mode enabled\n- **Linting**: ESLint with custom rules for imports and TypeScript\n\n## Content Management\n\n### Internationalization\n\n- **25 languages** supported (canonical list: `i18n.config.json`); **RTL support** for Arabic, Urdu\n- JSON UI strings in `src/intl/[locale]/`; translated markdown content in `public/content/translations/[locale]/`\n- Non-English markdown is propagated by the **intl-pipeline** (`src/scripts/intl-pipeline/`, entry `main.ts`). **Do not hand-propagate English changes into non-English files** -- let the pipeline run, or trigger `intl-pipeline.yml` with `stamp_only: true` if manifests must catch up urgently (e.g. unblocking a build). Hand-fixing a translation error is fine when the English side hasn't moved, since the manifest mapping stays valid. Spec: `tests/specs/PIPELINE-SPEC.md`.\n- Glossary: base URL from `GLOSSARY_API_URL` env var; default in `src/scripts/intl-pipeline/config.ts`. ETHGlossary is authoritative for Ethereum term translations.\n\nFor pipeline mechanics, recovery, manifests, ETHGlossary integration, and the `intl/pending-{base}` orchestration model, see the **`intl-pipeline` skill** at `.claude/skills/intl-pipeline/`. For translation-quality review (scoring rubric, language-group rules, ETHGlossary-as-authority policy, multi-agent role split), see the **`intl-review` skill** at `.claude/skills/intl-review/`.\n\n### Markdown Content\n\n- Educational content stored in `public/content/`\n- Processed with `next-mdx-remote`\n- Custom MDX components for rich content\n- Automatic table of contents generation\n- **All h1-h4 headings require a custom `{#lower-kebab-id}`** -- enforced by markdownlint via pre-commit hook. Run `pnpm lint:md:fix` to auto-add missing IDs. Config: `.markdownlint-cli2.jsonc`, custom rules: `.markdownlint-rules/`\n\n### Asset Management\n\n- Images optimized with Next.js Image component\n- SVGs loaded as React components via `@svgr/webpack`\n- Static assets served from `public/`\n- Placeholder generation for images\n\n## SEO & Meta\n\n- Sitemap generation in `app/sitemaps/sitemap.ts` (one sitemap per locale, indexed by `app/sitemap.xml/route.ts`)\n- Meta tags and Open Graph optimization\n- Structured data for search engines\n- Security headers (X-Frame-Options: DENY)\n\n## Development Guidelines\n\n### When Working on Features\n\n1. **Check existing patterns** - Look at similar components first\n2. **Prioritize Server Components** - Use App Router and Server Components when possible\n3. **Follow import order** - ESLint will enforce, but be proactive\n4. **Use TypeScript strictly** - No `any` types, prefer `unknown`\n5. **Test in Storybook** - Create stories for new components (filename pattern: `.stories.tsx`)\n6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations(\"common\")`) rather than reaching across namespaces\n7. **Mobile-first** - Design for mobile, enhance for desktop\n8. **Accessibility** - Use Radix primitives, semantic HTML\n9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales.\n\n### Component Development\n\n1. Create component in appropriate `src/components/` subdirectory\n   - Use `src/components/ui` for shadcn components or pure UI components\n2. Add TypeScript types and proper props interface\n3. Accept `ref` as a regular prop when needed (React 19); `forwardRef` only survives in legacy components\n4. Add a Storybook story (see below)\n5. Export from appropriate index file\n6. Update documentation if adding new patterns\n\n### Storybook stories\n\n**Location**: `__stories__/` for flat directories (`ui/`, `styles/`), co-located for foldered components. Story globs are recursive, so nesting works. **Filenames are kebab-case** (`app-card.stories.tsx`) even inside a PascalCase component directory -- enforced by the test below.\n\n**Title**: `Section / Group / Name`. The section is derived from the file path -- not a judgment call:\n\n| Path                                  | Section         |\n| ------------------------------------- | --------------- |\n| `src/styles/**`                       | `Design System` |\n| `src/components/ui/**`                | `UI`            |\n| `src/components/**` (everything else) | `Components`    |\n| `src/layouts/**`                      | `Layouts`       |\n| `app/**`                              | `Pages`         |\n\nUse a second-level group only when an established one fits (Actions, Forms, Layout, Navigation, Overlays, Data Display, Cards, Heroes, Site Chrome, Content, Data Viz, Features). **Don't invent a group for a single component** -- a flat `Components / Morpher` is correct and preferred. `tests/unit/storybook/story-titles.spec.ts` enforces the section prefix, explicit titles, and title uniqueness (Storybook silently merges duplicate titles into one sidebar entry).\n\n**Don't add a story for a deprecated component or variant.** A rendered example reads as an endorsement whatever the caption says, so the showcase should only contain things that are available to use -- note the deprecation in prose instead (see `HR`). A component whose future is merely undecided can keep its story with a status blurb (see `Carousel`).\n\nAdd `tags: [\"autodocs\"]`. Write the `parameters.docs.description.component` blurb for someone deciding whether to use the thing -- constraints and gotchas, not a restatement of the props table.\n\n**A green build does not mean a story renders.** `pnpm build-storybook` compiles stories without executing them, and every `UI /` story sets `chromatic: { disableSnapshot: true }`, so neither CI signal catches a story that throws. Open new stories in `pnpm storybook` before pushing.\n\nAsync server components render via a `next-intl/server` shim (`.storybook/next-intl-server.tsx`). Components calling `usePathname` need `parameters.nextjs.navigation.pathname`. New i18n namespaces must be added to the `ns` array in `.storybook/next-intl.ts`, or strings render as raw key tails.\n\n### Content Updates\n\n1. Markdown files go in `public/content/`\n2. Images in `public/images/` with descriptive names\n3. Translation strings in appropriate `src/intl/` JSON files\n4. Data files in `src/data/` with TypeScript types\n\n### Type-Safe Chain Names\n\nThis project enforces type-safe chain names via TypeScript. When working with layer 2 networks or wallet data:\n\n**Critical Files:**\n\n- `src/data/chains.ts` - Canonical source of all chain names (auto-updated weekly)\n- `src/lib/types.ts` - Defines `ChainName` type derived from chains.ts\n- `src/data/networks/networks.ts` - Uses `chainName: ChainName`\n- `src/data/wallets/wallet-data.ts` - Uses `supported_chains: ChainName[]`\n\n**Rules:**\n\n1. **Always look up exact names** - Before adding `chainName` or `supported_chains`, search `chains.ts` for the exact `name` value\n2. **Names are case-sensitive and exact** - e.g., use `\"Zircuit Mainnet\"` not `\"Zircuit\"`, use `\"OP Mainnet\"` not `\"Optimism\"`\n3. **Run type checking** - Use `pnpm type-check` to verify chain names are valid before committing\n4. **Non-EVM chains** - For Starknet and other non-EVM chains, use `NonEVMChainName` type\n\n**Common Mistakes:**\n\n- Using informal names: `\"Optimism\"` should be `\"OP Mainnet\"`\n- Missing \"Mainnet\" suffix: `\"Zircuit\"` should be `\"Zircuit Mainnet\"`\n- Wrong casing: `\"zksync Mainnet\"` should be `\"zkSync Mainnet\"`\n\n## Key Dependencies to Know\n\n### UI & Styling\n\n- `@radix-ui/*` - Accessible component primitives\n- `tailwind-variants` - Component variant patterns\n- `motion` - Animation library (formerly framer-motion; import from `motion/react`)\n- `lucide-react` - Icon library\n\n### Content & Data\n\n- `gray-matter` - Frontmatter parsing\n- `recharts` - Data visualization\n\n### Ethereum Integration\n\n- `viem` - Ethereum library\n- `wagmi` - React hooks for Ethereum\n- `@rainbow-me/rainbowkit` - Wallet connection\n\n## A/B Testing\n\nThe site uses a GDPR-compliant, cookie-less A/B testing system built on the Flags SDK precompute pattern, integrated with Matomo. The proxy assigns variants at the edge (header fingerprinting) and rewrites to signed, statically prerendered variant pages under `ab-code/[code]/` — experiments never force dynamic rendering. Experiment status, weights, and scheduling live in the Matomo dashboard (no deploys needed). Only the default locale is tested; other locales always get the original.\n\nKey gotcha: **variants are matched by array index, not names** — the `variants` array order must match the Matomo experiment order exactly, and the `testKey` must match the Matomo experiment name exactly.\n\nFull guide (architecture, step-by-step recipe, env vars): `docs/ab-testing.md`. Code: `proxy.ts`, `src/lib/ab-testing/`, `src/components/AB/`, `app/[locale]/ab-code/`.\n\n## Site Search\n\nSearch runs on a self-hosted Typesense instance, one collection per locale, crawled by DocSearch from production ethereum.org. Publishing is gated: a crawl goes to a staging collection and the live alias only moves if the new index passes size, field and relevance checks.\n\nTwo things bite most often. **Ranking hints reach the index as `docsearch:*` meta tags** from `src/lib/utils/metadata.ts`, so a ranking change does nothing until the site is deployed and re-crawled. And **the networks offered as block explorer results come from `src/data/networks/networks.ts`** -- the same list `/layer-2/networks` renders -- so adding an L2 there is the only edit needed.\n\nOverview: `docs/site-search.md`. Runbook, secrets and recovery: `typesense/README.md`. Code: `src/components/Search/`, `src/scripts/typesense/`, `typesense/`.\n\n## Deployment\n\n- **Platform**: Netlify (config in `netlify.toml`)\n- **Next.js Integration**: Uses `@netlify/plugin-nextjs` for seamless Netlify and Next.js compatibility\n- **Monitoring**: Matomo analytics integration\n\n### Runtime constraints\n\n- **`public/content` is not in the Netlify server function** (CDN-only). A page that reads it at render time must stay static (`revalidate: false` getters like `getStaticAppsData`) or have an `outputFileTracingIncludes` entry in `next.config.js`. A finite-`revalidate` getter (e.g. `getEventsData`, 24h) turns the whole route into ISR. Details: `docs/solutions/integration-issues/netlify-isr-404-async-server-components.md`.\n\n## Internal Infrastructure\n\nThe following external-looking services are managed by the ethereum.org team:\n\n- **`s3-dcl1.ethquokkaops.io`** — S3-compatible object storage for app screenshots and media. Used by the data layer to serve images for the `/dapps` and app listing pages. Downtime here means broken images on the live site.\n- **Netlify Blobs** (`@netlify/blobs`) — Key-value store used by the data layer to cache API responses. Accessed via `src/data-layer/storage.ts`.\n",".claude/commands/netlify-build-check.md":"# Command: /netlify-build-check\n\nPath: `.claude/commands/netlify-build-check.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/netlify-build-check.md)",".claude/commands/prepare-release.md":"# Command: /prepare-release\n\nPath: `.claude/commands/prepare-release.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/prepare-release.md)",".claude/commands/review-release.md":"# Command: /review-release\n\nPath: `.claude/commands/review-release.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/review-release.md)",".claude/commands/review-translations.md":"# Command: /review-translations\n\nPath: `.claude/commands/review-translations.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/review-translations.md)",".claude/skills/design-system/evals/evals.json":"# Evals Skill\n\nPath: `.claude/skills/design-system/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/evals/evals.json)",".claude/skills/design-system/references/a11y.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/a11y.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/a11y.md)",".claude/skills/design-system/references/callout-walkthrough.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/callout-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/callout-walkthrough.md)",".claude/skills/design-system/references/canonical-imports.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/canonical-imports.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/canonical-imports.md)",".claude/skills/design-system/references/card-walkthrough.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/card-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/card-walkthrough.md)",".claude/skills/design-system/references/cleanup-playbook.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/cleanup-playbook.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/cleanup-playbook.md)",".claude/skills/design-system/references/components.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/components.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/components.md)",".claude/skills/design-system/references/gotchas.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/gotchas.md)",".claude/skills/design-system/references/i18n-rtl.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/i18n-rtl.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/i18n-rtl.md)",".claude/skills/design-system/references/layouts.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/layouts.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/layouts.md)",".claude/skills/design-system/references/new-component-checklist.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/new-component-checklist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/new-component-checklist.md)",".claude/skills/design-system/references/page-hero-walkthrough.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/page-hero-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/page-hero-walkthrough.md)",".claude/skills/design-system/references/server-vs-client.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/server-vs-client.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/server-vs-client.md)",".claude/skills/design-system/references/spacing-typography.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/spacing-typography.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/spacing-typography.md)",".claude/skills/design-system/references/tokens.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/tokens.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/tokens.md)",".claude/skills/design-system/references/variant-vs-new.md":"# References Skill\n\nPath: `.claude/skills/design-system/references/variant-vs-new.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/variant-vs-new.md)",".claude/skills/intl-pipeline/evals/evals.json":"# Evals Skill\n\nPath: `.claude/skills/intl-pipeline/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/evals/evals.json)",".claude/skills/intl-pipeline/references/architecture.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/architecture.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/architecture.md)",".claude/skills/intl-pipeline/references/ethglossary.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/ethglossary.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/ethglossary.md)",".claude/skills/intl-pipeline/references/gotchas.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/gotchas.md)",".claude/skills/intl-pipeline/references/manifests.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/manifests.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/manifests.md)",".claude/skills/intl-pipeline/references/non-english-edits.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/non-english-edits.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/non-english-edits.md)",".claude/skills/intl-pipeline/references/orchestration.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/orchestration.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/orchestration.md)",".claude/skills/intl-pipeline/references/recovery.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/recovery.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/recovery.md)",".claude/skills/intl-pipeline/references/sanitizer.md":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/sanitizer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/sanitizer.md)",".claude/skills/intl-review/evals/evals.json":"# Evals Skill\n\nPath: `.claude/skills/intl-review/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/evals/evals.json)",".claude/skills/intl-review/references/agent-roles.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/agent-roles.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/agent-roles.md)",".claude/skills/intl-review/references/critical-vs-warning.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/critical-vs-warning.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/critical-vs-warning.md)",".claude/skills/intl-review/references/ethglossary-usage.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/ethglossary-usage.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/ethglossary-usage.md)",".claude/skills/intl-review/references/gotchas.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/gotchas.md)",".claude/skills/intl-review/references/known-patterns.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/known-patterns.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/known-patterns.md)",".claude/skills/intl-review/references/language-rules.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/language-rules.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/language-rules.md)",".claude/skills/intl-review/references/per-language-tracking.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/per-language-tracking.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/per-language-tracking.md)",".claude/skills/intl-review/references/scoring-rubric.md":"# References Skill\n\nPath: `.claude/skills/intl-review/references/scoring-rubric.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/scoring-rubric.md)",".claude/skills/performance/references/build.md":"# References Skill\n\nPath: `.claude/skills/performance/references/build.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/build.md)",".claude/skills/performance/references/bundle.md":"# References Skill\n\nPath: `.claude/skills/performance/references/bundle.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/bundle.md)",".claude/skills/performance/references/diagnose-table.md":"# References Skill\n\nPath: `.claude/skills/performance/references/diagnose-table.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/diagnose-table.md)",".claude/skills/performance/references/edge-caching.md":"# References Skill\n\nPath: `.claude/skills/performance/references/edge-caching.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/edge-caching.md)",".claude/skills/performance/references/images.md":"# References Skill\n\nPath: `.claude/skills/performance/references/images.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/images.md)",".claude/skills/performance/references/inp.md":"# References Skill\n\nPath: `.claude/skills/performance/references/inp.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/inp.md)",".claude/skills/performance/references/review-checklist.md":"# References Skill\n\nPath: `.claude/skills/performance/references/review-checklist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/review-checklist.md)",".claude/skills/review-page/reviewers/copy.md":"# Reviewers Skill\n\nPath: `.claude/skills/review-page/reviewers/copy.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/reviewers/copy.md)",".claude/skills/review-page/reviewers/design-system.md":"# Reviewers Skill\n\nPath: `.claude/skills/review-page/reviewers/design-system.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/reviewers/design-system.md)",".claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md":"# Runbooks Skill\n\nPath: `.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md)",".claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md":"# Runbooks Skill\n\nPath: `.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# CLAUDE.md - Ethereum.org Website\n\n## Project Overview\n\nThis is the official Ethereum.org website - a Next.js application that serves as the primary educational and community hub for Ethereum. The site is built with modern web technologies and focuses on accessibility, internationalization, and performance.\n\n## Technology Stack\n\n### Core Framework\n\n- **Next.js 16+** - React framework with App Router (Turbopack is the default bundler; webpack available via `pnpm dev:webpack` / `pnpm build:webpack`)\n- **React 19** - UI library\n- **TypeScript 5.5+** - Type safety and development experience\n- **Tailwind CSS 4+** - Utility-first CSS framework (CSS-first config in `src/styles/global.css`)\n\n### Key Dependencies\n\n- **next-intl 4+** - Internationalization (i18n) with 25 languages\n- **next-mdx-remote 5.0+** - MDX content processing\n- **Motion 12+** (`motion` package, formerly Framer Motion) - Animations and transitions; import from `motion/react`\n- **Radix UI** - Accessible component primitives\n- **shadcn/ui** - Component library built on Radix UI\n- **Recharts** - Data visualization\n- **Viem/Wagmi** - Ethereum blockchain integration\n\n### Development & Testing\n\n- **Storybook 10+** - Component development and testing\n- **Chromatic** - Visual regression testing\n- **ESLint** - Code linting with custom rules\n- **Prettier** - Code formatting\n- **Husky** - Git hooks\n- **PNPM** - Package manager\n\n## Project Structure\n\n- **app/** - Next.js App Router pages\n  - **[locale]/** - Internationalized routes\n- **src/**\n  - **components/** - React components\n    - **ui/** - Design system components\n    - **icons/** - SVG icon components\n  - **data/** - Static data and configurations\n  - **hooks/** - Custom React hooks\n  - **i18n/** - Internationalization config\n  - **intl/** - Translation files (25 languages)\n  - **layouts/** - Page layout components\n  - **lib/** - Utility functions and types\n    - **constants.ts** - App constants\n    - **types.ts** - TypeScript type definitions\n    - **utils/** - Utility functions\n  - **styles/** - Global styles and design tokens\n- **public/** - Static assets\n  - **content/** - Markdown content files\n  - **images/** - Image assets\n- **docs/** - Development documentation\n  - **solutions/** - Documented solutions to past problems, organized by category with YAML frontmatter (module, tags, problem_type)\n\n## Code Conventions\n\n### File Naming\n\n- **Components**: kebab-case (e.g., `button-group.tsx`)\n- **Utilities**: camelCase (e.g., `cn.ts`, `relativePath.ts`)\n- **Pages**: kebab-case following Next.js conventions\n- **Assets**: kebab-case (e.g., `eth-logo.png`)\n\n### TypeScript Patterns\n\n- Use `interface` for object shapes, `type` for unions/intersections\n- Prefer explicit typing over `any` (ESLint enforces `fixToUnknown`)\n- **NEVER leave unused variables or parameters** - ESLint `unused-imports/no-unused-vars` will fail the Netlify build. The only allowed unused arg pattern is a single underscore `_`. Do NOT use `_prefixedNames` (e.g., `_foo`) - either use the variable or remove it from the signature entirely.\n- Use generic constraints for reusable components\n- Export types from dedicated files in `@/lib/types`\n\n### Styling Conventions\n\n- **Primary approach**: Tailwind CSS utility classes\n- **Component variants**: Use `tailwind-variants` (`tv`) for new and refactored work. Existing `class-variance-authority` (`cva`) components don't need bulk migration -- swap to `tv` opportunistically when you're already touching the component for another reason.\n- **Dynamic classes**: Use `cn()` utility (clsx + tailwind-merge)\n- **Custom properties**: CSS variables in `src/styles/` for theme values\n- **Responsive design**: Mobile-first approach\n\nFor UI work, see the **`design-system` skill** at `.claude/skills/design-system/`. It's the canonical knowledge base for component choices, design tokens, RTL/i18n, server/client boundaries, and the \"use a variant, not a new component\" pattern.\n\n## Development Workflows\n\n### Available Scripts\n\n```bash\n# Development\npnpm dev                    # Start development server (Turbopack)\npnpm dev:webpack            # Start development server with webpack\npnpm build                  # Build for production (Turbopack)\npnpm build:webpack          # Build for production with webpack\npnpm start                  # Start production server\n\n# Code Quality\npnpm lint                   # Run ESLint\npnpm lint:fix              # Fix ESLint issues\npnpm type-check            # TypeScript type checking\npnpm format                # Format with Prettier\n\n# Testing\npnpm test:unit             # Playwright unit tests (unit project)\npnpm test:e2e              # Playwright end-to-end tests\npnpm test:visual           # Playwright + Chromatic full-page visual tests\n\n# Storybook\npnpm storybook             # Start Storybook dev server\npnpm build-storybook       # Build Storybook\npnpm chromatic             # Storybook Chromatic (needs CHROMATIC_STORYBOOK_TOKEN)\n\n# Content Management\npnpm lint:md               # Lint English markdown content\npnpm lint:md:fix           # Auto-fix header IDs and duplicates\n```\n\n### Testing Strategy\n\n- **Unit Testing**: Playwright `unit` project (`pnpm test:unit`) — heaviest coverage is the intl-pipeline sanitizer\n- **E2E Testing**: Playwright (`pnpm test:e2e`), see `docs/e2e-testing.md`\n- **Visual Testing**: Storybook + Chromatic for component regression; Playwright + Chromatic for full pages (see the `page-visual-tests` skill)\n- **Type Safety**: TypeScript strict mode enabled\n- **Linting**: ESLint with custom rules for imports and TypeScript\n\n## Content Management\n\n### Internationalization\n\n- **25 languages** supported (canonical list: `i18n.config.json`); **RTL support** for Arabic, Urdu\n- JSON UI strings in `src/intl/[locale]/`; translated markdown content in `public/content/translations/[locale]/`\n- Non-English markdown is propagated by the **intl-pipeline** (`src/scripts/intl-pipeline/`, entry `main.ts`). **Do not hand-propagate English changes into non-English files** -- let the pipeline run, or trigger `intl-pipeline.yml` with `stamp_only: true` if manifests must catch up urgently (e.g. unblocking a build). Hand-fixing a translation error is fine when the English side hasn't moved, since the manifest mapping stays valid. Spec: `tests/specs/PIPELINE-SPEC.md`.\n- Glossary: base URL from `GLOSSARY_API_URL` env var; default in `src/scripts/intl-pipeline/config.ts`. ETHGlossary is authoritative for Ethereum term translations.\n\nFor pipeline mechanics, recovery, manifests, ETHGlossary integration, and the `intl/pending-{base}` orchestration model, see the **`intl-pipeline` skill** at `.claude/skills/intl-pipeline/`. For translation-quality review (scoring rubric, language-group rules, ETHGlossary-as-authority policy, multi-agent role split), see the **`intl-review` skill** at `.claude/skills/intl-review/`.\n\n### Markdown Content\n\n- Educational content stored in `public/content/`\n- Processed with `next-mdx-remote`\n- Custom MDX components for rich content\n- Automatic table of contents generation\n- **All h1-h4 headings require a custom `{#lower-kebab-id}`** -- enforced by markdownlint via pre-commit hook. Run `pnpm lint:md:fix` to auto-add missing IDs. Config: `.markdownlint-cli2.jsonc`, custom rules: `.markdownlint-rules/`\n\n### Asset Management\n\n- Images optimized with Next.js Image component\n- SVGs loaded as React components via `@svgr/webpack`\n- Static assets served from `public/`\n- Placeholder generation for images\n\n## SEO & Meta\n\n- Sitemap generation in `app/sitemaps/sitemap.ts` (one sitemap per locale, indexed by `app/sitemap.xml/route.ts`)\n- Meta tags and Open Graph optimization\n- Structured data for search engines\n- Security headers (X-Frame-Options: DENY)\n\n## Development Guidelines\n\n### When Working on Features\n\n1. **Check existing patterns** - Look at similar components first\n2. **Prioritize Server Components** - Use App Router and Server Components when possible\n3. **Follow import order** - ESLint will enforce, but be proactive\n4. **Use TypeScript strictly** - No `any` types, prefer `unknown`\n5. **Test in Storybook** - Create stories for new components (filename pattern: `.stories.tsx`)\n6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations(\"common\")`) rather than reaching across namespaces\n7. **Mobile-first** - Design for mobile, enhance for desktop\n8. **Accessibility** - Use Radix primitives, semantic HTML\n9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales.\n\n### Component Development\n\n1. Create component in appropriate `src/components/` subdirectory\n   - Use `src/components/ui` for shadcn components or pure UI components\n2. Add TypeScript types and proper props interface\n3. Accept `ref` as a regular prop when needed (React 19); `forwardRef` only survives in legacy components\n4. Add a Storybook story (see below)\n5. Export from appropriate index file\n6. Update documentation if adding new patterns\n\n### Storybook stories\n\n**Location**: `__stories__/` for flat directories (`ui/`, `styles/`), co-located for foldered components. Story globs are recursive, so nesting works. **Filenames are kebab-case** (`app-card.stories.tsx`) even inside a PascalCase component directory -- enforced by the test below.\n\n**Title**: `Section / Group / Name`. The section is derived from the file path -- not a judgment call:\n\n| Path                                  | Section         |\n| ------------------------------------- | --------------- |\n| `src/styles/**`                       | `Design System` |\n| `src/components/ui/**`                | `UI`            |\n| `src/components/**` (everything else) | `Components`    |\n| `src/layouts/**`                      | `Layouts`       |\n| `app/**`                              | `Pages`         |\n\nUse a second-level group only when an established one fits (Actions, Forms, Layout, Navigation, Overlays, Data Display, Cards, Heroes, Site Chrome, Content, Data Viz, Features). **Don't invent a group for a single component** -- a flat `Components / Morpher` is correct and preferred. `tests/unit/storybook/story-titles.spec.ts` enforces the section prefix, explicit titles, and title uniqueness (Storybook silently merges duplicate titles into one sidebar entry).\n\n**Don't add a story for a deprecated component or variant.** A rendered example reads as an endorsement whatever the caption says, so the showcase should only contain things that are available to use -- note the deprecation in prose instead (see `HR`). A component whose future is merely undecided can keep its story with a status blurb (see `Carousel`).\n\nAdd `tags: [\"autodocs\"]`. Write the `parameters.docs.description.component` blurb for someone deciding whether to use the thing -- constraints and gotchas, not a restatement of the props table.\n\n**A green build does not mean a story renders.** `pnpm build-storybook` compiles stories without executing them, and every `UI /` story sets `chromatic: { disableSnapshot: true }`, so neither CI signal catches a story that throws. Open new stories in `pnpm storybook` before pushing.\n\nAsync server components render via a `next-intl/server` shim (`.storybook/next-intl-server.tsx`). Components calling `usePathname` need `parameters.nextjs.navigation.pathname`. New i18n namespaces must be added to the `ns` array in `.storybook/next-intl.ts`, or strings render as raw key tails.\n\n### Content Updates\n\n1. Markdown files go in `public/content/`\n2. Images in `public/images/` with descriptive names\n3. Translation strings in appropriate `src/intl/` JSON files\n4. Data files in `src/data/` with TypeScript types\n\n### Type-Safe Chain Names\n\nThis project enforces type-safe chain names via TypeScript. When working with layer 2 networks or wallet data:\n\n**Critical Files:**\n\n- `src/data/chains.ts` - Canonical source of all chain names (auto-updated weekly)\n- `src/lib/types.ts` - Defines `ChainName` type derived from chains.ts\n- `src/data/networks/networks.ts` - Uses `chainName: ChainName`\n- `src/data/wallets/wallet-data.ts` - Uses `supported_chains: ChainName[]`\n\n**Rules:**\n\n1. **Always look up exact names** - Before adding `chainName` or `supported_chains`, search `chains.ts` for the exact `name` value\n2. **Names are case-sensitive and exact** - e.g., use `\"Zircuit Mainnet\"` not `\"Zircuit\"`, use `\"OP Mainnet\"` not `\"Optimism\"`\n3. **Run type checking** - Use `pnpm type-check` to verify chain names are valid before committing\n4. **Non-EVM chains** - For Starknet and other non-EVM chains, use `NonEVMChainName` type\n\n**Common Mistakes:**\n\n- Using informal names: `\"Optimism\"` should be `\"OP Mainnet\"`\n- Missing \"Mainnet\" suffix: `\"Zircuit\"` should be `\"Zircuit Mainnet\"`\n- Wrong casing: `\"zksync Mainnet\"` should be `\"zkSync Mainnet\"`\n\n## Key Dependencies to Know\n\n### UI & Styling\n\n- `@radix-ui/*` - Accessible component primitives\n- `tailwind-variants` - Component variant patterns\n- `motion` - Animation library (formerly framer-motion; import from `motion/react`)\n- `lucide-react` - Icon library\n\n### Content & Data\n\n- `gray-matter` - Frontmatter parsing\n- `recharts` - Data visualization\n\n### Ethereum Integration\n\n- `viem` - Ethereum library\n- `wagmi` - React hooks for Ethereum\n- `@rainbow-me/rainbowkit` - Wallet connection\n\n## A/B Testing\n\nThe site uses a GDPR-compliant, cookie-less A/B testing system built on the Flags SDK precompute pattern, integrated with Matomo. The proxy assigns variants at the edge (header fingerprinting) and rewrites to signed, statically prerendered variant pages under `ab-code/[code]/` — experiments never force dynamic rendering. Experiment status, weights, and scheduling live in the Matomo dashboard (no deploys needed). Only the default locale is tested; other locales always get the original.\n\nKey gotcha: **variants are matched by array index, not names** — the `variants` array order must match the Matomo experiment order exactly, and the `testKey` must match the Matomo experiment name exactly.\n\nFull guide (architecture, step-by-step recipe, env vars): `docs/ab-testing.md`. Code: `proxy.ts`, `src/lib/ab-testing/`, `src/components/AB/`, `app/[locale]/ab-code/`.\n\n## Site Search\n\nSearch runs on a self-hosted Typesense instance, one collection per locale, crawled by DocSearch from production ethereum.org. Publishing is gated: a crawl goes to a staging collection and the live alias only moves if the new index passes size, field and relevance checks.\n\nTwo things bite most often. **Ranking hints reach the index as `docsearch:*` meta tags** from `src/lib/utils/metadata.ts`, so a ranking change does nothing until the site is deployed and re-crawled. And **the networks offered as block explorer results come from `src/data/networks/networks.ts`** -- the same list `/layer-2/networks` renders -- so adding an L2 there is the only edit needed.\n\nOverview: `docs/site-search.md`. Runbook, secrets and recovery: `typesense/README.md`. Code: `src/components/Search/`, `src/scripts/typesense/`, `typesense/`.\n\n## Deployment\n\n- **Platform**: Netlify (config in `netlify.toml`)\n- **Next.js Integration**: Uses `@netlify/plugin-nextjs` for seamless Netlify and Next.js compatibility\n- **Monitoring**: Matomo analytics integration\n\n### Runtime constraints\n\n- **`public/content` is not in the Netlify server function** (CDN-only). A page that reads it at render time must stay static (`revalidate: false` getters like `getStaticAppsData`) or have an `outputFileTracingIncludes` entry in `next.config.js`. A finite-`revalidate` getter (e.g. `getEventsData`, 24h) turns the whole route into ISR. Details: `docs/solutions/integration-issues/netlify-isr-404-async-server-components.md`.\n\n## Internal Infrastructure\n\nThe following external-looking services are managed by the ethereum.org team:\n\n- **`s3-dcl1.ethquokkaops.io`** — S3-compatible object storage for app screenshots and media. Used by the data layer to serve images for the `/dapps` and app listing pages. Downtime here means broken images on the live site.\n- **Netlify Blobs** (`@netlify/blobs`) — Key-value store used by the data layer to cache API responses. Accessed via `src/data-layer/storage.ts`.\n","isInternal":false,"tokens":3977,"sizeBytes":16657},{"name":"SKILL.md","path":".claude/skills/data-layer/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/data-layer/SKILL.md","title":"data-layer","category":"anthropic-skill","format":"markdown","content":"---\nname: data-layer\ndescription: This skill provides patterns for working with the data-layer module. Use when creating/editing files in src/data-layer/, src/lib/data/, or adding new data sources.\n---\n\n# Data Layer\n\n## Architecture\n\n```\nsrc/data-layer/\n├── fetchers/         # Fetch functions (one per data source)\n│   └── developer-tools/  # Multi-file fetcher (builder resources, GitHub/npm stats, ranking)\n├── index.ts          # Public API - typed getter functions\n├── tasks.ts          # Trigger.dev scheduled tasks\n├── storage.ts        # KEYS constant + get/set (Netlify Blobs or mock files)\n├── s3.ts             # S3 image upload utility for external images\n├── docs.md           # Module documentation\n├── mocks/            # Mock data files for local development\n└── .env.example      # Environment variables for data-layer/Trigger.dev\n\nsrc/lib/data/\n└── index.ts          # Next.js caching adapter (createCachedGetter)\n```\n\n## Environment Variables\n\nThe data-layer uses a **dedicated `.env.local`** at `src/data-layer/.env.local`, separate from the root `.env.local`: `cp src/data-layer/.env.example src/data-layer/.env.local`, fill in the keys (see `.env.example` for all options), then `pnpm trigger:dev` runs tasks locally. `GITHUB_TOKEN_READ_ONLY` and Sentry vars are shared with the main app (configure in both files); everything else (API keys, Netlify Blobs tokens, S3 credentials, Trigger.dev config) is data-layer only. In production, configure vars in the Trigger.dev project dashboard — the app and data-layer run in separate environments.\n\n## Key Files\n\n`storage.ts` defines the `KEYS` constant, `tasks.ts` the `WEEKLY`/`DAILY`/`HOURLY` task tuples; `index.ts` holds the one-liner getters — snippets for both under \"Adding a New Data Source\" below.\n\n### storage.ts - Storage Abstraction\n\n`get<T>(key)` / `set(key, data)` switch between Netlify Blobs (prod) and local mock JSON files (`USE_MOCK_DATA=true` for local development).\n\n### s3.ts - Image Upload Utility\n\nCentralized S3 upload for external images. Fetchers use this to upload external images to a single S3 bucket, reducing Next.js `remotePatterns` complexity.\n\n```typescript\n// Upload single image\nconst s3Url = await uploadToS3(sourceUrl, \"events/logos\")\n\n// Batch upload (parallel)\nconst s3Urls = await uploadManyToS3(urls, \"apps/banners\")\n```\n\nKey features:\n\n- **SSRF protection** - Blocks private/internal network addresses\n- **Deduplication** - SHA256 hash of source URL as key\n- **Existence check** - Skips if already uploaded\n- **5MB size limit** - Returns `null` for large images\n- **Content-Type detection** - From header or URL extension fallback\n\n## Rules\n\n### 1. Getters must be pure passthrough\n\nNo transformations in `index.ts` - just `get<T>(KEYS.X)`:\n\n```typescript\n// Correct\nexport const getEventsData = () => get<EventItem[]>(KEYS.EVENTS)\n\n// Wrong - no transformations in getters\nexport const getEventsData = () => {\n  const data = await get<EventItem[]>(KEYS.EVENTS)\n  return data?.map(transform) ?? null\n}\n```\n\nAll transformations belong in the fetcher (`src/data-layer/fetchers/`).\n\n### 2. KEYS is the single source of truth\n\nAll task IDs are defined in `KEYS` in `storage.ts`. The getter in `index.ts` and the task tuple in `WEEKLY`/`DAILY`/`HOURLY` must use the same key.\n\n### 3. Expose via lib/data for caching\n\nAdd cached wrapper in `src/lib/data/index.ts`:\n\n```typescript\nexport const getEventsData = createCachedGetter(\n  dataLayer.getEventsData,\n  [\"events-data\"],\n  CACHE_REVALIDATE_DAY // or CACHE_REVALIDATE_HOUR\n)\n```\n\nThe `revalidate` parameter is `number | false`. Passing `false` is a deliberate pattern to keep a route fully static — a finite revalidate opts the page into ISR, which fails on Netlify for pages reading `public/content/` files. Example: `getStaticAppsData` in `src/lib/data/index.ts`, used by components embedded in MDX pages (data refreshes only on deploy).\n\n### 4. Use S3 for external images\n\nExternal images should be uploaded to S3 in the fetcher to centralize image domains:\n\n```typescript\n// In fetcher - correct\nimport { uploadToS3 } from \"../s3\"\n\nconst logoUrl = await uploadToS3(event.logoImage, \"events/logos\")\nreturn { ...event, logoImage: logoUrl ?? \"\" }\n```\n\nAlways handle `null` returns (upload failures) with fallback/empty string.\n\n### 5. Keep fetchers isolated from the app\n\nFetchers run on Trigger.dev — a separate runtime, deployment, and bundle from the Next.js app. They cannot assume the app's filesystem, environment, or modules are available.\n\nAny import or runtime dependency reaching outside `src/data-layer/` is a warning sign. Allowed: types (`@/lib/types`, `@/lib/interfaces`), pure constants (`@/lib/constants`), and pure utility functions with no app-runtime dependencies. Not allowed: anything that reads `process.cwd()`, anything from `app/` or `public/`, anything from `src/components/`, or `src/lib/data/` (which wraps the data layer and would create a cycle).\n\nIf a fetcher needs data that lives in the app — content files, frontmatter, etc. — fetch it over the network via the GitHub API and treat the repo as an external system. See `fetchGitHubContributors.ts` for the pattern. Don't work around this with `additionalFiles` in `trigger.config.ts`; bundling app files into the data-layer deployment re-creates the coupling.\n\n## Adding a New Data Source\n\n1. **Create fetcher** in `src/data-layer/fetchers/fetchNewData.ts`:\n\n   ```typescript\n   export async function fetchNewData(): Promise<YourDataType> {\n     // Fetch and transform data here\n   }\n   ```\n\n2. **Add key** to `KEYS` in `src/data-layer/storage.ts`:\n\n   ```typescript\n   export const KEYS = {\n     // ...existing keys\n     NEW_DATA: \"fetch-new-data\",\n   } as const\n   ```\n\n3. **Add task tuple** to `WEEKLY`, `DAILY`, or `HOURLY` in `tasks.ts` (getter and tuple must use the same `KEYS` entry — Rule 2):\n\n   ```typescript\n   const DAILY: TaskDef[] = [\n     // ...existing tasks\n     [KEYS.NEW_DATA, fetchNewData],\n   ]\n   ```\n\n4. **Add getter** in `src/data-layer/index.ts`:\n\n   ```typescript\n   export const getNewData = () => get<YourDataType>(KEYS.NEW_DATA)\n   ```\n\n5. **Add mock file** at `src/data-layer/mocks/fetch-new-data.json` (read when `USE_MOCK_DATA=true`)\n\n6. **Add cached wrapper** in `src/lib/data/index.ts`:\n   ```typescript\n   export const getNewData = createCachedGetter(\n     dataLayer.getNewData,\n     [\"new-data\"],\n     CACHE_REVALIDATE_HOUR\n   )\n   ```\n","frontmatter":{"name":"data-layer","description":"This skill provides patterns for working with the data-layer module. Use when creating/editing files in src/data-layer/, src/lib/data/, or adding new data sources."},"isInternal":false,"tokens":1629,"sizeBytes":6490},{"name":"evals.json","path":".claude/skills/design-system/evals/evals.json","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/evals/evals.json","title":"Evals Skill","category":"anthropic-skill","format":"json","content":"# Evals Skill\n\nPath: `.claude/skills/design-system/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/evals/evals.json)","tokens":300,"sizeBytes":214},{"name":"a11y.md","path":".claude/skills/design-system/references/a11y.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/a11y.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/a11y.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/a11y.md)","tokens":300,"sizeBytes":223},{"name":"callout-walkthrough.md","path":".claude/skills/design-system/references/callout-walkthrough.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/callout-walkthrough.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/callout-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/callout-walkthrough.md)","tokens":300,"sizeBytes":253},{"name":"canonical-imports.md","path":".claude/skills/design-system/references/canonical-imports.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/canonical-imports.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/canonical-imports.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/canonical-imports.md)","tokens":300,"sizeBytes":249},{"name":"card-walkthrough.md","path":".claude/skills/design-system/references/card-walkthrough.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/card-walkthrough.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/card-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/card-walkthrough.md)","tokens":300,"sizeBytes":247},{"name":"cleanup-playbook.md","path":".claude/skills/design-system/references/cleanup-playbook.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/cleanup-playbook.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/cleanup-playbook.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/cleanup-playbook.md)","tokens":300,"sizeBytes":247},{"name":"components.md","path":".claude/skills/design-system/references/components.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/components.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/components.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/components.md)","tokens":300,"sizeBytes":235},{"name":"gotchas.md","path":".claude/skills/design-system/references/gotchas.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/gotchas.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/gotchas.md)","tokens":300,"sizeBytes":229},{"name":"i18n-rtl.md","path":".claude/skills/design-system/references/i18n-rtl.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/i18n-rtl.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/i18n-rtl.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/i18n-rtl.md)","tokens":300,"sizeBytes":231},{"name":"layouts.md","path":".claude/skills/design-system/references/layouts.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/layouts.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/layouts.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/layouts.md)","tokens":300,"sizeBytes":229},{"name":"new-component-checklist.md","path":".claude/skills/design-system/references/new-component-checklist.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/new-component-checklist.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/new-component-checklist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/new-component-checklist.md)","tokens":300,"sizeBytes":261},{"name":"page-hero-walkthrough.md","path":".claude/skills/design-system/references/page-hero-walkthrough.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/page-hero-walkthrough.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/page-hero-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/page-hero-walkthrough.md)","tokens":300,"sizeBytes":257},{"name":"server-vs-client.md","path":".claude/skills/design-system/references/server-vs-client.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/server-vs-client.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/server-vs-client.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/server-vs-client.md)","tokens":300,"sizeBytes":247},{"name":"spacing-typography.md","path":".claude/skills/design-system/references/spacing-typography.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/spacing-typography.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/spacing-typography.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/spacing-typography.md)","tokens":300,"sizeBytes":251},{"name":"tokens.md","path":".claude/skills/design-system/references/tokens.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/tokens.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/tokens.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/tokens.md)","tokens":300,"sizeBytes":227},{"name":"variant-vs-new.md","path":".claude/skills/design-system/references/variant-vs-new.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/variant-vs-new.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/design-system/references/variant-vs-new.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/variant-vs-new.md)","tokens":300,"sizeBytes":243},{"name":"SKILL.md","path":".claude/skills/design-system/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/SKILL.md","title":"design-system","category":"anthropic-skill","format":"markdown","content":"---\nname: design-system\ndescription: Use when building, refactoring, or styling any UI in the ethereum.org Next.js site (`src/components/`, `app/`, `src/styles/`, `public/content/`, or any `.tsx`/`.mdx`/`.css` change that affects the rendered UI). Provides canonical component choices, design tokens, RTL/i18n rules, server/client guidance, and the \"use a variant, not a new component\" pattern for the project's Tailwind v4 + Radix + shadcn-style design system.\n---\n\n# ethereum.org Design System\n\nTailwind v4 (CSS-first config, no `tailwind.config.ts`) + React 19 / Next.js App Router + Radix UI primitives + shadcn-style component layer. Tokens live in CSS. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Habit: Reuse Over Reinvent\n\nThe single highest-leverage habit for keeping this codebase consistent: **when you need new UI, look for a primitive or variant first, only invent if nothing fits.** Most \"new component\" instincts are actually \"new variant\" instincts in disguise.\n\nBefore you write any UI code, ask:\n- Is there a primitive that already does this? (`Card`, `Button`, `Alert`, `Tag`, `Hero/*`)\n- Is the difference small enough to express as a *variant* on an existing primitive?\n- Can I compose existing primitives instead of inlining a long Tailwind class chain?\n\nIf you find yourself writing `flex items-center gap-X rounded-Y border bg-... p-Z` for a card-like thing, you're reinventing `<Card>`. If you write `<p className=\"text-4xl font-bold\">N</p>` for a stat, you're reinventing `<BigNumber>`. If you write `<div className=\"text-5xl font-bold\">Title</div>`, you're reinventing `<h1>` (which is already styled by `base.css`). **Compose, don't inline.**\n\nWhen the existing primitive doesn't quite fit, the answer is usually \"add a variant,\" not \"create a new file.\" See `references/variant-vs-new.md`.\n\n## Top Rules\n\n1. **No raw `<a>` or `<button>`.** Use `<Button>`/`<ButtonLink>` from `@/components/ui/buttons/Button` and `InlineLink`/`BaseLink`/`LinkWithArrow` from `@/components/ui/Link`. These primitives handle event tracking, external-link safety, locale routing, and focus rings.\n2. **No raw color values.** Use semantic tokens (`text-body`, `bg-background`, `border-border`, `text-primary`). Hex literals and `rgb()` calls bypass dark mode.\n3. **Prefer adding a variant** to an existing primitive over creating a new component. Card, Button, Alert, Tag are the most common targets.\n4. **Server Components by default.** Only `\"use client\"` when you need state, effects, browser APIs, or inline event handlers.\n5. **All text is translatable.** `getTranslations` from `next-intl/server` (server) or `useTranslations` from `next-intl` (client). One namespace-bound `t` per namespace -- bind a second function (e.g. `const tCommon = useTranslations(\"common\")`) to access another namespace. The legacy `@/hooks/useTranslation` wrapper is deprecated for new code. Never hard-code user-facing English. **In an `app/[locale]/` page or `generateMetadata`, call `setRequestLocale(locale)` before any next-intl API** or on-demand renders throw `static to dynamic ... reason: headers` -- see `references/i18n-rtl.md`.\n6. **Logical CSS for direction.** Use `ms-`/`me-`/`ps-`/`pe-`/`inset-s-`/`inset-e-`/`border-s`/`border-e`/`text-start`/`text-end`. The site supports Arabic and Urdu (RTL). Hard-coded `left-`/`right-`/`ml-`/`mr-`/`pl-`/`pr-` breaks RTL.\n7. **Locale-aware formatters.** `numberFormat()` from `@/lib/utils/numbers`, `dateTimeFormat()` from `@/lib/utils/date`. Never `toLocaleString` / `Intl.NumberFormat` directly.\n8. **`useRtlFlip()` for directional icons** (right-pointing arrows/chevrons). Or use `ChevronNext`/`ChevronPrev` from `@/components/Chevron`.\n9. **Markdown content goes through `MdComponents`.** The `<Card>` markdown shortcode is backed by `@/components/MarkdownCard` (a thin wrapper around the `ui/card` primitives with an MDX-friendly prop shape). For app code, compose the primitives directly from `@/components/ui/card`.\n10. **Storybook stories ship with new UI components.** No automated unit tests; Storybook + Chromatic + types are the verification layer.\n11. **Don't add new layouts.** There are six canonical layouts (`TopicLayout`, `StaticLayout`, `DocsLayout`, `TutorialLayout`, `ContentLayout`, `BaseLayout`). New sectioned content goes in `src/data/topics/<key>.ts` as a `TopicLayout` config -- not a new layout component. See `references/layouts.md`.\n\n## Highest-Value Gotchas (index)\n\nLandmines where the code looks reasonable but the pattern is wrong. One line each; the canonical write-ups live in the named reference.\n\n- **Look-alike imports** -- Tooltip (`@/components/Tooltip`, not `ui/tooltip`), Modal (`ui/dialog-modal` vs `ui/dialog`; don't mix sources within a feature), Card (`ui/card` for app code, not `MarkdownCard`), Heroes (named exports of `@/components/Hero`; `title` is always the `<h1>`, no `header` prop). Decision trees: `references/canonical-imports.md`.\n- **Stale shadcn token names** (`bg-popover`, `bg-muted`, `text-muted-foreground`, ...) linger in a few `ui/` files but don't resolve in this project's tokens. Don't add new uses. List: `references/tokens.md`; replacements: `references/cleanup-playbook.md`.\n- **`useColorModeValue` is a deprecated Chakra leftover** -- use `dark:` + semantic tokens.\n- **`Card` is variant-driven, not `className`-driven.** Padding/spacing/background/radius/text color go through `variant`/`size`; a `className` override means a missing variant case. Full system: `references/card-walkthrough.md`. Surprise behaviors (`isSecondary` no-op on `solid`, `CardBanner fit=\"contain\"` auto-backdrop, `LinkBox` needs `LinkOverlay`, `commonControlClasses` shared with `Switch`): `references/gotchas.md`.\n- **No `Heading` primitive** -- `base.css` styles `<h1>`-`<h6>`; just write the tag. To match a heading level's size anywhere, use `text-h1`-`text-h6` (size + line-height only) -- never the raw `text-3xl lg:text-4xl` pair, and never re-apply a weight on a real heading. Canonical write-up: `references/spacing-typography.md`.\n- **Spacing is `.flow` + the `page`/`space`/`hero` tokens**, not hand-rolled margins. App pages follow a `<main className=\"p-page\"> > <MainArticle className=\"flow\"> > <Section id>` skeleton. Details: `references/spacing-typography.md`; token table: `references/tokens.md`.\n- **Locale formatting** -- one stray `toLocaleString` remains in `ui/chart.tsx`; don't add more. Use `numberFormat()`.\n\n### Shadows: default to the Tailwind scale\n\nPick by surface: dropdowns/tooltips `shadow-md`, cards/popovers/modals `shadow-lg`, large framed boxes/sheets `shadow-xl`. Three rules:\n\n1. Only two custom shadows exist (`shadow-primary-xl`, `shadow-primary-no-blur-*`, both in `utilities.css`) -- anything new needs a brand-tint justification.\n2. Hover elevation is the `hover-lift-*` utility or `Card hoverLift` (both `motion-safe`-gated), never a per-component resting/hover shadow pair.\n3. A genuinely-needed custom shadow is a raw `box-shadow` `@utility`, never an arbitrary `shadow-[...]` -- arbitrary shadows route color through `--tw-shadow-color`, which the global `* { dark:shadow-body }` rule grays out in dark mode.\n\nOld-shadow-token mapping table: `references/cleanup-playbook.md`.\n\n## Quick \"Where Do I Import From?\" Cheatsheet\n\n| I need... | Import |\n|---|---|\n| Card | `import { Card, CardBanner, CardContent, CardTitle, CardParagraph } from \"@/components/ui/card\"` |\n| Modal/Dialog (typical) | `import Modal from \"@/components/ui/dialog-modal\"` (default export) |\n| Side sheet | `import { Sheet, ... } from \"@/components/ui/sheet\"` |\n| Tooltip | `import Tooltip from \"@/components/Tooltip\"` (NOT `@/components/ui/tooltip`) |\n| Button | `import { Button, ButtonLink } from \"@/components/ui/buttons/Button\"` |\n| Anchor (in prose) | `import InlineLink from \"@/components/ui/Link\"` (default) |\n| Anchor (CTA with arrow) | `import { LinkWithArrow } from \"@/components/ui/Link\"` |\n| Page hero | `import { PageHero, HubHero } from \"@/components/Hero\"` |\n| Inline alert | `import { Alert, AlertContent, AlertDescription } from \"@/components/ui/alert\"` |\n| Top-of-page banner | `import { Alert } from \"@/components/ui/alert\"` then `<Alert variant=\"banner\">` |\n| Big numeric display | `import BigNumber from \"@/components/BigNumber\"` |\n| Layout | `import { Stack, HStack, VStack, Flex, Center } from \"@/components/ui/flex\"` |\n| Number formatting | `import { numberFormat } from \"@/lib/utils/numbers\"` |\n| Date formatting | `import { dateTimeFormat } from \"@/lib/utils/date\"` |\n| RTL flip helper | `import { useRtlFlip } from \"@/hooks/useRtlFlip\"` |\n\nFor full decision trees with all the look-alike landmines, see `references/canonical-imports.md`.\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies. Don't read them all upfront.\n\n- **`references/canonical-imports.md`** -- unsure which of several look-alike imports is canonical.\n- **`references/components.md`** -- the full component inventory with variants and usage.\n- **`references/tokens.md`** -- adding a token, gradients, z-index, semantic-token choices, work in `src/styles/`.\n- **`references/spacing-typography.md`** -- page/section layout, heading sizes, spacing rhythm, the `.flow` system, citation and footnote markers.\n- **`references/gotchas.md`** -- unexpected primitive behavior; the long-tail confusion patterns.\n- **`references/variant-vs-new.md`** -- before creating any new component file.\n- **`references/cleanup-playbook.md`** -- refactoring existing anti-patterns; the \"old pattern -> new pattern\" map.\n- **`references/i18n-rtl.md`** -- user-facing text, number/date formatting, RTL, translation keys, `setRequestLocale`.\n- **`references/server-vs-client.md`** -- `\"use client\"` decisions and SSR-boundary structure.\n- **`references/a11y.md`** -- interactive elements, forms, images, heading hierarchy.\n- **`references/card-walkthrough.md`** -- any card-shaped UI work.\n- **`references/callout-walkthrough.md`** -- adding or modifying an in-content `Callout`.\n- **`references/page-hero-walkthrough.md`** -- a new page that needs a hero.\n- **`references/layouts.md`** -- layout selection, new topic hubs, one-off layout refactors.\n- **`references/new-component-checklist.md`** -- before opening a PR for a new component.\n\n## Other Project Skills That May Apply\n\n- **`data-layer`** -- For data fetching/sources. UI work that needs data should compose with this.\n\n## Pre-Merge Smoke Test\n\nBefore opening a PR for any UI work:\n\n- [ ] No raw `<a>` or `<button>`\n- [ ] No hard-coded colors (`#hex`, `rgb()`, `hsla()`); semantic tokens only\n- [ ] No `left-`/`right-`/`ml-`/`mr-`/`pl-`/`pr-` (use logical equivalents)\n- [ ] All user-facing strings translatable\n- [ ] `numberFormat()`/`dateTimeFormat()` for formatting (not native APIs)\n- [ ] Server Components wherever possible\n- [ ] New UI primitives have a `.stories.tsx`\n- [ ] Headings use `<h1>`-`<h6>` (not `<div className=\"text-5xl font-bold\">`)\n- [ ] If introducing a new component, justify why it isn't a variant of an existing one\n","frontmatter":{"name":"design-system","description":"Use when building, refactoring, or styling any UI in the ethereum.org Next.js site (`src/components/`, `app/`, `src/styles/`, `public/content/`, or any `.tsx`/`.mdx`/`.css` change that affects the rendered UI). Provides canonical component choices, design tokens, RTL/i18n rules, server/client guidance, and the \"use a variant, not a new component\" pattern for the project's Tailwind v4 + Radix + shadcn-style design system."},"isInternal":false,"tokens":2862,"sizeBytes":11079},{"name":"SKILL.md","path":".claude/skills/image-gen/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/image-gen/SKILL.md","title":"image-gen","category":"anthropic-skill","format":"markdown","content":"---\nname: image-gen\ndescription: Generate illustration images (heroes, decorations, section art) for ethereum.org via the OpenRouter image API, then key/trim/check and place them in the repo. Trigger on \"make an image for\", \"generate a hero\", \"new illustration for <page>\", \"fix the glyph\", \"redraw this image\", \"chroma key this\", or edits to `src/scripts/image-gen/`. Not for screenshots, logos, or brand assets.\n---\n\n# Image generation\n\nThe user asks in chat (\"make a hero for /privacy\"); Claude drives the flow below. The script is Claude's tool for the parts that need the key or pixel math: it wraps OpenRouter's `POST /api/v1/images` and the post-processing. Three subcommands:\n\n```bash\npnpm tsx src/scripts/image-gen/main.ts models [--filter gemini]          # live params per model (aspect, resolution, background, n, ref limit)\npnpm tsx src/scripts/image-gen/main.ts generate --type cutout|full-bleed --prompt-file <f> [--ref <img>]... [--model <id>] [--aspect 16:9] [--quality <q>] [--eth-glyph auto|on|off] [--n 1] [--name <slug>] [--out-dir <dir>] [--dry-run]\npnpm tsx src/scripts/image-gen/main.ts process <img> --type cutout|full-bleed --out <file> [--key-color green|magenta] [--pad 0] [--max-size <px>]\n```\n\n## The key\n\n`OPENROUTER_API_KEY` lives in `.env.local`, loaded by the script itself. **Never read `.env.local`** (`.claude/settings.json` denies it; `.env.example` is fine to read). Never echo, log, or pass the key on a command line. Each person uses their own OpenRouter key with a spend limit set.\n\n**If a key ever appears in your context** (tool output, an error body, a file you read), tell the user right away that it was exposed and should be revoked and replaced at openrouter.ai/settings/keys, without repeating the key. Then carry on with the task; rotation happens separately.\n\n## Workflow\n\n1. **Context.** Read the target page and component, then decide the type. If the page doesn't settle it, ask with AskUserQuestion; never guess.\n   - **cutout** -- subject on transparency, nothing touching any edge (`PageHero` `heroImg`, section decorations).\n   - **full-bleed** -- scene fills the frame edge to edge (`HubHero` backgrounds, banners).\n2. **References.** Pick 2-6 existing images that match the target style (look at them first); see **House style** below. More refs pull harder toward house style; mismatched refs muddy it.\n3. **Prompt.** Write the subject/scene and style only. The script appends the cutout or full-bleed constraints (key color, margins, no borders) and, when the prompt mentions Ethereum, the ETH glyph spec and reference (see **The ETH glyph**), so don't restate them.\n4. **Confirm before spending.** Show the prompt, refs, model, aspect, and `--dry-run` output. Generate only after the user agrees, and get a fresh go-ahead for every further call; agreement to one generation isn't agreement to the next. A call costs about $0.11-0.26 and the price follows output pixels (21:9 is the cheapest, 1:1 and 3:2 the most).\n5. **Generate** one image per call: `--n 1`, never higher unless the user asks for a batch. Use a stable kebab `--name` describing the subject (see **Naming** below); reuse it for every iteration on that target, even if the subject drifts. Don't pass `--out-dir`: the default archive, `/tmp/image-gen/<name>/`, sits outside the repo and the session scratchpad, so candidates survive across iterations and sessions. Numbering continues (`-1`, `-2`, ...) and nothing is ever overwritten. Each candidate gets `<name>-<i>-raw.*`, a processed `<name>-<i>.png`, and `<name>-<i>.json` (prompt, refs, params, cost, check results).\n6. **Review.** Look at every output. `CHECK` lines are automatic failures (subject touching an edge, flat border strip, background not a key color); regenerate those rather than placing them. Then zoom in (crop at 3-4x) on every ETH glyph and every structure the user has flagged before, and check them against **The ETH glyph** and **Compositions that fail**. Report defects you find rather than letting the user discover them.\n7. **Place in situ, automatically.** Don't wait for a pick: put the best passing candidate at its target location right away so the user can judge it on the real page in their running dev server.\n   - `process` the archived raw (or `cp` the archived `.png`) to its final path, `public/images/<area>/<descriptive-name>.png`, sized to roughly 2x its largest rendered width with `--max-size`. Cutouts are trimmed to zero margin (`--pad 0`, the default); the page sets the spacing. Run a lossless PNG optimizer (oxipng) on the result.\n   - If the target also uses a portrait version on small viewports (a `*-portrait.png` beside it, e.g. some `HubHero` backgrounds), crop it from the landscape image rather than generating it separately, so both stay identical.\n   - Wire it into the target component (camelCase raster import, see the design-system skill's `page-hero-walkthrough.md`). If it replaces an existing image, repoint the import and leave the old file alone, so reverting is just `git checkout` on the component.\n   - Tell the user the URL to check (e.g. `/organizations/`) and list the other candidates by path.\n   - Never commit, and never start a dev server; the user runs their own.\n8. **Iterate in place.** Each new trial is copied from the archive over the same in-repo file, so the page updates without touching the import again. Static imports pick up the change through their content hash; for a plain `/images/...` string `src`, tell the user to hard-refresh. Revise by adding a candidate as a `--ref` with an updated prompt. To go back to an earlier candidate (\"I prefer the first one\"), copy that archived file in; never regenerate it. Always say which archive number is currently in situ.\n9. **Never delete the archive.** Leave `/tmp/image-gen/<name>/` alone, even after the image is placed. The user decides when it's done. When the user is happy, show `git status` so they can see exactly which files changed.\n\n## Editing an existing image\n\nMost work after the first generation is fixing details. Doing it well:\n\n- **Fix only what the user flagged.** Starting over or \"improving\" unflagged areas changes sizes, colors and objects the user had already accepted, and is usually worse.\n- **Redraw faithfully.** Pass the approved image as the sole (first) `--ref` and open with \"Redraw the reference image faithfully: identical camera, framing, <list every element>, same colors, same linework. Change only <X>: <exact description of the fix>. Everything else stays exactly as it is.\" OpenAI reproduces the rest closely.\n- **Take the whole redraw.** Patching just the fixed region back into the old image fails: a redraw shifts edges by around 10px. If everything else matches, place the full redraw.\n- **Check the neighbors.** Removing or changing one element can break an adjacent one (removing an elevator shaft truncated the next one). Compare the whole image against the previous version before placing.\n- **If the user supplies an edited version, it becomes the source.** Archive it in `/tmp/image-gen/<name>/`, place it, and use it as ref 1 for any later redraw. Never assume the user can edit images; for a detail the model keeps getting wrong, a manual edit can be suggested, not expected.\n\n## House style\n\nethereum.org illustrations are hand-drawn, airy and utopian. Good references: `public/images/home/hero.png` (plus a full-resolution crop of its palms and strokes, which shows the line texture better than the whole image), `public/images/heroes/developers-hub-hero.png`, `learn-hub-hero.png` and `guides-hub-hero.jpg`. Avoid the roadmap, garden and quizzes heroes as references; they lean purple and pink.\n\nCues that work in prompts:\n\n- Lines: hand-drawn in cobalt and royal blue, never black, lighter periwinkle for distant details. Gentle crayon quality: \"lines waver a little in weight and have slightly soft, lightly textured edges, fills carry a faint paper grain; subtle and clean, mostly crisp, not scratchy\". Full crayon wording overshoots into grain the user doesn't want (aim for about 75% clean, 25% texture).\n- Color: mostly white and pale blue with peach, orange and cyan accents; red awnings are fine. Flat fills, no gradients, glow or halos.\n- Architecture: sci-fi, white and rounded, slender columns, curved balconies, glass as pale blue panels with thin white grid lines, thin peach accent bands, rooftop plants. Classical forms (columns, pediments, domes) stay but in that white style. No brick or boxy present-day buildings.\n- Ground: white or pale-blue tiles; floating slab edges in pale periwinkle, not brown earth.\n- Plants: palms with feathery fronds of thin cobalt and periwinkle strokes, in white planters. No green, no round deciduous trees.\n- People: simple outlined figures, flat-colored clothes, simple hair, low detail.\n- Always add \"No text, letters, numbers or currency symbols anywhere, no logos other than the Ethereum diamond, no UI labels.\" Clock faces get no numbers.\n\n## The ETH glyph\n\nImage models get the Ethereum diamond wrong more than anything else: one solid rhombus, a white fill or a line bridging the gap, an extra lattice of facets, a glyph rotated off the surface it sits on, or a smudge on a small coin. The **shape** must match `src/components/icons/eth-home-icon.svg` exactly. Its **colors don't matter**: they can change to suit the design (a coin imprint in coin tones, a white glyph on a pediment).\n\nThe shape, for checking outputs and describing fixes (the script's reference renders it as unfilled outlines for this reason):\n\n- Two separate pieces stacked vertically, with an open gap between them where the background shows through.\n- Upper piece: a tall kite with exactly four facets. Its side corners sit about halfway down the whole glyph and its bottom point about two-thirds down. A center line runs top to bottom, and a line from each side corner rises to meet it about a third of the way down.\n- Lower piece: a downward chevron with exactly two facets. Its top edge is a shallow V parallel to the upper piece's bottom edges, and its tips sit just below the upper piece's corners.\n- About 1.6 times as tall as wide, symmetric, the same width top and bottom. On a tilted or foreshortened surface it follows that surface's perspective and keeps the same structure.\n\nHow the script helps: with `--eth-glyph auto` (the default), any prompt that mentions Ethereum, ETH, a glyph or a diamond gets the detailed shape spec (`src/scripts/image-gen/eth-glyph.ts`) appended and a rendering of the canonical glyph attached as the **last** reference image. User refs keep their order, so \"the first reference image\" still means yours; never call the glyph anything but \"the last reference image\" in your own prompt. Use `--eth-glyph on` to force it, `off` when the image has no glyph and the prompt only names Ethereum in passing, or when the reference slot is needed.\n\nIn prompts, also:\n\n- Ask for few glyphs, each drawn large. A glyph below about 15px in the output, or lying on foreshortened paper, is unreadable at any prompt fidelity; leave it off small coins and badges.\n- Say where each glyph sits and which way it faces (\"upright and centered at the top of the certificate\", \"lying flat in the center of the floor square, aligned with its edges\").\n- Don't ask for glows, highlights or \"dimensional\" facets on it; they turn into extra lines inside the shape.\n- To fix a wrong glyph on an otherwise approved image, redraw faithfully (see **Editing an existing image**) with \"Change only the Ethereum diamond on <where>\" and its position, size and facing; the spec and reference supply the shape.\n\n## Compositions that fail\n\nThese cost several failed generations each. Design them out in the prompt instead of fighting them:\n\n- **Merging or branching rail tracks:** the model can't draw a clean switch, and \"hide it under a train\" produced two misaligned trains. Use a single continuous track.\n- **Hard occlusion:** legs under tables, columns behind foreground objects, a bridge running behind a floating object. Use a solid-front desk or counter that reaches the floor, a single colonnade, fewer overlapping layers.\n- **Repeated structure:** column rows, colonnades and dome drums come out unevenly spaced. Spell it out (\"exactly four identical columns at exactly equal spacing on the same base line\") and check spacing at zoom.\n- **Impossible layering:** floors that don't continue behind objects, staircases that loop. Keep depth simple: one island, one level change.\n\n## Naming\n\nName images for what they depict, never for where they're used: a town square with people used as the `/some-page/` hero is `town-square-buildings-with-people.png`, not `some-page-hero.png`. Placement changes; content doesn't. Kebab-case, specific enough to tell it apart from neighbors in the same folder. The `--name` passed to `generate` follows the same rule, so the archive folder and the final file match. If the final image ends up depicting something different from the original name, rename the in-repo file to match what's in the image (the archive folder keeps its name).\n\n## Models\n\nDefault `openai/gpt-image-2.5-sunburst` at the highest `quality` it offers. It matches the house style (clothing, palms, architecture, ground, people, flat color) where `google/gemini-3-pro-image` (Nano Banana Pro) kept drawing black vector lines, purple and red casts and ordinary buildings despite explicit prompts. Switch with `--model` only when the user asks; run `models` to see what a model accepts -- the script validates flags against that list and drops options a model lacks.\n\n- **Cutouts:** OpenAI supports `background: transparent`, so the script requests real alpha and skips keying. Models without it get a flat key background that the script removes.\n- **Size:** OpenRouter exposes no size parameter; output tops out around 1536px on the long side (21:9 comes back 1536x656). For a wide hero that needs more, a local Real-ESRGAN upscale reads better on the page than the raw 1536px, though it flattens some crayon texture.\n- `n` is 1 per call on Gemini; `--n 3` makes three calls. Models that accept a higher `n` batch it.\n\n## Chroma keying\n\nOnly for models without native alpha, or for chroma images made elsewhere.\n\n- Green (`#00FF00`) by default. Use `--key-color magenta` when the subject itself is green or teal; a green subject keyed on green turns partly transparent.\n- Alpha comes from how strongly a pixel leans toward the sampled border color, then the background is unmixed from partially transparent pixels. Despill applies only to a 2px rim near transparency, so interior colors are untouched.\n- Prefer PNG sources. JPEG compression smears the key into dark outlines, which leaves a faint tinted rim.\n- `process` works on any existing chroma image, e.g. one generated by hand on the OpenRouter site.\n","frontmatter":{"name":"image-gen","description":"Generate illustration images (heroes, decorations, section art) for ethereum.org via the OpenRouter image API, then key/trim/check and place them in the repo. Trigger on \"make an image for\", \"generate a hero\", \"new illustration for <page>\", \"fix the glyph\", \"redraw this image\", \"chroma key this\", or edits to `src/scripts/image-gen/`. Not for screenshots, logos, or brand assets."},"isInternal":false,"tokens":3470,"sizeBytes":14826},{"name":"evals.json","path":".claude/skills/intl-pipeline/evals/evals.json","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/evals/evals.json","title":"Evals Skill","category":"anthropic-skill","format":"json","content":"# Evals Skill\n\nPath: `.claude/skills/intl-pipeline/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/evals/evals.json)","tokens":300,"sizeBytes":214},{"name":"architecture.md","path":".claude/skills/intl-pipeline/references/architecture.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/architecture.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/architecture.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/architecture.md)","tokens":300,"sizeBytes":239},{"name":"ethglossary.md","path":".claude/skills/intl-pipeline/references/ethglossary.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/ethglossary.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/ethglossary.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/ethglossary.md)","tokens":300,"sizeBytes":237},{"name":"gotchas.md","path":".claude/skills/intl-pipeline/references/gotchas.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/gotchas.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/gotchas.md)","tokens":300,"sizeBytes":229},{"name":"manifests.md","path":".claude/skills/intl-pipeline/references/manifests.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/manifests.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/manifests.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/manifests.md)","tokens":300,"sizeBytes":233},{"name":"non-english-edits.md","path":".claude/skills/intl-pipeline/references/non-english-edits.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/non-english-edits.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/non-english-edits.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/non-english-edits.md)","tokens":300,"sizeBytes":249},{"name":"orchestration.md","path":".claude/skills/intl-pipeline/references/orchestration.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/orchestration.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/orchestration.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/orchestration.md)","tokens":300,"sizeBytes":241},{"name":"recovery.md","path":".claude/skills/intl-pipeline/references/recovery.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/recovery.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/recovery.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/recovery.md)","tokens":300,"sizeBytes":231},{"name":"cost-guard-tripped.md","path":".claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md","title":"Runbooks Skill","category":"anthropic-skill","format":"markdown","content":"# Runbooks Skill\n\nPath: `.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md)","tokens":300,"sizeBytes":267},{"name":"fix-sanitizer-bug.md","path":".claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md","title":"Runbooks Skill","category":"anthropic-skill","format":"markdown","content":"# Runbooks Skill\n\nPath: `.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md)","tokens":300,"sizeBytes":265},{"name":"sanitizer.md","path":".claude/skills/intl-pipeline/references/sanitizer.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/sanitizer.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/sanitizer.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/sanitizer.md)","tokens":300,"sizeBytes":233},{"name":"SKILL.md","path":".claude/skills/intl-pipeline/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/SKILL.md","title":"intl-pipeline","category":"anthropic-skill","format":"markdown","content":"---\nname: intl-pipeline\ndescription: Use when working on the translation pipeline (`src/scripts/intl-pipeline/`), the post-import sanitizer (`intl-sanitizer.ts`), the `intl/pending-*` branches, the `intl-pipeline.yml` GitHub Actions workflow, ETHGlossary integration, or any translation-related issue (broken translations, missing locales, manifest drift, sanitizer false positives, ETHGlossary term lookups, language-group transliteration questions). Provides the manifest-driven incremental pipeline mental model, the \"don't hand-propagate\" rule, recovery procedures, the ETHGlossary-as-canonical-source policy, and the `intl/pending-{base}` orchestration contract.\n---\n\n# intl-pipeline\n\nLLM-based, manifest-driven, incremental translation pipeline for ethereum.org. Translates English content (markdown in `public/content/`, JSON UI strings in `src/intl/en/`) into 24 target languages via Gemini — reached either directly or through OpenRouter (`LLM_PROVIDER`), whose keys carry a server-side spend limit — with a post-import sanitizer normalizing common artifacts. **The workflow is manual-dispatch only**; the daily cron was removed after a run billed $1,108 unattended (`references/runbooks/cost-guard-tripped.md`). ETHGlossary is the authoritative source for all Ethereum-ecosystem term translations consumed by the pipeline. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Rule: Don't Hand-Propagate English Changes\n\nThe single highest-leverage habit for keeping translations correct: **never hand-edit translated content to reflect an English change.** The pipeline tracks state via manifests (`.manifests/{destPath}/source.json` + `translation.json` per file+locale); hand-edits that follow an English change desynchronize the manifest from reality, and the next pipeline run either re-translates over your edit or produces merge conflicts.\n\nThe rule is NOT \"never edit locales.\" It IS \"don't hand-propagate English updates\":\n\n- **Allowed:** Fixing a translation error when the English side hasn't moved (review-time corrections, sanitizer follow-ups). The manifest's English-to-locale map stays valid.\n- **Not allowed:** Editing a locale to reflect a new English value (URL change, attribute change, restructured paragraph). The manifest map becomes wrong.\n- **If English-to-locale sync is genuinely urgent** (build-breaking structural change with no pending PR open): make the English edit, then trigger `intl-pipeline.yml` with `stamp_only: true` to refresh manifests without translation. Safe only when no `intl/pending-{base}` branch exists for that base.\n\n**Deletions go through English too.** To remove a string — including an orphaned key code no longer references — delete it from the English source only; the pipeline propagates the removal on its next run. Hand-deleting the locale copies is hand-propagation.\n\n## Top Rules\n\n1. **ETHGlossary is canonical for term translations.** Brand names, people's names, programming languages, OS/platform names, concept terms — all live in ETHGlossary. Don't maintain parallel term banks. The pipeline queries the API via `GLOSSARY_API_URL` (default in `src/scripts/intl-pipeline/config.ts`).\n2. **One translation PR at a time per base branch.** The pipeline commits to `intl/pending-{base}` (e.g., `intl/pending-dev`). Subsequent runs merge `{base}` into pending first, then translate the delta. Parallel translation PRs against the same base will conflict.\n3. **The pipeline only targets `dev` in production.** Hot fixes to `staging` / `master` go out English-only and catch up via prepare-release. Don't translate against `staging` / `master` unless you have a specific reason and use a custom `target_branch`.\n4. **Manifests are inseparable from their locale file.** Each translated file has two manifests, centralized under `.manifests/{destPath}/`: `source.json` and `translation.json`. Delete one, you must regenerate both — easiest via the pipeline in `full` mode for that file+locale. Never hand-edit a manifest.\n5. **The sanitizer runs post-translation, not pre.** Its job is to fix Gemini-introduced artifacts (BiDi mistakes, code-fence drift, brand-name mistranslations). It receives translation outputs, never English source.\n6. **Don't add transliteration data here.** All term/brand/person transliteration policy lives in ETHGlossary's `docs/translation-policy.md` and per-language entries. The intl-pipeline consumes; it does not author.\n7. **Sanitizer fixes must split on code blocks first.** Every text transformation in `intl-sanitizer.ts` MUST start with the code-block split pattern. Modifying code-fence contents breaks Solidity / Python / TypeScript examples in tutorials.\n8. **An untagged code fence is code.** Fence bodies never reach the model; only a prose-tagged fence (```` ```text ````, `txt`, `md`, `markdown`, `mdx` -- `PROSE_FENCE_TAGS` in `lib/shared-patterns.ts`) is translatable. To make a fence's prose translatable, tag it ```` ```text ```` in the English source. `code-fence-content` fails any other fence whose body drifts from English, comments excepted.\n9. **Pipeline failures are not always pipeline bugs.** A \"translation looks wrong\" report may be: bad Gemini output (file upstream), missing ETHGlossary term (add there), correct per language-group policy (read translation-policy.md), or an actual pipeline bug. Triage before patching.\n\n## Highest-Value Gotchas\n\nThese are landmines where the obvious-looking action is wrong. The full set is in `references/gotchas.md`; these come up most often.\n\n### Naming legacy\n\nOld names are dead — `src/scripts/i18n/`, `gemini-translations.yml`, `main-incremental.ts`, \"i18n pipeline\"; the canonical name everywhere is `intl-pipeline`. Update old names on sight.\n\n### Manifests are centralized under `.manifests/`, not next to the locale file\n\nTwo manifests per file+locale at `.manifests/{destPath}/source.json` + `translation.json` (e.g. `.manifests/src/intl/ja/common.json/source.json`). Structure, lifecycle, and debugging: `references/manifests.md`.\n\n### Spend bounds are input-side, and the tell is input tokens per call\n\nHealthy runs average 4k–9k input tokens per call; the input:output ratio is NOT a signal. Before merging anything that touches batching, prompt assembly, or context selection, run `pnpm intl:estimate` and compare per-request input tokens against that range. Bounds, triage, and the incident that motivated them: `references/runbooks/cost-guard-tripped.md`.\n\n### `intl/pending-{base}` branch lifecycle\n\nThe pipeline creates `intl/pending-{base}` on first run, commits translations to it, opens a PR against `{base}`. Subsequent runs MERGE `{base}` into pending first, then translate the delta. After the pending PR is merged, the branch is deleted; the next run creates a fresh one.\n\nDo NOT rebase, squash, or force-push `intl/pending-{base}`. The pipeline depends on its history.\n\n### SOV-language inline element reordering\n\nFor Korean, Urdu, and other SOV (subject-object-verb) languages, inline elements (links, inline code, JSX components) often appear in REVERSE order vs. English. The pipeline's inert-propagation pass matches elements by **value**, not by position. Any logic that assumes positional order will silently corrupt those languages.\n\n### JSX attribute translation is a separate pass\n\nJSX attribute values are NOT translated in the main Phase 4 LLM call. Phase 4b is a dedicated pass with an allow-list of translatable attribute names (`title`, `description`, `alt`, `label`, `aria-label`, `placeholder`, etc.) defined in `src/scripts/intl-pipeline/lib/shared-patterns.ts`. Touching attribute translation means touching that pass, not Phase 4.\n\n### The sanitizer runs inside each task, before the gates\n\nSince the gates landed, `runSanitizer` is called per task on that task's own output (`sanitizeAndGate` in `main.ts`), then the gates judge the sanitized bytes, then content and manifest are recorded together. There is no post-squash sanitize commit any more; a sanitizer change is exercised on the next run's tasks, not on a branch sweep.\n\n### Sanitizer test scope is per-file, never per-language sweep\n\nNEVER run the sanitizer against an entire language. It processes thousands of files and hangs for 30+ minutes. There is no per-file env var — the only env-based scoping is per-language via `TARGET_LANGUAGES` (e.g. `TARGET_LANGUAGES=ja`), which still sweeps that whole language. Scope to specific files programmatically by calling the exported `runSanitizer(filesWithContent)` from `intl-sanitizer.ts` with just the affected file(s). The slash command `/fix-sanitizer-bug` enforces this; if you script around it, preserve the constraint.\n\n## Quick \"Where Do I Look?\" Cheatsheet\n\n| I need...                               | Path                                                                                  |\n| --------------------------------------- | ------------------------------------------------------------------------------------- |\n| Pipeline entry                          | `src/scripts/intl-pipeline/main.ts`                                                   |\n| Sanitizer                               | `src/scripts/intl-pipeline/intl-sanitizer.ts`                                         |\n| Gemini adapter                          | `src/scripts/intl-pipeline/lib/llm/gemini.ts`                                         |\n| OpenRouter transport                    | `src/scripts/intl-pipeline/lib/llm/openrouter.ts`                                     |\n| Adapter registry / provider selection   | `src/scripts/intl-pipeline/lib/llm/adapters.ts`, `constants.ts`                       |\n| Spend bounds (budget + run fuse)        | `src/scripts/intl-pipeline/lib/llm/cost-meter.ts`                                     |\n| Pre-commit gates (structure, MDX, JSON) | `src/scripts/intl-pipeline/lib/gates.ts`                                              |\n| Deterministic-failure quarantine        | `src/scripts/intl-pipeline/lib/quarantine.ts`, `.manifests/quarantine.json`           |\n| Work planner (shared with estimate)     | `src/scripts/intl-pipeline/lib/llm/plan.ts`                                           |\n| Cost estimate, no LLM calls             | `pnpm intl:estimate` (`src/scripts/intl-pipeline/estimate.ts`)                        |\n| Structural gate, standalone CLI         | `src/scripts/intl-pipeline/verify-structure.ts` (checks in `lib/verify-structure.ts`) |\n| One-off repairs (never run by a run)    | `migrate-manifests.ts`, `repair-anchors.ts` -- see \"One-off repair scripts\"           |\n| Prompt builder                          | `src/scripts/intl-pipeline/lib/llm/prompt-builder.ts`                                 |\n| Content normalizer                      | `src/scripts/intl-pipeline/lib/llm/content-normalizer.ts`                             |\n| Shared patterns (JSX attrs allow-list)  | `src/scripts/intl-pipeline/lib/shared-patterns.ts`                                    |\n| Glossary config                         | `src/scripts/intl-pipeline/config.ts`                                                 |\n| Workflow file                           | `.github/workflows/intl-pipeline.yml`                                                 |\n| Per-file pipeline spec (canonical)      | `tests/specs/PIPELINE-SPEC.md`                                                        |\n| Concurrency / chunking spec             | `tests/specs/CONCURRENCY-SPEC.md`                                                     |\n| Test fixture mutation table             | `tests/specs/SPEC.md`                                                                 |\n| Sanitizer test suite                    | `tests/unit/intl-pipeline/sanitizer/`                                                 |\n| Pipeline test suite                     | `tests/unit/intl-pipeline/`                                                           |\n| Future-work backlog                     | `src/scripts/intl-pipeline/FUTURE.md`                                                 |\n| Language config (canonical list)        | `i18n.config.json`                                                                    |\n| ETHGlossary repo                        | https://github.com/ethereum/ethglossary                                               |\n| ETHGlossary API root                    | https://glossary.ethereum.org                                                         |\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies. Don't read them all upfront.\n\n- **`references/architecture.md`** — debugging pipeline behavior; the phase-by-phase walkthrough.\n- **`references/manifests.md`** — manifests misbehave, or changing what the pipeline tracks.\n- **`references/orchestration.md`** — working with `intl/pending-{base}` branches.\n- **`references/recovery.md`** — translations broken, triaging \"the pipeline did something wrong.\"\n- **`references/sanitizer.md`** — sanitizer behavior, false positives/negatives, fix-function catalog.\n- **`references/runbooks/fix-sanitizer-bug.md`** — confirmed sanitizer bug; when (not) to run `/fix-sanitizer-bug`.\n- **`references/runbooks/cost-guard-tripped.md`** — `[cost-guard]` aborts, budget skips, cost-bound design, provider selection.\n- **`references/ethglossary.md`** — term lookups, `script_rule`/`term_role` semantics, missing terms.\n- **`references/non-english-edits.md`** — about to hand-edit a translated file.\n- **`references/gotchas.md`** — something feels off and it isn't inline above.\n\n## Other Project Skills That May Apply\n\n- **`intl-review`** — For translation quality review (scoring rubric, language-group rules, brand-name policies, per-language findings). The review side of the same coin; the pipeline produces, intl-review evaluates.\n- **`data-layer`** — For data fetching, if pipeline changes touch externally-sourced content.\n\n## Pre-Merge Smoke Test\n\nBefore opening a PR that touches the pipeline:\n\n- [ ] Sanitizer test suite passes (`npx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/`)\n- [ ] Pipeline test suite passes (`npx playwright test --project=unit tests/unit/intl-pipeline/`)\n- [ ] No hand-edits to `public/content/translations/` or `src/intl/{non-en}/` files\n- [ ] No manual changes to manifest files under `.manifests/`\n- [ ] If renaming workflow / config paths, all doc references updated (search for old names)\n- [ ] If adding pipeline-affecting code, `src/scripts/intl-pipeline/FUTURE.md` updated (or item removed if completed)\n- [ ] If new sanitizer fix function, the code-block-split pattern is the first operation inside it\n- [ ] If touching term policy or transliteration, change goes in ETHGlossary first — never duplicate term data here\n- [ ] If touching the orchestration model (pending branch, temp branch, stamp_only), `references/orchestration.md` updated alongside\n- [ ] If touching batching, prompt assembly, or context selection: `pnpm intl:estimate` run and per-request input tokens sane (4k–9k), plus `tests/unit/intl-pipeline/cost-incident.spec.ts` passing\n- [ ] No new LLM call site that bypasses `callGeminiRaw` — it is the single choke point where the per-call ceiling and run fuse are enforced\n","frontmatter":{"name":"intl-pipeline","description":"Use when working on the translation pipeline (`src/scripts/intl-pipeline/`), the post-import sanitizer (`intl-sanitizer.ts`), the `intl/pending-*` branches, the `intl-pipeline.yml` GitHub Actions workflow, ETHGlossary integration, or any translation-related issue (broken translations, missing locales, manifest drift, sanitizer false positives, ETHGlossary term lookups, language-group transliteration questions). Provides the manifest-driven incremental pipeline mental model, the \"don't hand-propagate\" rule, recovery procedures, the ETHGlossary-as-canonical-source policy, and the `intl/pending-{base}` orchestration contract."},"isInternal":false,"tokens":3261,"sizeBytes":15248},{"name":"evals.json","path":".claude/skills/intl-review/evals/evals.json","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/evals/evals.json","title":"Evals Skill","category":"anthropic-skill","format":"json","content":"# Evals Skill\n\nPath: `.claude/skills/intl-review/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/evals/evals.json)","tokens":300,"sizeBytes":210},{"name":"agent-roles.md","path":".claude/skills/intl-review/references/agent-roles.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/agent-roles.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-review/references/agent-roles.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/agent-roles.md)","tokens":300,"sizeBytes":233},{"name":"critical-vs-warning.md","path":".claude/skills/intl-review/references/critical-vs-warning.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/critical-vs-warning.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-review/references/critical-vs-warning.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/critical-vs-warning.md)","tokens":300,"sizeBytes":249},{"name":"ethglossary-usage.md","path":".claude/skills/intl-review/references/ethglossary-usage.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/ethglossary-usage.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-review/references/ethglossary-usage.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/ethglossary-usage.md)","tokens":300,"sizeBytes":245},{"name":"gotchas.md","path":".claude/skills/intl-review/references/gotchas.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/gotchas.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-review/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/gotchas.md)","tokens":300,"sizeBytes":225},{"name":"known-patterns.md","path":".claude/skills/intl-review/references/known-patterns.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/known-patterns.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-review/references/known-patterns.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/known-patterns.md)","tokens":300,"sizeBytes":239},{"name":"language-rules.md","path":".claude/skills/intl-review/references/language-rules.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/language-rules.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-review/references/language-rules.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/language-rules.md)","tokens":300,"sizeBytes":239},{"name":"per-language-tracking.md","path":".claude/skills/intl-review/references/per-language-tracking.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/per-language-tracking.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-review/references/per-language-tracking.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/per-language-tracking.md)","tokens":300,"sizeBytes":253},{"name":"scoring-rubric.md","path":".claude/skills/intl-review/references/scoring-rubric.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/scoring-rubric.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/intl-review/references/scoring-rubric.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/references/scoring-rubric.md)","tokens":300,"sizeBytes":239},{"name":"SKILL.md","path":".claude/skills/intl-review/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-review/SKILL.md","title":"intl-review","category":"anthropic-skill","format":"markdown","content":"---\nname: intl-review\ndescription: Use when reviewing translation imports/PRs (especially against `intl/pending-*` branches), evaluating translation quality across any of the 24 target languages, triaging issues in `.claude/translation-review/`, scoring a locale's translation, running the `/review-translations` slash command, debugging brand-name mistranslations or glossary deviations, or working with the per-language findings tracking files. Covers the scoring rubric, ETHGlossary-as-authority policy, language-group rules, agent-role split (structural / terminology / semantic), the concept-tag-vs-brand-tag distinction in frontmatter, and the critical-vs-warning severity policy.\n---\n\n# intl-review\n\nTranslation-quality review for ethereum.org's 24-language pipeline output. Reviews target the LLM (Gemini)-produced translations on `intl/pending-{base}` branches plus any historical Crowdin imports still in flight. ETHGlossary (https://glossary.ethereum.org) is the authoritative source for terminology — deviations are **critical issues**, not warnings. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Rule: ETHGlossary Is Authority\n\nWhen evaluating a translated brand name, person name, programming language, OS name, or any Ethereum-ecosystem term, **the ETHGlossary entry for that term in that language is the truth**. Deviations are **critical** issues that must be flagged (and auto-fixed when running `/review-translations` without `--no-fix`).\n\nThis is not a stylistic preference — it's a determinism guarantee. The translation pipeline queries ETHGlossary; reviewers verify the output matches. If you think the glossary is wrong, the fix is to update ETHGlossary (https://github.com/ethereum/ethglossary), not to leave the translation as-is.\n\nUse the `/filter` endpoint to get the subset of glossary terms that actually appear in a given English source file. Don't reason from memory about brand names; look them up.\n\n## Top Rules\n\n1. **Glossary deviations are CRITICAL, not warnings.** Any time a translated term differs from the ETHGlossary entry for that language, that's a critical issue. The review report must list them so Phase 5 can auto-fix.\n2. **Internal hrefs must stay English.** `/governance` in Spanish content must be `/governance`, not `/gobernanza`. Translated URLs break navigation. Same for anchor IDs (`#section-id`).\n3. **Concept tags translate; brand-name tags don't.** Tutorial frontmatter `tags:` arrays contain a mix. Brand names (`\"solidity\"`, `\"hardhat\"`, `\"alchemy\"`) stay English. Concept/category tags (`\"smart contracts\"`, `\"testing\"`, `\"security\"`) are intentionally translated by the pipeline and must NOT be reverted to English.\n4. **Code blocks: functional code stays English; comments may be translated.** Identifiers, strings, config keys, console output — English. Code comments (`//`, `/* */`, `#`) may be translated to aid reader comprehension.\n5. **Brand-name policy is per-language, per-term.** Some brands stay Latin in CJK (Solidity, Hardhat per ETHGlossary's `script_rule: always_latin`); some transliterate (Ethereum → イーサリアム per `transliterate`); some have native calques in `zh`/`zh-tw`. Use the glossary; don't pattern-match.\n6. **MDX syntax errors are CRITICAL (build-breaking).** Raw `<` before numbers, unclosed backticks, orphaned closing tags, JSX attributes with embedded unescaped quotes — any of these breaks the build for that locale. Flag and fix.\n7. **Reporting zero critical issues is a valid outcome.** Don't invent issues to \"show your work.\" If a thorough review surfaces no criticals, report `0 critical, N warnings` (or `0/0`) and that's a valid result.\n8. **Auto-fix is opt-out, not opt-in.** `/review-translations` applies fixes by default. Use `--no-fix` for review-only runs (GitHub Actions context, no commit auth). The review report still lists what would have been fixed.\n9. **All review changes go on the `intl/pending-*` branch being reviewed — NEVER `dev`.** Translation fixes AND knowledge-base updates (`known-patterns.md`, `per-language/{lang}.md`) are committed to that review's own `intl/pending-{base}` branch (the `/review-translations` worktree) so they ride with the PR. If you catch yourself editing `.claude/translation-review/` in the `dev` checkout, move the changes to the review branch and restore `dev`. Sequence: commit → push → submit review (`--approve` once the fix is pushed and no criticals remain, else `--comment`).\n\n## Highest-Value Gotchas\n\nThese are landmines where the obvious-looking call is wrong. The full set is in `references/gotchas.md`.\n\n### Frontmatter `tags` arrays mix two policies\n\nTutorial frontmatter `tags:` mix brand-name tags (stay English) with concept/category tags (intentionally translated — do NOT revert). Rule of thumb: proper noun / product name → English; generic descriptive term → translated form is right. Details and examples: `references/known-patterns.md`.\n\n### Semantic inversions on consensus terms\n\nHigh-frequency real-world failure mode: Gemini occasionally swaps proof-of-stake and proof-of-work, or mistranslates \"mainnet\" as \"market\" or \"main network.\" These are critical semantic errors, not warnings. Spot-check consensus, mainnet, testnet, validator, miner, client when reviewing.\n\n### Cross-script contamination\n\nOld Crowdin imports sometimes have characters from one script leaking into another (Devanagari in Turkish, CJK in Arabic). Less common with the new pipeline but still possible. Flag any unexpected-script characters; they indicate translation memory leaks.\n\n### Lorem ipsum / placeholder text in JSON\n\nReal translation values sometimes contain \"Lorem ipsum dolor sit amet\" or similar placeholder text. Treat as a critical user-visible issue.\n\n### Translated GitHub `@username` handles\n\n`@axic` becoming `@kwaaxic` (Swahili) or similar — broken attribution. Flag.\n\n### Asymmetric backticks and orphaned HTML tags\n\nSingle-open / double-close backticks (`` `text`` ``), missing `</em>`before`</li>`, raw `</a>` without opener — all break MDX compilation. Critical.\n\n## Quick \"Where Do I Look?\" Cheatsheet\n\n| I need...                              | Path                                                                                                               |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |\n| Slash command (full pipeline)          | `/review-translations` (`.claude/commands/review-translations.md`)                                                 |\n| Issue-pattern catalog                  | `.claude/translation-review/known-patterns.md` + `references/known-patterns.md`                                    |\n| Per-language rules                     | `references/language-rules.md` (sourced from `.claude/translation-review/localization-rules-by-language-group.md`) |\n| Scoring rubric                         | `references/scoring-rubric.md`                                                                                     |\n| Per-language findings                  | `.claude/translation-review/per-language/{lang}.md`                                                                |\n| ETHGlossary terms (filtered to source) | `POST /api/v1/filter`                                                                                              |\n| ETHGlossary terms (full per-language)  | `GET /api/v1/translations/{lang}`                                                                                  |\n| ETHGlossary policy                     | https://github.com/ethereum/ethglossary/blob/main/docs/translation-policy.md                                       |\n| Sanitizer source                       | `src/scripts/intl-pipeline/intl-sanitizer.ts`                                                                      |\n| Sanitizer test research                | `docs/solutions/integration-issues/sanitizer-test-research.md`                                                     |\n| Past review post-mortems               | `docs/solutions/integration-issues/` + `docs/solutions/logic-errors/`                                              |\n| Language config                        | `i18n.config.json`                                                                                                 |\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies.\n\n- **`references/known-patterns.md`** — Load when starting any review. The catalog of recurring issue patterns (brand mistranslations, MDX syntax errors, semantic inversions, cross-script contamination, etc.).\n- **`references/language-rules.md`** — Load when working on a specific non-Latin-script language (ar, bn, hi, ja, ko, mr, ru, ta, te, uk, ur, zh, zh-tw). Per-language-group rules for prose, UI tags, numerals, code, people's names.\n- **`references/scoring-rubric.md`** — Load when scoring a PR or producing a quality report. The 5-category rubric (Brand Name Preservation, Technical Accuracy, Semantic Fidelity, Terminology Consistency, Tone/Register) with weighting.\n- **`references/ethglossary-usage.md`** — Load when integrating ETHGlossary lookups into a review (which endpoint, when to use `/filter` vs `/translations/{lang}`, how to handle missing terms, what `script_rule` and `term_role` mean for reviewers).\n- **`references/per-language-tracking.md`** — Load when writing to or reading from `.claude/translation-review/per-language/{lang}.md`. The convention for accumulating findings across reviews.\n- **`references/gotchas.md`** — Load when something feels off and you can't find it inline. The long tail of confusion patterns.\n- **`references/critical-vs-warning.md`** — Load when deciding whether to flag an issue as critical (auto-fix) or warning (manual review). The severity rubric.\n- **`references/agent-roles.md`** — Load when planning a multi-agent review (structural / terminology / semantic). The split and what each role focuses on.\n\n## Other Project Skills That May Apply\n\n- **`intl-pipeline`** — For the pipeline side: how translations are produced, manifest invariants, sanitizer behavior, the `intl/pending-{base}` model. When a review surfaces a pipeline-level bug (not a translation-level one), reach for this skill.\n- **`design-system`** — When a translation issue is rendering-related (BiDi flip, RTL spacing, layout overflow from longer non-English strings).\n\n## Pre-Merge Smoke Test\n\nBefore submitting a translation-quality review:\n\n- [ ] ETHGlossary `/filter` endpoint was called for at least one file per language (don't review from memory)\n- [ ] All critical issues listed (glossary deviations, MDX syntax errors, brand mistranslations, translated hrefs, semantic inversions)\n- [ ] Frontmatter `tags` distinction respected (brand tags English, concept tags translated)\n- [ ] Code-block policy applied (functional code English, comments may be translated)\n- [ ] Per-language quality score produced in the 5-category rubric\n- [ ] Per-language findings file updated at `.claude/translation-review/per-language/{lang}.md`\n- [ ] If new issue pattern surfaced, added to `references/known-patterns.md` (or `known-patterns.md` in the KB)\n- [ ] All review changes (fixes + knowledge-base updates) committed to the `intl/pending-*` branch under review and pushed — never left on `dev`\n- [ ] Build verification (`--build-local` or `--netlify-check`) run if the review touched MDX-syntax-related issues\n","frontmatter":{"name":"intl-review","description":"Use when reviewing translation imports/PRs (especially against `intl/pending-*` branches), evaluating translation quality across any of the 24 target languages, triaging issues in `.claude/translation-review/`, scoring a locale's translation, running the `/review-translations` slash command, debugging brand-name mistranslations or glossary deviations, or working with the per-language findings tracking files. Covers the scoring rubric, ETHGlossary-as-authority policy, language-group rules, agent-role split (structural / terminology / semantic), the concept-tag-vs-brand-tag distinction in frontmatter, and the critical-vs-warning severity policy."},"isInternal":false,"tokens":2444,"sizeBytes":11554},{"name":"SKILL.md","path":".claude/skills/new-page/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/new-page/SKILL.md","title":"new-page","category":"anthropic-skill","format":"markdown","content":"---\nname: new-page\ndescription: Interview-then-scaffold guide for adding a new page to ethereum.org. Forks first on markdown-backed vs App Router page, then picks the layout and hero, maps the Figma design to existing components, and builds it reuse-first. Invoke with /new-page.\ndisable-model-invocation: true\n---\n\n# New Page\n\nA new ethereum.org page is **reuse-first**: every region maps to a layout, hero, primitive, or variant that already exists — you *configure* what's there, you don't build a parallel version of it. This skill interviews you for the missing context, then scaffolds the page against the design system.\n\nWork the steps in order. Each ends on a checkable condition — don't start the next until the current one is met.\n\n## Step 0 — Load the design system\n\nInvoke the `design-system` skill and read its `SKILL.md` fully before asking anything else. It is the source of truth for component choices, tokens, layouts, RTL/i18n, and the server/client boundary. **Do not restate its catalog here** — when you need the layout inventory read `design-system/references/layouts.md`, and verify it against `src/layouts/index.ts` (layouts get refactored).\n\n**Done when:** `design-system` SKILL.md has been read this session.\n\n## Step 1 — Interview\n\nThe primary fork is **markdown-backed or not**. It decides the layout system, the build, and how the page is translated — so settle it first. It is an *authoring* decision (how the content is written and maintained), **not** a visual one: a hero-topped prose page can be either, because `ContentLayout` gives markdown `TopicLayout` and App Router pages the same hero + card-TOC chrome. So **ask it; never infer it from the design.**\n\nUse `AskUserQuestion`:\n\n1. **Markdown page or App Router page?**\n   - *Markdown page* — content authored in `public/content/<route>/index.md`, rendered through `[...slug]`, translated by the intl-pipeline, built from a fixed set of MDX shortcodes. Choose for editorial/educational content a writer maintains in markdown.\n   - *App Router page* — a hand-built `app/[locale]/<route>/page.tsx` that composes `ContentLayout`. Choose when the page is interactive, data-backed, or needs layout/components the markdown shortcodes can't express. Translated via `getTranslations` JSON keys.\n2. **Route** — the URL path (e.g. `/stablecoins/`). The folder name *is* the route.\n3. **Design source** — a Figma link/selection, a written description, or none.\n4. **Content** (markdown only) — does the user have the body ready, or should you draft it?\n\nThen **check the route is free**: no existing `public/content/<route>/index.md` or `app/[locale]/<route>/page.tsx`. If one exists you're *editing*, not creating — surface it and confirm before any write; never silently overwrite a page you didn't author.\n\n**Done when:** markdown-vs-App-Router, route, and design source are known, and the route is confirmed free (or editing is okayed).\n\n## Step 2 — Pull the design (only if Figma was provided)\n\nRead the design **fresh** with the Figma MCP (designs change between runs — never rely on a read from an earlier turn): `get_metadata` for the frame tree, then `get_design_context` / `get_screenshot` on each region; `search_design_system` / `get_code_connect_map` for existing code mappings. Load the `figma-use` skill before any `use_figma` call.\n\nProduce a **component map** and **always show it to the user as a table** — three columns: **Region** (each design region, top to bottom) · **Maps to** (the existing primitive + variant that renders it — `PageHero`, `Card`, `BigNumber`, …) · **Status** (`reuse`, `missing asset`, `placeholder copy`, `missing data`, …). Walk the frame top to bottom and map **every** region. The table is the user's only window into what you saw in the frame — render it every run; never just reason about the mapping internally.\n\n**Node names lie.** A node called `Screenshot 2026-…` or `Frame 1789` is routinely real content — a stat band (`BigNumber`), a video embed (`YouTube`), a callout (`Alert`) — not a throwaway. Never omit a region because its name *looks* like a draft; `get_screenshot` it and see what it actually is before deciding.\n\n**Missing data → ask, don't guess.** When a region needs a value the design doesn't carry — a YouTube ID behind a thumbnail, a real destination URL, a live data source — you can't extract it from the frame. Flag it and ask the user; a guessed embed or invented link is worse than an open question.\n\n**Match, don't mimic.** Reproduce the design with existing components and their variants. Do **not** layer custom Tailwind/CSS on a primitive to chase pixel parity. If a region has no fitting primitive or variant, the answer is *add a variant* (`design-system/references/variant-vs-new.md`) — never a one-off component or bespoke styles. Flag any such gap and confirm before inventing.\n\n**Pressure-test the authoring fork.** The map can reveal a region markdown shortcodes can't express — an interactive widget, a data-backed list, a bespoke multi-column grid, or cards with **lucide** icons (`MarkdownCard` accepts `emoji` *or* `icon: ReactNode`, but a plain `.md` file can't author a ReactNode, so from markdown the `<Card>` shortcode is effectively emoji-only). Any of these means the page wants to be App Router. If Step 1 said markdown, surface the conflict and re-confirm before scaffolding — flipping it later is a rewrite.\n\n**Done when:** the component-map table has been shown to the user and they've confirmed or corrected it; every region is accounted for — mapped, flagged for an approved variant, or raised as missing data — and the authoring fork still holds.\n\n## Step 3 — Pick the layout and hero\n\nBranches on Step 1.\n\n### Markdown page — pick the `template:`\n\nThe layout is selected by the `template:` frontmatter value (default `static`). Full inventory in `design-system/references/layouts.md`; live set in `src/layouts/index.ts`.\n\n| `template:` | Layout | Use for | Hero it renders |\n|---|---|---|---|\n| `static` (default) | `StaticLayout` | one-off editorial prose, no sub-nav | title + breadcrumbs only (`PageHero variant=\"no-divider\"`, **no image**) |\n| `use-cases` / `staking` / `roadmap` / `upgrade` | `TopicLayout` | topic hub: full side-image hero + sibling sub-nav | image `PageHero` from frontmatter `image` / `summary` / `buttons` |\n| `docs` | `DocsLayout` | developer docs with the docs sidebar | — |\n| `tutorial` | `TutorialLayout` | developer tutorial with author/date/skill metadata | — |\n\n- Each Topic template carries a distinct **MDX component list** (`componentsMapping`, `src/layouts/index.ts`) — pick the one whose shortcodes match the content.\n- **Side-image hero in the design → a Topic template** (`static` renders only a title hero). `showDropdown`, new-topic wiring, and the never-add-a-new-layout rule: `design-system/references/layouts.md`.\n\n### App Router page — compose `ContentLayout`, pass the hero that fits\n\nHand-build `app/[locale]/<route>/page.tsx` composing `ContentLayout`, and choose the hero from the design:\n\n- `PageHero` — breadcrumbs + title + optional `heroImg` + description + buttons. The workhorse; covers both image and title-only heroes.\n- `HubHero` — large hub hero (exemplar: `/learn/`).\n\nBuild the `tocItems` array by hand and wrap each prose section in `<Section id>`. `ContentLayout` owns the card TOC, contributors block, and feedback. Exemplars: `/learn/`, `/what-is-ethereum/`.\n\n**Done when:** markdown → `template:` (and `showDropdown` if relevant) chosen; App Router → hero component chosen. Confirmed with the user.\n\n## Step 4 — Scaffold\n\nBuild from the Step 2 component map. Reuse-first throughout — no raw `<a>`/`<button>`, no hex colors, logical CSS props (`ms-`/`me-`/…), locale-aware `numberFormat()`/`dateTimeFormat()`. Icons: **lucide in `.tsx`, emoji in markdown** (markdown can't pass the `icon` ReactNode prop).\n\n**Markdown page:**\n- Create `public/content/<route>/index.md` with frontmatter (`title`, `description`, `lang: en`, plus `template:` unless static). English only — never hand-write translated copies; the intl-pipeline propagates them.\n- **Inline images co-locate** next to `index.md`, referenced relatively — a `/images/...` inline path 500s the MDX render; the frontmatter `image:` hero is the exception. Details: `design-system/references/gotchas.md`.\n- Every h1–h4 needs a `{#kebab-id}`; run `pnpm lint:md:fix`.\n- If using `TopicLayout`: add `src/data/topics/<key>.ts`, wire `layoutMapping`/`componentsMapping` in `src/layouts/index.ts`, and add `src/intl/en/page-<key>.json`.\n\n**App Router page:**\n- Create `app/[locale]/<route>/page.tsx` composing `ContentLayout` (mirror `/learn/` or `/what-is-ethereum/`): pass `heroSection`, the hand-built `tocItems`, and `contributors`; wrap prose in `<Section id>`. Server Component unless it needs state/effects/handlers.\n- All user-facing strings via `getTranslations`; add keys to `src/intl/en/`.\n\n**Done when:** the page renders through its layout with content/strings in place and no inlined reinventions of existing primitives.\n\n## Step 5 — Verify (static checks)\n\n- `pnpm type-check` (catches invalid chain names and type errors).\n- Walk the design-system **Pre-Merge Smoke Test** checklist.\n- Add a `.stories.tsx` for any genuinely new UI primitive.\n\n**Done when:** type-check passes and the smoke checklist is clean.\n\n## Step 6 — QA via the adversarial review loop\n\nStatic checks pass on pages that are visibly broken — a wrong image path, a failed MDX compile, a section that silently didn't render. So the page must be run and audited via the **adversarial reviewer loop**, not an eyeball comparison.\n\n1. **Run it.** Start `pnpm dev`, wait for \"Ready\", load the page (locale-stripped for `en`: `/<route>/`). Confirm **HTTP 200** — a 500 is almost always an MDX or asset error; read the dev log (a non-co-located inline image is the classic one). There is nothing to review on a broken page.\n2. **Run the review loop.** Read `review-page/SKILL.md` and execute its Steps 1–5 against this route until green. You already hold the design source and route from Steps 1–2, so its Step 0 (running page + known design source) is already satisfied. Read and run that procedure; don't try to invoke `/review-page` as a skill from here.\n   - **A hero finding is a template bug, not a CSS gap.** When the design-system reviewer flags a title-only hero against a side-image design: on a markdown page that's a wrong-`template:` (use a Topic template, not `static`); on an App Router page, pass `heroImg` to `PageHero`. Never rebuild a hero with custom markup.\n3. **Hand off.** Leave the dev server running and give the user the local URL the server printed (e.g. `http://localhost:3000/<route>/`).\n\n**Done when:** the review loop is green (or hits its round cap with the remaining findings handed off), and the user has the live URL.\n","frontmatter":{"name":"new-page","description":"Interview-then-scaffold guide for adding a new page to ethereum.org. Forks first on markdown-backed vs App Router page, then picks the layout and hero, maps the Figma design to existing components, and builds it reuse-first. Invoke with /new-page.","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":2667,"sizeBytes":10939},{"name":"SKILL.md","path":".claude/skills/page-visual-tests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/page-visual-tests/SKILL.md","title":"page-visual-tests","category":"anthropic-skill","format":"markdown","content":"---\nname: page-visual-tests\ndescription: Playwright + Chromatic full-page visual tests for ethereum.org. Trigger on \"add a page to the visual suite\", \"the snapshot keeps changing\", \"chromatic pages\", \"chromatic playwright\", or edits to `tests/visual/`, `playwright.visual.config.ts`, or the `page-visual-tests` job in `.github/workflows/ci.yml`. Skip for Storybook Chromatic (`.github/workflows/chromatic.yml`), e2e (`tests/e2e/`), unit (`tests/unit/`).\n---\n\n# Page Visual Tests (Playwright + Chromatic)\n\nThis skill covers the **page visual tests** Chromatic project only (the `page-visual-tests` job in ci.yml publishes via `chromaui/action`; local publish is `pnpm chromatic:pages` with `CHROMATIC_PAGES_TOKEN`) — not Storybook Chromatic.\n\nThe Playwright suite captures DOM archives (not PNGs) per page × viewport; Chromatic re-renders them in the cloud to diff. A green local `pnpm test:visual` just means archives were produced — the diff happens after upload.\n\n## Files that matter\n\n- `playwright.visual.config.ts` — visual-only config (3 viewports + `webServer`)\n- `playwright.config.ts` — base (e2e + unit; **no `webServer`**)\n- `tests/visual/pages.spec.ts` — page list + readiness pattern\n- `.github/workflows/ci.yml` — CI (the `page-visual-tests` job)\n- `src/components/ui/skeleton.tsx`, `src/components/ui/spinner.tsx` — loading primitives\n- `package.json` scripts: `test:visual*`, `chromatic:pages`\n\n## Non-obvious constraints\n\n**Dual Playwright config.** `webServer` lives only in `playwright.visual.config.ts`. Moving it into the base config breaks `pnpm test:unit` and `pnpm test:e2e` in CI — they try to start Next against a missing `.next` build.\n\n**Desktop viewport is 1024, not 1280.** Chromatic caps snapshots at `width × height ≤ 25M` px. The tallest tested pages reach ~22.5k px; 1280 overflows, 1024 fits. Measure `document.documentElement.scrollHeight` before raising the viewport or adding a long page.\n\n**Loading contract: `data-slot=\"loading\"`.** The shared `Skeleton` and `Spinner` primitives carry this attribute. Each test waits until `document.querySelectorAll('[data-slot=\"loading\"]').length === 0` before snapshotting. Any bespoke loader — raw `animate-pulse-light`, a local Skeleton copy, a custom spinner — is invisible to the wait and will silently flake. Fix by routing through the shared primitive or adding `data-slot=\"loading\"` to the bespoke loader's root.\n\n**Imports come from `@chromatic-com/playwright`, not `@playwright/test`.** The two packages re-export `expect` with skewed types, so `expect(...).toHaveCount(0)` misbehaves — prefer `page.waitForFunction` for the loading wait.\n\n**Environment.** `USE_MOCK_DATA=true` and `NEXT_PUBLIC_BUILD_LOCALES` are required at build and test time. The local `test:visual:build` script sets `NEXT_PUBLIC_BUILD_LOCALES=en`; CI's shared build in ci.yml uses `NEXT_PUBLIC_BUILD_LOCALES: \"en,es,zh,ar\"` because one build artifact serves the e2e, lighthouse, and visual jobs. Paths in the spec are unprefixed (`/wallets/`, not `/en/wallets/`) because `localePrefix: \"as-needed\"` serves English at the root — adding `/en` would just trigger a redirect.\n\n**Random ordering: `safeShuffle`.** Lodash `shuffle` and `.sort(() => Math.random() - 0.5)` flake snapshots independently of loaders. Wrap them with `safeShuffle` from `src/lib/utils/random.ts` — it returns the list unchanged when `IS_VISUAL_TEST=true`. Current call sites: `src/lib/utils/wallets.ts`, `src/lib/utils/apps.ts` (Highlights/Discover/AppOfTheWeek), `src/components/Staking/StakingProductsCardGrid/index.tsx`. The env var is exposed to the client bundle via `next.config.js`'s `env` block; without that, `process.env.IS_VISUAL_TEST` evaluates to `undefined` in client components and the shuffle still runs.\n\n**Use `domcontentloaded`, not `networkidle`.** Analytics and background fetches keep the network perpetually busy.\n\n## Canonical test\n\n```ts\nimport { takeSnapshot, test } from \"@chromatic-com/playwright\"\n\nconst pages: Array<{ name: string; path: string }> = [\n  { name: \"Homepage\", path: \"/\" },\n  { name: \"Docs - Smart Contracts\", path: \"/developers/docs/smart-contracts/\" },\n  // ...\n]\n\ntest.describe(\"Page Visual Tests\", () => {\n  for (const { name, path } of pages) {\n    test(name, async ({ page }, testInfo) => {\n      await page.goto(path, { waitUntil: \"domcontentloaded\" })\n      await page.waitForFunction(\n        () => document.querySelectorAll('[data-slot=\"loading\"]').length === 0\n      )\n      // FeedbackWidget is dynamic({ ssr: false }); waiting for its button proves\n      // hydration finished and dynamic chunks landed — same pattern other\n      // ssr:false components (Emoji/Twemoji) rely on to render.\n      await page.waitForSelector('[data-testid=\"feedback-widget-button\"]')\n      await takeSnapshot(page, testInfo)\n    })\n  }\n})\n```\n\n## Common situations\n\n**Adding a page.** Each entry costs three snapshots (one per viewport) against Chromatic's budget, so check whether the page's layout (under `src/layouts/`) is already covered before adding. Scan the page subtree for bespoke loaders — they're the single biggest flake cause — and confirm full-page height stays under the 25M-pixel budget. Local loop: `pnpm test:visual:build` once, then `pnpm test:visual:desktop` for iteration, `pnpm test:visual` for the full sweep.\n\n**Flaky snapshot.** Two main causes. (1) A loader without `data-slot=\"loading\"` — run with `--trace=on` and inspect the `waitForFunction` step; ~0 ms duration means it isn't being waited on. (2) Random ordering — grep the page subtree for `shuffle(`, `Math.random()`, or `.sort(() =>` and route through `safeShuffle`. If dynamic content is drifting, double-check `USE_MOCK_DATA=true` is set in both build and test steps.\n\n**Local `pnpm dev` masks a regression.** `playwright.visual.config.ts` sets `reuseExistingServer: true`, which is correct for CI but means a `pnpm dev` already running on :3000 will be used silently in place of the production build the suite assumes. If a snapshot diff doesn't reproduce in CI, kill the dev server and run `pnpm test:visual:build` to rebuild against the production output before retrying.\n\n**Pixel-limit error.** Measure the page's full-page height at 1024 px; if it exceeds ~24,400 px, the page needs shortening or removal from the suite. Don't crop to viewport — below-the-fold coverage is the point of this suite.\n\n**Works locally, fails in CI.** Usually `HOME: /root` missing from the test step — GitHub Actions overrides `HOME` inside containers, and Playwright can no longer find the browsers baked into the `mcr.microsoft.com/playwright` image. Also check that the image tag matches `@playwright/test` in `package.json`.\n","frontmatter":{"name":"page-visual-tests","description":"Playwright + Chromatic full-page visual tests for ethereum.org. Trigger on \"add a page to the visual suite\", \"the snapshot keeps changing\", \"chromatic pages\", \"chromatic playwright\", or edits to `tests/visual/`, `playwright.visual.config.ts`, or the `page-visual-tests` job in `.github/workflows/ci.yml`. Skip for Storybook Chromatic (`.github/workflows/chromatic.yml`), e2e (`tests/e2e/`), unit (`tests/unit/`)."},"isInternal":false,"tokens":1657,"sizeBytes":6712},{"name":"build.md","path":".claude/skills/performance/references/build.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/build.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/performance/references/build.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/build.md)","tokens":300,"sizeBytes":221},{"name":"bundle.md","path":".claude/skills/performance/references/bundle.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/bundle.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/performance/references/bundle.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/bundle.md)","tokens":300,"sizeBytes":223},{"name":"diagnose-table.md","path":".claude/skills/performance/references/diagnose-table.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/diagnose-table.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/performance/references/diagnose-table.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/diagnose-table.md)","tokens":300,"sizeBytes":239},{"name":"edge-caching.md","path":".claude/skills/performance/references/edge-caching.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/edge-caching.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/performance/references/edge-caching.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/edge-caching.md)","tokens":300,"sizeBytes":235},{"name":"images.md","path":".claude/skills/performance/references/images.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/images.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/performance/references/images.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/images.md)","tokens":300,"sizeBytes":223},{"name":"inp.md","path":".claude/skills/performance/references/inp.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/inp.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/performance/references/inp.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/inp.md)","tokens":300,"sizeBytes":217},{"name":"review-checklist.md","path":".claude/skills/performance/references/review-checklist.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/review-checklist.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# References Skill\n\nPath: `.claude/skills/performance/references/review-checklist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/references/review-checklist.md)","tokens":300,"sizeBytes":243},{"name":"SKILL.md","path":".claude/skills/performance/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/performance/SKILL.md","title":"performance","category":"anthropic-skill","format":"markdown","content":"---\nname: performance\ndescription: Performance patterns and anti-patterns for ethereum.org covering TTFB, LCP, INP, CLS, bundle size, build memory, and RSC payload. Use when diagnosing a perf regression or reviewing proposed code changes for perf landmines.\nuser-invocable: false\n---\n\n# Performance Knowledge\n\nKnowledge base. No runtime actions; authoring conventions for maintainers below. Read the relevant `references/*.md` files directly via the Read tool.\n\n## Two entry points\n\n- **Diagnosing** an existing regression → `references/diagnose-table.md` (symptom → bucket).\n- **Reviewing** a proposed change before merge → `references/review-checklist.md` (diff pattern → rules to load).\n\n## Cross-cutting anti-patterns\n\nThese look right but aren't. Check every review or diagnose pass against this table.\n\n| Looks like…                                                  | Actually…                                                                                                     |\n| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |\n| Adding `CDN-Cache-Control` to fix TTFB                       | Netlify strips them; CF ignores due to `max-age=0`.                                                           |\n| Using `unstable_cache` in MDX routes                         | Triggers ISR → runtime 404s. See `edge-caching.md`.                                                           |\n| ``await import(`.../${locale}/${ns}.json`)``                 | Webpack enumerates all combinations → OOM. Use `fs.readFile`.                                                 |\n| Adding a `useSession` / `cookies()` in root layout           | Every route becomes dynamic. Edge caching dies silently.                                                      |\n| `priority: true` via `getImageProps()` on manual `<picture>` | `fetchPriority` attribute is not set; add it manually.                                                        |\n| Bumping `revalidate` to hourly to \"fix staleness\"            | Increases edge miss rate. Keep daily; add client `/api/*` for freshness.                                      |\n| Mocking DB/content in perf tests                             | Perf wins must be measured against real prod build (`pnpm build && start`).                                   |\n| Removing `common.json` keys without audit                    | Lookups of removed keys silently render the bare key (fallback). Grep the key across `src/` and `app/` first. |\n\n## File map\n\n| Topic                           | File                             |\n| ------------------------------- | -------------------------------- |\n| Symptom → bucket routing        | `references/diagnose-table.md`   |\n| Diff pattern → rules routing    | `references/review-checklist.md` |\n| TTFB, edge caching, CDN         | `references/edge-caching.md`     |\n| Bundle size, code splitting, RSC payload | `references/bundle.md`  |\n| Build OOM, ENOSPC, file tracing | `references/build.md`            |\n| INP, main-thread work           | `references/inp.md`              |\n| LCP, images                     | `references/images.md`           |\n\nFor data-fetching patterns (Trigger.dev tasks, Netlify Blobs, `src/lib/data` caching, internal `/api/*` routes, fetcher retries), use the `data-layer` skill instead.\n\n## Conventions for adding rules\n\n- One topic per file; no content duplication across files.\n- Cite a PR (`PR #N`) when a rule comes from a specific shipped change. Add a SHA only when no PR exists.\n- Forward-looking (\"do this, avoid that\"). Keep entries prescriptive — no postmortems or incident narratives.\n- New review check? Add a row to `review-checklist.md` AND the matching bucket file. Don't restate the rule inline.\n","frontmatter":{"name":"performance","description":"Performance patterns and anti-patterns for ethereum.org covering TTFB, LCP, INP, CLS, bundle size, build memory, and RSC payload. Use when diagnosing a perf regression or reviewing proposed code changes for perf landmines.","user-invocable":false},"isInternal":false,"tokens":747,"sizeBytes":3846},{"name":"copy.md","path":".claude/skills/review-page/reviewers/copy.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/reviewers/copy.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Reviewers Skill\n\nPath: `.claude/skills/review-page/reviewers/copy.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/reviewers/copy.md)","tokens":300,"sizeBytes":216},{"name":"design-system.md","path":".claude/skills/review-page/reviewers/design-system.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/reviewers/design-system.md","title":"Reviewers Skill","category":"anthropic-skill","format":"markdown","content":"# Reviewers Skill\n\nPath: `.claude/skills/review-page/reviewers/design-system.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/reviewers/design-system.md)","tokens":300,"sizeBytes":234},{"name":"SKILL.md","path":".claude/skills/review-page/SKILL.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/review-page/SKILL.md","title":"review-page","category":"anthropic-skill","format":"markdown","content":"---\nname: review-page\ndescription: Adversarial review loop for a built ethereum.org page — spawns copy and design-system reviewer subagents against the Figma source, triages their findings, fixes, and re-reviews until a fresh round comes back clean. Invoke with /review-page.\ndisable-model-invocation: true\n---\n\n# Review Page\n\nAudit a built page by turning two **adversarial** reviewers loose on it, fixing what they find, and re-running them until a fresh round comes back **green**. Each reviewer is a subagent prompted to assume the page is *wrong* and hunt for proof — never to rubber-stamp it. You (the orchestrator) own the Figma pull, the triage, and the fixes; the reviewers only report. This is the QA half of building a page — pair it with `/new-page`, or run it standalone against any existing page.\n\nWork the steps in order. Each ends on a checkable condition — don't start the next until the current one is met.\n\n## Step 0 — Ground truth\n\nYou need two things before any review:\n\n1. **The live page.** Start `pnpm dev`, load the route (locale-stripped for `en`: `/<route>/`), confirm **HTTP 200**. A 500 is a build/asset error — fix that first; there is nothing to audit on a broken page.\n2. **The design source.** A Figma link/selection is the gold standard. A written spec serves the copy reviewer but not the pixel-level design checks. **No source at all → the copy reviewer has nothing to sync against; say so and confirm the user still wants a design-system-only pass.**\n\n**Done when:** the page serves 200 and the design source is known (or its absence is confirmed).\n\n## Step 1 — Capture both sides\n\nThe reviewers compare *the page as it renders* against *the design as it's specified*. Capture both once, to a scratch dir, and pass the paths to the subagents — don't make each reviewer re-pull Figma (reads drift between pulls, and it burns rate limits).\n\n- **Design.** Read the Figma **fresh** — never a read from an earlier turn: `get_metadata` for the frame tree, `get_screenshot` per region, `get_design_context` for the text, `get_variable_defs` for tokens. Save the region screenshots; keep the text/token dump.\n- **Live page.** Full-page screenshot with `playwright-cli`, plus per-section shots for a long page. Save them beside the design shots.\n\n**Done when:** design and live-page artifacts are saved and their paths are known.\n\n## Step 2 — Spawn the adversarial reviewers\n\nRead both briefs in `reviewers/` and spawn **one subagent per brief, in parallel** — a single message with two `Agent` calls. Give each subagent its brief verbatim, the route, and the artifact paths from Step 1. Each returns a structured findings list — `{location, claim, evidence, fix, severity}` — and nothing else. **Reviewers report; they never edit** (two agents editing in parallel would collide).\n\n- `reviewers/copy.md` — every string on the page matches the Figma/source copy.\n- `reviewers/design-system.md` — components, spacing, heading sizes, and colors are the design system's, with no custom CSS bolted onto primitives.\n\n**Done when:** both subagents have returned their findings lists.\n\n## Step 3 — Triage\n\nAdversarial reviewers over-report by design. **You** judge each finding before touching code — keep the real ones, drop the noise. Two calls need judgment:\n\n- **Intentional copy divergence.** A string differs because the *design is stale*, or because it's a templated/localized value — not a copy bug. Confirm against the source, not the reviewer's assumption.\n- **Justified custom CSS.** Custom CSS *chasing pixel parity* on a primitive is a finding; custom CSS the design *genuinely requires* (a one-off the system can't express) is acceptable per \"match, don't mimic.\" Don't strip styling the design needs.\n\nShow the user the **confirmed** findings as a table — `Region · Finding · Fix · Severity` — and flag anything ambiguous for their call. This is their window into what the reviewers saw.\n\n**Done when:** every finding is confirmed or dropped, and the confirmed table has been shown to the user.\n\n## Step 4 — Fix\n\nApply the confirmed findings yourself, reuse-first — a variant, not custom CSS; the right primitive, not a re-invention. Load the `design-system` skill if you haven't, for the component and token calls.\n\n**Done when:** every confirmed finding is fixed in the working tree.\n\n## Step 5 — Re-review until green\n\nRe-capture the **live page** (it changed; the Figma side didn't — reuse Step 1's design artifacts) and re-spawn the same reviewers. The loop is **green** when a fresh adversarial round returns **zero** confirmed findings. Until then: triage → fix → re-review.\n\nCap it: if the loop isn't green after **3 rounds**, stop and hand the user the stubborn findings — a finding that survives three fixes usually needs a human design call, not another pass.\n\n**Done when:** a fresh round is green, or the 3-round cap is hit and the remaining findings are handed off.\n","frontmatter":{"name":"review-page","description":"Adversarial review loop for a built ethereum.org page — spawns copy and design-system reviewer subagents against the Figma source, triages their findings, fixes, and re-reviews until a fresh round comes back clean. Invoke with /review-page.","disable-model-invocation":true,"disableModelInvocation":true},"isInternal":false,"tokens":1148,"sizeBytes":4974},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md - Ethereum.org Website\n\n## Project Overview\n\nThis is the official Ethereum.org website - a Next.js application that serves as the primary educational and community hub for Ethereum. The site is built with modern web technologies and focuses on accessibility, internationalization, and performance.\n\n## Technology Stack\n\n### Core Framework\n\n- **Next.js 16+** - React framework with App Router (Turbopack is the default bundler; webpack available via `pnpm dev:webpack` / `pnpm build:webpack`)\n- **React 19** - UI library\n- **TypeScript 5.5+** - Type safety and development experience\n- **Tailwind CSS 4+** - Utility-first CSS framework (CSS-first config in `src/styles/global.css`)\n\n### Key Dependencies\n\n- **next-intl 4+** - Internationalization (i18n) with 25 languages\n- **next-mdx-remote 5.0+** - MDX content processing\n- **Motion 12+** (`motion` package, formerly Framer Motion) - Animations and transitions; import from `motion/react`\n- **Radix UI** - Accessible component primitives\n- **shadcn/ui** - Component library built on Radix UI\n- **Recharts** - Data visualization\n- **Viem/Wagmi** - Ethereum blockchain integration\n\n### Development & Testing\n\n- **Storybook 10+** - Component development and testing\n- **Chromatic** - Visual regression testing\n- **ESLint** - Code linting with custom rules\n- **Prettier** - Code formatting\n- **Husky** - Git hooks\n- **PNPM** - Package manager\n\n## Project Structure\n\n- **app/** - Next.js App Router pages\n  - **[locale]/** - Internationalized routes\n- **src/**\n  - **components/** - React components\n    - **ui/** - Design system components\n    - **icons/** - SVG icon components\n  - **data/** - Static data and configurations\n  - **hooks/** - Custom React hooks\n  - **i18n/** - Internationalization config\n  - **intl/** - Translation files (25 languages)\n  - **layouts/** - Page layout components\n  - **lib/** - Utility functions and types\n    - **constants.ts** - App constants\n    - **types.ts** - TypeScript type definitions\n    - **utils/** - Utility functions\n  - **styles/** - Global styles and design tokens\n- **public/** - Static assets\n  - **content/** - Markdown content files\n  - **images/** - Image assets\n- **docs/** - Development documentation\n  - **solutions/** - Documented solutions to past problems, organized by category with YAML frontmatter (module, tags, problem_type)\n\n## Code Conventions\n\n### File Naming\n\n- **Components**: kebab-case (e.g., `button-group.tsx`)\n- **Utilities**: camelCase (e.g., `cn.ts`, `relativePath.ts`)\n- **Pages**: kebab-case following Next.js conventions\n- **Assets**: kebab-case (e.g., `eth-logo.png`)\n\n### TypeScript Patterns\n\n- Use `interface` for object shapes, `type` for unions/intersections\n- Prefer explicit typing over `any` (ESLint enforces `fixToUnknown`)\n- **NEVER leave unused variables or parameters** - ESLint `unused-imports/no-unused-vars` will fail the Netlify build. The only allowed unused arg pattern is a single underscore `_`. Do NOT use `_prefixedNames` (e.g., `_foo`) - either use the variable or remove it from the signature entirely.\n- Use generic constraints for reusable components\n- Export types from dedicated files in `@/lib/types`\n\n### Styling Conventions\n\n- **Primary approach**: Tailwind CSS utility classes\n- **Component variants**: Use `tailwind-variants` (`tv`) for new and refactored work. Existing `class-variance-authority` (`cva`) components don't need bulk migration -- swap to `tv` opportunistically when you're already touching the component for another reason.\n- **Dynamic classes**: Use `cn()` utility (clsx + tailwind-merge)\n- **Custom properties**: CSS variables in `src/styles/` for theme values\n- **Responsive design**: Mobile-first approach\n\nFor UI work, see the **`design-system` skill** at `.claude/skills/design-system/`. It's the canonical knowledge base for component choices, design tokens, RTL/i18n, server/client boundaries, and the \"use a variant, not a new component\" pattern.\n\n## Development Workflows\n\n### Available Scripts\n\n```bash\n# Development\npnpm dev                    # Start development server (Turbopack)\npnpm dev:webpack            # Start development server with webpack\npnpm build                  # Build for production (Turbopack)\npnpm build:webpack          # Build for production with webpack\npnpm start                  # Start production server\n\n# Code Quality\npnpm lint                   # Run ESLint\npnpm lint:fix              # Fix ESLint issues\npnpm type-check            # TypeScript type checking\npnpm format                # Format with Prettier\n\n# Testing\npnpm test:unit             # Playwright unit tests (unit project)\npnpm test:e2e              # Playwright end-to-end tests\npnpm test:visual           # Playwright + Chromatic full-page visual tests\n\n# Storybook\npnpm storybook             # Start Storybook dev server\npnpm build-storybook       # Build Storybook\npnpm chromatic             # Storybook Chromatic (needs CHROMATIC_STORYBOOK_TOKEN)\n\n# Content Management\npnpm lint:md               # Lint English markdown content\npnpm lint:md:fix           # Auto-fix header IDs and duplicates\n```\n\n### Testing Strategy\n\n- **Unit Testing**: Playwright `unit` project (`pnpm test:unit`) — heaviest coverage is the intl-pipeline sanitizer\n- **E2E Testing**: Playwright (`pnpm test:e2e`), see `docs/e2e-testing.md`\n- **Visual Testing**: Storybook + Chromatic for component regression; Playwright + Chromatic for full pages (see the `page-visual-tests` skill)\n- **Type Safety**: TypeScript strict mode enabled\n- **Linting**: ESLint with custom rules for imports and TypeScript\n\n## Content Management\n\n### Internationalization\n\n- **25 languages** supported (canonical list: `i18n.config.json`); **RTL support** for Arabic, Urdu\n- JSON UI strings in `src/intl/[locale]/`; translated markdown content in `public/content/translations/[locale]/`\n- Non-English markdown is propagated by the **intl-pipeline** (`src/scripts/intl-pipeline/`, entry `main.ts`). **Do not hand-propagate English changes into non-English files** -- let the pipeline run, or trigger `intl-pipeline.yml` with `stamp_only: true` if manifests must catch up urgently (e.g. unblocking a build). Hand-fixing a translation error is fine when the English side hasn't moved, since the manifest mapping stays valid. Spec: `tests/specs/PIPELINE-SPEC.md`.\n- Glossary: base URL from `GLOSSARY_API_URL` env var; default in `src/scripts/intl-pipeline/config.ts`. ETHGlossary is authoritative for Ethereum term translations.\n\nFor pipeline mechanics, recovery, manifests, ETHGlossary integration, and the `intl/pending-{base}` orchestration model, see the **`intl-pipeline` skill** at `.claude/skills/intl-pipeline/`. For translation-quality review (scoring rubric, language-group rules, ETHGlossary-as-authority policy, multi-agent role split), see the **`intl-review` skill** at `.claude/skills/intl-review/`.\n\n### Markdown Content\n\n- Educational content stored in `public/content/`\n- Processed with `next-mdx-remote`\n- Custom MDX components for rich content\n- Automatic table of contents generation\n- **All h1-h4 headings require a custom `{#lower-kebab-id}`** -- enforced by markdownlint via pre-commit hook. Run `pnpm lint:md:fix` to auto-add missing IDs. Config: `.markdownlint-cli2.jsonc`, custom rules: `.markdownlint-rules/`\n\n### Asset Management\n\n- Images optimized with Next.js Image component\n- SVGs loaded as React components via `@svgr/webpack`\n- Static assets served from `public/`\n- Placeholder generation for images\n\n## SEO & Meta\n\n- Sitemap generation in `app/sitemaps/sitemap.ts` (one sitemap per locale, indexed by `app/sitemap.xml/route.ts`)\n- Meta tags and Open Graph optimization\n- Structured data for search engines\n- Security headers (X-Frame-Options: DENY)\n\n## Development Guidelines\n\n### When Working on Features\n\n1. **Check existing patterns** - Look at similar components first\n2. **Prioritize Server Components** - Use App Router and Server Components when possible\n3. **Follow import order** - ESLint will enforce, but be proactive\n4. **Use TypeScript strictly** - No `any` types, prefer `unknown`\n5. **Test in Storybook** - Create stories for new components (filename pattern: `.stories.tsx`)\n6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations(\"common\")`) rather than reaching across namespaces\n7. **Mobile-first** - Design for mobile, enhance for desktop\n8. **Accessibility** - Use Radix primitives, semantic HTML\n9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales.\n\n### Component Development\n\n1. Create component in appropriate `src/components/` subdirectory\n   - Use `src/components/ui` for shadcn components or pure UI components\n2. Add TypeScript types and proper props interface\n3. Accept `ref` as a regular prop when needed (React 19); `forwardRef` only survives in legacy components\n4. Add a Storybook story (see below)\n5. Export from appropriate index file\n6. Update documentation if adding new patterns\n\n### Storybook stories\n\n**Location**: `__stories__/` for flat directories (`ui/`, `styles/`), co-located for foldered components. Story globs are recursive, so nesting works. **Filenames are kebab-case** (`app-card.stories.tsx`) even inside a PascalCase component directory -- enforced by the test below.\n\n**Title**: `Section / Group / Name`. The section is derived from the file path -- not a judgment call:\n\n| Path                                  | Section         |\n| ------------------------------------- | --------------- |\n| `src/styles/**`                       | `Design System` |\n| `src/components/ui/**`                | `UI`            |\n| `src/components/**` (everything else) | `Components`    |\n| `src/layouts/**`                      | `Layouts`       |\n| `app/**`                              | `Pages`         |\n\nUse a second-level group only when an established one fits (Actions, Forms, Layout, Navigation, Overlays, Data Display, Cards, Heroes, Site Chrome, Content, Data Viz, Features). **Don't invent a group for a single component** -- a flat `Components / Morpher` is correct and preferred. `tests/unit/storybook/story-titles.spec.ts` enforces the section prefix, explicit titles, and title uniqueness (Storybook silently merges duplicate titles into one sidebar entry).\n\n**Don't add a story for a deprecated component or variant.** A rendered example reads as an endorsement whatever the caption says, so the showcase should only contain things that are available to use -- note the deprecation in prose instead (see `HR`). A component whose future is merely undecided can keep its story with a status blurb (see `Carousel`).\n\nAdd `tags: [\"autodocs\"]`. Write the `parameters.docs.description.component` blurb for someone deciding whether to use the thing -- constraints and gotchas, not a restatement of the props table.\n\n**A green build does not mean a story renders.** `pnpm build-storybook` compiles stories without executing them, and every `UI /` story sets `chromatic: { disableSnapshot: true }`, so neither CI signal catches a story that throws. Open new stories in `pnpm storybook` before pushing.\n\nAsync server components render via a `next-intl/server` shim (`.storybook/next-intl-server.tsx`). Components calling `usePathname` need `parameters.nextjs.navigation.pathname`. New i18n namespaces must be added to the `ns` array in `.storybook/next-intl.ts`, or strings render as raw key tails.\n\n### Content Updates\n\n1. Markdown files go in `public/content/`\n2. Images in `public/images/` with descriptive names\n3. Translation strings in appropriate `src/intl/` JSON files\n4. Data files in `src/data/` with TypeScript types\n\n### Type-Safe Chain Names\n\nThis project enforces type-safe chain names via TypeScript. When working with layer 2 networks or wallet data:\n\n**Critical Files:**\n\n- `src/data/chains.ts` - Canonical source of all chain names (auto-updated weekly)\n- `src/lib/types.ts` - Defines `ChainName` type derived from chains.ts\n- `src/data/networks/networks.ts` - Uses `chainName: ChainName`\n- `src/data/wallets/wallet-data.ts` - Uses `supported_chains: ChainName[]`\n\n**Rules:**\n\n1. **Always look up exact names** - Before adding `chainName` or `supported_chains`, search `chains.ts` for the exact `name` value\n2. **Names are case-sensitive and exact** - e.g., use `\"Zircuit Mainnet\"` not `\"Zircuit\"`, use `\"OP Mainnet\"` not `\"Optimism\"`\n3. **Run type checking** - Use `pnpm type-check` to verify chain names are valid before committing\n4. **Non-EVM chains** - For Starknet and other non-EVM chains, use `NonEVMChainName` type\n\n**Common Mistakes:**\n\n- Using informal names: `\"Optimism\"` should be `\"OP Mainnet\"`\n- Missing \"Mainnet\" suffix: `\"Zircuit\"` should be `\"Zircuit Mainnet\"`\n- Wrong casing: `\"zksync Mainnet\"` should be `\"zkSync Mainnet\"`\n\n## Key Dependencies to Know\n\n### UI & Styling\n\n- `@radix-ui/*` - Accessible component primitives\n- `tailwind-variants` - Component variant patterns\n- `motion` - Animation library (formerly framer-motion; import from `motion/react`)\n- `lucide-react` - Icon library\n\n### Content & Data\n\n- `gray-matter` - Frontmatter parsing\n- `recharts` - Data visualization\n\n### Ethereum Integration\n\n- `viem` - Ethereum library\n- `wagmi` - React hooks for Ethereum\n- `@rainbow-me/rainbowkit` - Wallet connection\n\n## A/B Testing\n\nThe site uses a GDPR-compliant, cookie-less A/B testing system built on the Flags SDK precompute pattern, integrated with Matomo. The proxy assigns variants at the edge (header fingerprinting) and rewrites to signed, statically prerendered variant pages under `ab-code/[code]/` — experiments never force dynamic rendering. Experiment status, weights, and scheduling live in the Matomo dashboard (no deploys needed). Only the default locale is tested; other locales always get the original.\n\nKey gotcha: **variants are matched by array index, not names** — the `variants` array order must match the Matomo experiment order exactly, and the `testKey` must match the Matomo experiment name exactly.\n\nFull guide (architecture, step-by-step recipe, env vars): `docs/ab-testing.md`. Code: `proxy.ts`, `src/lib/ab-testing/`, `src/components/AB/`, `app/[locale]/ab-code/`.\n\n## Site Search\n\nSearch runs on a self-hosted Typesense instance, one collection per locale, crawled by DocSearch from production ethereum.org. Publishing is gated: a crawl goes to a staging collection and the live alias only moves if the new index passes size, field and relevance checks.\n\nTwo things bite most often. **Ranking hints reach the index as `docsearch:*` meta tags** from `src/lib/utils/metadata.ts`, so a ranking change does nothing until the site is deployed and re-crawled. And **the networks offered as block explorer results come from `src/data/networks/networks.ts`** -- the same list `/layer-2/networks` renders -- so adding an L2 there is the only edit needed.\n\nOverview: `docs/site-search.md`. Runbook, secrets and recovery: `typesense/README.md`. Code: `src/components/Search/`, `src/scripts/typesense/`, `typesense/`.\n\n## Deployment\n\n- **Platform**: Netlify (config in `netlify.toml`)\n- **Next.js Integration**: Uses `@netlify/plugin-nextjs` for seamless Netlify and Next.js compatibility\n- **Monitoring**: Matomo analytics integration\n\n### Runtime constraints\n\n- **`public/content` is not in the Netlify server function** (CDN-only). A page that reads it at render time must stay static (`revalidate: false` getters like `getStaticAppsData`) or have an `outputFileTracingIncludes` entry in `next.config.js`. A finite-`revalidate` getter (e.g. `getEventsData`, 24h) turns the whole route into ISR. Details: `docs/solutions/integration-issues/netlify-isr-404-async-server-components.md`.\n\n## Internal Infrastructure\n\nThe following external-looking services are managed by the ethereum.org team:\n\n- **`s3-dcl1.ethquokkaops.io`** — S3-compatible object storage for app screenshots and media. Used by the data layer to serve images for the `/dapps` and app listing pages. Downtime here means broken images on the live site.\n- **Netlify Blobs** (`@netlify/blobs`) — Key-value store used by the data layer to cache API responses. Accessed via `src/data-layer/storage.ts`.\n","isInternal":false,"tokens":3977,"sizeBytes":16657},{"name":"fix-sanitizer-bug.md","path":".claude/commands/fix-sanitizer-bug.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/fix-sanitizer-bug.md","title":"Command: /fix-sanitizer-bug","category":"command-prompt","format":"markdown","content":"---\ndescription: Guided workflow for fixing translation sanitizer bugs — triage, test, fix, verify\nallowed-tools: Bash, Read, Glob, Grep, Task, Edit, Write, AskUserQuestion\nargument-hint: [--language=CODE] [--issue=\"description\"] [--file=PATH] [--skip-build]\n---\n\n# Fix Sanitizer Bug\n\nIterative workflow for fixing bugs in the post-import translation sanitizer (`src/scripts/intl-pipeline/intl-sanitizer.ts`). Follows a test-first approach: triage the issue, write a failing test, implement the fix, verify across languages.\n\n## Context\n- Current branch: !`git branch --show-current`\n- Arguments: $ARGUMENTS\n- Sanitizer: `src/scripts/intl-pipeline/intl-sanitizer.ts`\n- Test files: `tests/unit/intl-pipeline/sanitizer/*.spec.ts`\n- Research docs: `docs/solutions/integration-issues/`\n\n## Phase 0: Gather Context\n\n### Parse Flags\n\nExtract from $ARGUMENTS:\n- `LANGUAGE`: from `--language=CODE` (e.g., `ja`, `zh-tw`, `es`)\n- `ISSUE_DESC`: from `--issue=\"...\"` (brief description of the bug)\n- `FILE_PATH`: from `--file=PATH` (specific file where issue was spotted)\n- `SKIP_BUILD`: from `--skip-build` (skip build verification)\n\n### Collect Information\n\nIf flags are missing, use AskUserQuestion to gather:\n\n1. **What language?** — Which locale has the issue (e.g., `ja`, `tr`, `zh-tw`)\n2. **What's the artifact?** — Exact text of the translation bug (copy-paste the broken string)\n3. **Where?** — File path or general area (markdown content, JSON translations, frontmatter)\n4. **English source?** — What does the correct English look like\n\nRead the affected file and the English source to confirm the issue:\n```\nTranslated: {FILE_PATH}\nEnglish:    {ENGLISH_EQUIVALENT_PATH}\n```\n\n**IMPORTANT:** Capture the exact broken pattern NOW before any processing. Copy the raw artifact verbatim — you'll need it for the test.\n\n## Phase 1: Document the Problem\n\nAppend the new pattern to the research doc:\n\n**File:** `docs/solutions/integration-issues/sanitizer-test-research.md`\n\nAdd a row to the \"New Patterns\" table:\n\n```markdown\n| N+1 | {PATTERN_DESCRIPTION} | {SOURCE_PR_OR_REVIEW} | `{EXACT_EXAMPLE}` | {SEVERITY} |\n```\n\nSeverity guide:\n- **Critical** — Breaks rendering, navigation, or MDX compilation\n- **High** — Breaks links, images, or loses content\n- **Medium** — Wrong text displayed, semantic errors\n- **Low** — Cosmetic, formatting-only\n\n## Phase 2: Triage — Fix, Warn, or Document Only\n\nThis is the most important decision. Use AskUserQuestion:\n\n**\"What type of fix does this need?\"**\n\n### Option A: Deterministic Fix (auto-correct)\nUse when the pattern is:\n- Regex-matchable with no false positives\n- The correct output is always the same (no judgment needed)\n- Safe to apply across all languages\n\nExamples: escaped bold `\\*\\*text\\*\\*`, ticker typos `EHT→ETH`, date format `DD/MM/YYYY→YYYY-MM-DD`\n\n→ Proceed to Phase 3A (write fix function + test)\n\n### Option B: Warning Only (detect + report)\nUse when:\n- The pattern is detectable but the fix requires context/judgment\n- Auto-fixing could cause collateral damage (see Bug #1: href substitution)\n- Different files may need different resolutions\n\nExamples: translated hrefs, missing brand names, code fence drift\n\n→ Proceed to Phase 3B (write warn function + test)\n\n### Option C: Document Only (not automatable)\nUse when:\n- The issue is semantic (wrong word choice, not a pattern)\n- No reliable regex can detect it\n- It needs human/AI review judgment\n\nExamples: \"Gas\" → \"Sprit\" (gasoline) in German, tone inconsistency\n\n→ Skip to Phase 6 (update docs only)\n\n## Phase 3A: Write Failing Test (Fix Function)\n\n### Determine which test file\n\n- Pure function (no English source needed) → `tests/unit/intl-pipeline/sanitizer/standalone-fixes.spec.ts`\n- Needs English comparison → `tests/unit/intl-pipeline/sanitizer/english-comparison.spec.ts`\n- End-to-end through processMarkdownFile/processJsonFile → `tests/unit/intl-pipeline/sanitizer/integration.spec.ts`\n\n### Write the test FIRST\n\nAdd a `test.describe` block for the new function. Include at minimum:\n\n```typescript\ntest.describe(\"fixNewIssue\", () => {\n  test(\"fixes the broken pattern\", () => {\n    const input = \"{EXACT_BROKEN_PATTERN_FROM_PHASE_0}\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(\"{EXPECTED_CORRECT_OUTPUT}\")\n    expect(fixCount).toBe(1)\n  })\n\n  test(\"leaves correct content unchanged\", () => {\n    const input = \"{ALREADY_CORRECT_CONTENT}\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(input)\n    expect(fixCount).toBe(0)\n  })\n\n  test(\"skips code blocks\", () => {\n    // If the fix operates on prose, it MUST skip code blocks\n    const input = \"```\\n{PATTERN_INSIDE_CODE}\\n```\"\n    const { content, fixCount } = fixNewIssue(input)\n    expect(content).toBe(input)\n    expect(fixCount).toBe(0)\n  })\n})\n```\n\n### Add import to test file\n\nAdd the new function name to the destructured import from `_testOnly` at the top of the test file.\n\n### Verify test fails\n\n```bash\nnpx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/{FILE}.spec.ts\n```\n\nThe new test MUST fail (function doesn't exist yet). Existing tests should still pass.\n\n## Phase 3B: Write Failing Test (Warn Function)\n\nSame as 3A but assert warnings instead of content changes:\n\n```typescript\ntest.describe(\"warnNewIssue\", () => {\n  test(\"warns on broken pattern\", () => {\n    const warnings = warnNewIssue(\"{BROKEN_INPUT}\", \"{ENGLISH_INPUT}\")\n    expect(warnings.length).toBeGreaterThan(0)\n    expect(warnings[0]).toContain(\"{EXPECTED_WARNING_SUBSTRING}\")\n  })\n\n  test(\"no warning on clean content\", () => {\n    const warnings = warnNewIssue(\"{CLEAN_INPUT}\", \"{ENGLISH_INPUT}\")\n    expect(warnings).toHaveLength(0)\n  })\n})\n```\n\nUse `tests/unit/intl-pipeline/sanitizer/warnings.spec.ts` for warn-only functions.\n\n## Phase 4: Implement the Fix\n\n### Write the function in the sanitizer\n\n**File:** `src/scripts/intl-pipeline/intl-sanitizer.ts`\n\n**For fix functions** — follow the established pattern:\n\n```typescript\nfunction fixNewIssue(content: string): {\n  content: string\n  fixCount: number\n} {\n  let fixCount = 0\n\n  // MANDATORY: Split to preserve code blocks\n  const codeBlockPattern = /(```[\\s\\S]*?```|~~~[\\s\\S]*?~~~|`[^`]+`)/g\n  const parts = content.split(codeBlockPattern)\n\n  for (let i = 0; i < parts.length; i++) {\n    if (i % 2 === 1) continue // Skip code blocks\n\n    // Your fix logic here\n    parts[i] = parts[i].replace(/{PATTERN}/g, () => {\n      fixCount++\n      return \"{REPLACEMENT}\"\n    })\n  }\n\n  return { content: parts.join(\"\"), fixCount }\n}\n```\n\n**Critical rules:**\n- ALWAYS split on code blocks first (fenced + inline)\n- ALWAYS return `{ content, fixCount }` for fix functions\n- ALWAYS return `string[]` for warn functions\n- Use word boundaries `\\b` for brand names to avoid partial matches\n- Use `escapeRegex()` when building regex from dynamic strings\n\n### Add to _testOnly export\n\nAdd the function name to the `_testOnly` export object near the bottom of the file.\n\n### Wire into processMarkdownFile or processJsonFile\n\nAdd the function call using the `applyFix` helper pattern:\n\n```typescript\napplyFix(\n  () => fixNewIssue(content),\n  (n) => `Fixed ${n} new issues`\n)\n```\n\n**Placement matters:** Consider whether the fix should run before or after existing fixes. Some fixes depend on others having run first.\n\nFor warn functions, add directly:\n```typescript\nconst newWarnings = warnNewIssue(content, englishMd)\nissues.push(...newWarnings)\n```\n\n## Phase 5: Run Tests and Verify\n\n### Step 1: Unit tests\n\n```bash\nnpx playwright test --project=unit tests/unit/intl-pipeline/sanitizer/\n```\n\n**All tests must pass** — both the new test and all existing 600+ tests.\n\nIf a test fails:\n- New test fails → fix the implementation, not the test\n- Existing test fails → your fix has a regression, investigate the interaction\n\n### Step 2: Run sanitizer against ONLY the affected files\n\n**CRITICAL: NEVER run the sanitizer against an entire language. It processes thousands of files and will hang for 30+ minutes. Always scope to the specific files from the PR.**\n\nDetermine which files to test from the PR context (e.g., `gaming/index.md`). The sanitizer's only env-var scoping is `TARGET_LANGUAGES` (per-language, not per-file) — there is no `TARGET_FILES`. For single files, write an inline script that calls `processMarkdownFile` directly:\n\n```bash\n# Language-level scoping (still processes every file for that language):\nTARGET_LANGUAGES={LANGUAGE} \\\n  pnpm exec tsx ./src/scripts/intl-pipeline/intl-sanitizer.ts\n\n# Preferred for single files — inline script calling processMarkdownFile directly:\npnpm exec tsx -e '\nconst { _testOnly } = require(\"./src/scripts/intl-pipeline/intl-sanitizer\");\nconst fs = require(\"fs\");\nconst file = \"public/content/translations/{LANGUAGE}/{PAGE_PATH}\";\nconst content = fs.readFileSync(file, \"utf8\");\nconst result = _testOnly.processMarkdownFile(file, content);\nconsole.log(result.issues.join(\"\\n\"));\nif (result.fixed) fs.writeFileSync(file, result.content);\n'\n```\n\nCheck the output for:\n- Your fix being applied (look for the issue label in the log)\n- No unexpected fixes in other areas\n- Fix count looks reasonable (not 0, not thousands)\n\n### Step 3: Inspect the actual changes\n\n```bash\ngit diff public/content/translations/{LANGUAGE}/{PAGE_PATH}\n```\n\nVerify:\n- The broken pattern is corrected\n- No collateral damage to surrounding content\n- Changes look correct to a human reader\n\n### Step 4: Build verification (conditional)\n\n**Only run this if the fix touches MDX syntax** — angle brackets, tags, components, backticks.\n\n**Skip if** `--skip-build` flag is set, or fix is purely textual (ticker corrections, brand tags, date normalization, guillemets, bold/italic escaping).\n\n```bash\nNEXT_PUBLIC_BUILD_LOCALES=en,{LANGUAGE} pnpm build\n```\n\n**NOTE:** This step requires `dangerouslyDisableSandbox: true` and significant RAM. Only use when the fix could affect MDX compilation.\n\n### Step 5: Cross-language spot check on the SAME file only\n\nTest the same page in 2-3 other languages to check for false positives. **NEVER run across all files for a language.**\n\n```bash\n# Test the same page path in a few other languages\nfor lang in es tr ja; do\n  pnpm exec tsx -e \"\n    const { _testOnly } = require('./src/scripts/intl-pipeline/intl-sanitizer');\n    const fs = require('fs');\n    const file = 'public/content/translations/$lang/{PAGE_PATH}';\n    if (!fs.existsSync(file)) { console.log('$lang: file not found, skipping'); process.exit(0); }\n    const content = fs.readFileSync(file, 'utf8');\n    const result = _testOnly.processMarkdownFile(file, content);\n    console.log('$lang:', result.issues.length ? result.issues.join('; ') : 'clean');\n  \"\ndone\n```\n\nCheck that your fix doesn't trigger unexpectedly in other languages.\n\n## Phase 6: If Not Resolved\n\nIf the fix doesn't resolve the issue after Phase 5:\n\n### Diagnose the root cause\n\nUse AskUserQuestion:\n\n**\"What went wrong?\"**\n\n1. **Pattern mismatch** — regex doesn't match the real-world variant\n   - Get more examples of the broken pattern\n   - Broaden the regex\n   - Add another test case for the variant\n   - Go back to Phase 4\n\n2. **Interaction effect** — another fix runs first and changes content\n   - Identify which fix runs first and transforms the input\n   - Reorder the fix in `processMarkdownFile` (earlier or later)\n   - Add an interaction test in `integration.spec.ts`\n   - Go back to Phase 4\n\n3. **False positives in other languages** — fix breaks something elsewhere\n   - Add language-specific exclusions\n   - Add a cross-language test case\n   - Consider making it warn-only instead\n   - Go back to Phase 3\n\n4. **Not actually automatable** — needs more context than regex can provide\n   - Convert to warn function or document-only\n   - Go back to Phase 2 and re-triage\n\n## Phase 7: Update Documentation\n\n### Update research doc\n\n**File:** `docs/solutions/integration-issues/sanitizer-test-research.md`\n\nIf the pattern was new (not already in the table), ensure it was added in Phase 1.\n\nIf the fix worked, move the pattern from \"New Patterns Not Yet Covered\" to \"Patterns Already Handled by Sanitizer\" with the function name.\n\n### Update existing bug docs if relevant\n\nCheck if this relates to previously documented bugs:\n- `docs/solutions/integration-issues/intl-pipeline-bugs-from-pr-18041-review.md`\n\n### Report summary\n\nDisplay to user:\n```\n## Fix Complete\n\n**Issue:** {ISSUE_DESCRIPTION}\n**Type:** {fix | warn | document-only}\n**Function:** {FUNCTION_NAME}\n**Test file:** {TEST_FILE}\n**Tests:** {N} new tests added, {TOTAL} total passing\n**Languages verified:** {LANGUAGES_CHECKED}\n**Files changed:**\n  - src/scripts/intl-pipeline/intl-sanitizer.ts (fix + export)\n  - tests/unit/intl-pipeline/sanitizer/{FILE}.spec.ts (new tests)\n  - docs/solutions/integration-issues/sanitizer-test-research.md (documentation)\n```\n\n","isInternal":false,"tokens":3206,"sizeBytes":12861},{"name":"netlify-build-check.md","path":".claude/commands/netlify-build-check.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/netlify-build-check.md","title":"Command: /netlify-build-check","category":"command-prompt","format":"markdown","content":"# Command: /netlify-build-check\n\nPath: `.claude/commands/netlify-build-check.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/netlify-build-check.md)","tokens":300,"sizeBytes":220},{"name":"prepare-release.md","path":".claude/commands/prepare-release.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/prepare-release.md","title":"Command: /prepare-release","category":"command-prompt","format":"markdown","content":"# Command: /prepare-release\n\nPath: `.claude/commands/prepare-release.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/prepare-release.md)","tokens":300,"sizeBytes":208},{"name":"review-release.md","path":".claude/commands/review-release.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/review-release.md","title":"Command: /review-release","category":"command-prompt","format":"markdown","content":"# Command: /review-release\n\nPath: `.claude/commands/review-release.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/review-release.md)","tokens":300,"sizeBytes":205},{"name":"review-translations.md","path":".claude/commands/review-translations.md","rawUrl":"https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/review-translations.md","title":"Command: /review-translations","category":"command-prompt","format":"markdown","content":"# Command: /review-translations\n\nPath: `.claude/commands/review-translations.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/commands/review-translations.md)","tokens":300,"sizeBytes":220}],"systemPromptSnippet":"<yakaai_skills repo=\"ethereum/ethereum-org-website\">\n<!-- File: AGENTS.md (Tokens: ~3977 | Category: root-instruction) -->\n# CLAUDE.md - Ethereum.org Website\n\n## Project Overview\n\nThis is the official Ethereum.org website - a Next.js application that serves as the primary educational and community hub for Ethereum. The site is built with modern web technologies and focuses on accessibility, internationalization, and performance.\n\n## Technology Stack\n\n### Core Framework\n\n- **Next.js 16+** - React framework with App Router (Turbopack is the default bundler; webpack available via `pnpm dev:webpack` / `pnpm build:webpack`)\n- **React 19** - UI library\n- **TypeScript 5.5+** - Type safety and development experience\n- **Tailwind CSS 4+** - Utility-first CSS framework (CSS-first config in `src/styles/global.css`)\n\n### Key Dependencies\n\n- **next-intl 4+** - Internationalization (i18n) with 25 languages\n- **next-mdx-remote 5.0+** - MDX content processing\n- **Motion 12+** (`motion` package, formerly Framer Motion) - Animations and transitions; import from `motion/react`\n- **Radix UI** - Accessible component primitives\n- **shadcn/ui** - Component library built on Radix UI\n- **Recharts** - Data visualization\n- **Viem/Wagmi** - Ethereum blockchain integration\n\n### Development & Testing\n\n- **Storybook 10+** - Component development and testing\n- **Chromatic** - Visual regression testing\n- **ESLint** - Code linting with custom rules\n- **Prettier** - Code formatting\n- **Husky** - Git hooks\n- **PNPM** - Package manager\n\n## Project Structure\n\n- **app/** - Next.js App Router pages\n  - **[locale]/** - Internationalized routes\n- **src/**\n  - **components/** - React components\n    - **ui/** - Design system components\n    - **icons/** - SVG icon components\n  - **data/** - Static data and configurations\n  - **hooks/** - Custom React hooks\n  - **i18n/** - Internationalization config\n  - **intl/** - Translation files (25 languages)\n  - **layouts/** - Page layout components\n  - **lib/** - Utility functions and types\n    - **constants.ts** - App constants\n    - **types.ts** - TypeScript type definitions\n    - **utils/** - Utility functions\n  - **styles/** - Global styles and design tokens\n- **public/** - Static assets\n  - **content/** - Markdown content files\n  - **images/** - Image assets\n- **docs/** - Development documentation\n  - **solutions/** - Documented solutions to past problems, organized by category with YAML frontmatter (module, tags, problem_type)\n\n## Code Conventions\n\n### File Naming\n\n- **Components**: kebab-case (e.g., `button-group.tsx`)\n- **Utilities**: camelCase (e.g., `cn.ts`, `relativePath.ts`)\n- **Pages**: kebab-case following Next.js conventions\n- **Assets**: kebab-case (e.g., `eth-logo.png`)\n\n### TypeScript Patterns\n\n- Use `interface` for object shapes, `type` for unions/intersections\n- Prefer explicit typing over `any` (ESLint enforces `fixToUnknown`)\n- **NEVER leave unused variables or parameters** - ESLint `unused-imports/no-unused-vars` will fail the Netlify build. The only allowed unused arg pattern is a single underscore `_`. Do NOT use `_prefixedNames` (e.g., `_foo`) - either use the variable or remove it from the signature entirely.\n- Use generic constraints for reusable components\n- Export types from dedicated files in `@/lib/types`\n\n### Styling Conventions\n\n- **Primary approach**: Tailwind CSS utility classes\n- **Component variants**: Use `tailwind-variants` (`tv`) for new and refactored work. Existing `class-variance-authority` (`cva`) components don't need bulk migration -- swap to `tv` opportunistically when you're already touching the component for another reason.\n- **Dynamic classes**: Use `cn()` utility (clsx + tailwind-merge)\n- **Custom properties**: CSS variables in `src/styles/` for theme values\n- **Responsive design**: Mobile-first approach\n\nFor UI work, see the **`design-system` skill** at `.claude/skills/design-system/`. It's the canonical knowledge base for component choices, design tokens, RTL/i18n, server/client boundaries, and the \"use a variant, not a new component\" pattern.\n\n## Development Workflows\n\n### Available Scripts\n\n```bash\n# Development\npnpm dev                    # Start development server (Turbopack)\npnpm dev:webpack            # Start development server with webpack\npnpm build                  # Build for production (Turbopack)\npnpm build:webpack          # Build for production with webpack\npnpm start                  # Start production server\n\n# Code Quality\npnpm lint                   # Run ESLint\npnpm lint:fix              # Fix ESLint issues\npnpm type-check            # TypeScript type checking\npnpm format                # Format with Prettier\n\n# Testing\npnpm test:unit             # Playwright unit tests (unit project)\npnpm test:e2e              # Playwright end-to-end tests\npnpm test:visual           # Playwright + Chromatic full-page visual tests\n\n# Storybook\npnpm storybook             # Start Storybook dev server\npnpm build-storybook       # Build Storybook\npnpm chromatic             # Storybook Chromatic (needs CHROMATIC_STORYBOOK_TOKEN)\n\n# Content Management\npnpm lint:md               # Lint English markdown content\npnpm lint:md:fix           # Auto-fix header IDs and duplicates\n```\n\n### Testing Strategy\n\n- **Unit Testing**: Playwright `unit` project (`pnpm test:unit`) — heaviest coverage is the intl-pipeline sanitizer\n- **E2E Testing**: Playwright (`pnpm test:e2e`), see `docs/e2e-testing.md`\n- **Visual Testing**: Storybook + Chromatic for component regression; Playwright + Chromatic for full pages (see the `page-visual-tests` skill)\n- **Type Safety**: TypeScript strict mode enabled\n- **Linting**: ESLint with custom rules for imports and TypeScript\n\n## Content Management\n\n### Internationalization\n\n- **25 languages** supported (canonical list: `i18n.config.json`); **RTL support** for Arabic, Urdu\n- JSON UI strings in `src/intl/[locale]/`; translated markdown content in `public/content/translations/[locale]/`\n- Non-English markdown is propagated by the **intl-pipeline** (`src/scripts/intl-pipeline/`, entry `main.ts`). **Do not hand-propagate English changes into non-English files** -- let the pipeline run, or trigger `intl-pipeline.yml` with `stamp_only: true` if manifests must catch up urgently (e.g. unblocking a build). Hand-fixing a translation error is fine when the English side hasn't moved, since the manifest mapping stays valid. Spec: `tests/specs/PIPELINE-SPEC.md`.\n- Glossary: base URL from `GLOSSARY_API_URL` env var; default in `src/scripts/intl-pipeline/config.ts`. ETHGlossary is authoritative for Ethereum term translations.\n\nFor pipeline mechanics, recovery, manifests, ETHGlossary integration, and the `intl/pending-{base}` orchestration model, see the **`intl-pipeline` skill** at `.claude/skills/intl-pipeline/`. For translation-quality review (scoring rubric, language-group rules, ETHGlossary-as-authority policy, multi-agent role split), see the **`intl-review` skill** at `.claude/skills/intl-review/`.\n\n### Markdown Content\n\n- Educational content stored in `public/content/`\n- Processed with `next-mdx-remote`\n- Custom MDX components for rich content\n- Automatic table of contents generation\n- **All h1-h4 headings require a custom `{#lower-kebab-id}`** -- enforced by markdownlint via pre-commit hook. Run `pnpm lint:md:fix` to auto-add missing IDs. Config: `.markdownlint-cli2.jsonc`, custom rules: `.markdownlint-rules/`\n\n### Asset Management\n\n- Images optimized with Next.js Image component\n- SVGs loaded as React components via `@svgr/webpack`\n- Static assets served from `public/`\n- Placeholder generation for images\n\n## SEO & Meta\n\n- Sitemap generation in `app/sitemaps/sitemap.ts` (one sitemap per locale, indexed by `app/sitemap.xml/route.ts`)\n- Meta tags and Open Graph optimization\n- Structured data for search engines\n- Security headers (X-Frame-Options: DENY)\n\n## Development Guidelines\n\n### When Working on Features\n\n1. **Check existing patterns** - Look at similar components first\n2. **Prioritize Server Components** - Use App Router and Server Components when possible\n3. **Follow import order** - ESLint will enforce, but be proactive\n4. **Use TypeScript strictly** - No `any` types, prefer `unknown`\n5. **Test in Storybook** - Create stories for new components (filename pattern: `.stories.tsx`)\n6. **Consider i18n** - All user-facing text should be translatable. Server components: `getTranslations` and `getLocale` from `next-intl/server`. Client components: `useTranslations` from `next-intl`, one namespace-bound function per namespace - to access a second namespace, bind another function (e.g. `const tCommon = useTranslations(\"common\")`) rather than reaching across namespaces\n7. **Mobile-first** - Design for mobile, enhance for desktop\n8. **Accessibility** - Use Radix primitives, semantic HTML\n9. **Use locale-aware formatting wrappers** - Use `numberFormat()` from `src/lib/utils/numbers.ts` instead of `new Intl.NumberFormat()`, and `dateTimeFormat()` from `src/lib/utils/date.ts` instead of `new Intl.DateTimeFormat()` / `.toLocaleDateString()` / `.toLocaleTimeString()`. Both enforce correct numbering systems and calendar for Urdu and Arabic locales.\n\n### Component Development\n\n1. Create component in appropriate `src/components/` subdirectory\n   - Use `src/components/ui` for shadcn components or pure UI components\n2. Add TypeScript types and proper props interface\n3. Accept `ref` as a regular prop when needed (React 19); `forwardRef` only survives in legacy components\n4. Add a Storybook story (see below)\n5. Export from appropriate index file\n6. Update documentation if adding new patterns\n\n### Storybook stories\n\n**Location**: `__stories__/` for flat directories (`ui/`, `styles/`), co-located for foldered components. Story globs are recursive, so nesting works. **Filenames are kebab-case** (`app-card.stories.tsx`) even inside a PascalCase component directory -- enforced by the test below.\n\n**Title**: `Section / Group / Name`. The section is derived from the file path -- not a judgment call:\n\n| Path                                  | Section         |\n| ------------------------------------- | --------------- |\n| `src/styles/**`                       | `Design System` |\n| `src/components/ui/**`                | `UI`            |\n| `src/components/**` (everything else) | `Components`    |\n| `src/layouts/**`                      | `Layouts`       |\n| `app/**`                              | `Pages`         |\n\nUse a second-level group only when an established one fits (Actions, Forms, Layout, Navigation, Overlays, Data Display, Cards, Heroes, Site Chrome, Content, Data Viz, Features). **Don't invent a group for a single component** -- a flat `Components / Morpher` is correct and preferred. `tests/unit/storybook/story-titles.spec.ts` enforces the section prefix, explicit titles, and title uniqueness (Storybook silently merges duplicate titles into one sidebar entry).\n\n**Don't add a story for a deprecated component or variant.** A rendered example reads as an endorsement whatever the caption says, so the showcase should only contain things that are available to use -- note the deprecation in prose instead (see `HR`). A component whose future is merely undecided can keep its story with a status blurb (see `Carousel`).\n\nAdd `tags: [\"autodocs\"]`. Write the `parameters.docs.description.component` blurb for someone deciding whether to use the thing -- constraints and gotchas, not a restatement of the props table.\n\n**A green build does not mean a story renders.** `pnpm build-storybook` compiles stories without executing them, and every `UI /` story sets `chromatic: { disableSnapshot: true }`, so neither CI signal catches a story that throws. Open new stories in `pnpm storybook` before pushing.\n\nAsync server components render via a `next-intl/server` shim (`.storybook/next-intl-server.tsx`). Components calling `usePathname` need `parameters.nextjs.navigation.pathname`. New i18n namespaces must be added to the `ns` array in `.storybook/next-intl.ts`, or strings render as raw key tails.\n\n### Content Updates\n\n1. Markdown files go in `public/content/`\n2. Images in `public/images/` with descriptive names\n3. Translation strings in appropriate `src/intl/` JSON files\n4. Data files in `src/data/` with TypeScript types\n\n### Type-Safe Chain Names\n\nThis project enforces type-safe chain names via TypeScript. When working with layer 2 networks or wallet data:\n\n**Critical Files:**\n\n- `src/data/chains.ts` - Canonical source of all chain names (auto-updated weekly)\n- `src/lib/types.ts` - Defines `ChainName` type derived from chains.ts\n- `src/data/networks/networks.ts` - Uses `chainName: ChainName`\n- `src/data/wallets/wallet-data.ts` - Uses `supported_chains: ChainName[]`\n\n**Rules:**\n\n1. **Always look up exact names** - Before adding `chainName` or `supported_chains`, search `chains.ts` for the exact `name` value\n2. **Names are case-sensitive and exact** - e.g., use `\"Zircuit Mainnet\"` not `\"Zircuit\"`, use `\"OP Mainnet\"` not `\"Optimism\"`\n3. **Run type checking** - Use `pnpm type-check` to verify chain names are valid before committing\n4. **Non-EVM chains** - For Starknet and other non-EVM chains, use `NonEVMChainName` type\n\n**Common Mistakes:**\n\n- Using informal names: `\"Optimism\"` should be `\"OP Mainnet\"`\n- Missing \"Mainnet\" suffix: `\"Zircuit\"` should be `\"Zircuit Mainnet\"`\n- Wrong casing: `\"zksync Mainnet\"` should be `\"zkSync Mainnet\"`\n\n## Key Dependencies to Know\n\n### UI & Styling\n\n- `@radix-ui/*` - Accessible component primitives\n- `tailwind-variants` - Component variant patterns\n- `motion` - Animation library (formerly framer-motion; import from `motion/react`)\n- `lucide-react` - Icon library\n\n### Content & Data\n\n- `gray-matter` - Frontmatter parsing\n- `recharts` - Data visualization\n\n### Ethereum Integration\n\n- `viem` - Ethereum library\n- `wagmi` - React hooks for Ethereum\n- `@rainbow-me/rainbowkit` - Wallet connection\n\n## A/B Testing\n\nThe site uses a GDPR-compliant, cookie-less A/B testing system built on the Flags SDK precompute pattern, integrated with Matomo. The proxy assigns variants at the edge (header fingerprinting) and rewrites to signed, statically prerendered variant pages under `ab-code/[code]/` — experiments never force dynamic rendering. Experiment status, weights, and scheduling live in the Matomo dashboard (no deploys needed). Only the default locale is tested; other locales always get the original.\n\nKey gotcha: **variants are matched by array index, not names** — the `variants` array order must match the Matomo experiment order exactly, and the `testKey` must match the Matomo experiment name exactly.\n\nFull guide (architecture, step-by-step recipe, env vars): `docs/ab-testing.md`. Code: `proxy.ts`, `src/lib/ab-testing/`, `src/components/AB/`, `app/[locale]/ab-code/`.\n\n## Site Search\n\nSearch runs on a self-hosted Typesense instance, one collection per locale, crawled by DocSearch from production ethereum.org. Publishing is gated: a crawl goes to a staging collection and the live alias only moves if the new index passes size, field and relevance checks.\n\nTwo things bite most often. **Ranking hints reach the index as `docsearch:*` meta tags** from `src/lib/utils/metadata.ts`, so a ranking change does nothing until the site is deployed and re-crawled. And **the networks offered as block explorer results come from `src/data/networks/networks.ts`** -- the same list `/layer-2/networks` renders -- so adding an L2 there is the only edit needed.\n\nOverview: `docs/site-search.md`. Runbook, secrets and recovery: `typesense/README.md`. Code: `src/components/Search/`, `src/scripts/typesense/`, `typesense/`.\n\n## Deployment\n\n- **Platform**: Netlify (config in `netlify.toml`)\n- **Next.js Integration**: Uses `@netlify/plugin-nextjs` for seamless Netlify and Next.js compatibility\n- **Monitoring**: Matomo analytics integration\n\n### Runtime constraints\n\n- **`public/content` is not in the Netlify server function** (CDN-only). A page that reads it at render time must stay static (`revalidate: false` getters like `getStaticAppsData`) or have an `outputFileTracingIncludes` entry in `next.config.js`. A finite-`revalidate` getter (e.g. `getEventsData`, 24h) turns the whole route into ISR. Details: `docs/solutions/integration-issues/netlify-isr-404-async-server-components.md`.\n\n## Internal Infrastructure\n\nThe following external-looking services are managed by the ethereum.org team:\n\n- **`s3-dcl1.ethquokkaops.io`** — S3-compatible object storage for app screenshots and media. Used by the data layer to serve images for the `/dapps` and app listing pages. Downtime here means broken images on the live site.\n- **Netlify Blobs** (`@netlify/blobs`) — Key-value store used by the data layer to cache API responses. Accessed via `src/data-layer/storage.ts`.\n\n<!-- File: .claude/skills/data-layer/SKILL.md (Tokens: ~1629 | Category: anthropic-skill) -->\n---\nname: data-layer\ndescription: This skill provides patterns for working with the data-layer module. Use when creating/editing files in src/data-layer/, src/lib/data/, or adding new data sources.\n---\n\n# Data Layer\n\n## Architecture\n\n```\nsrc/data-layer/\n├── fetchers/         # Fetch functions (one per data source)\n│   └── developer-tools/  # Multi-file fetcher (builder resources, GitHub/npm stats, ranking)\n├── index.ts          # Public API - typed getter functions\n├── tasks.ts          # Trigger.dev scheduled tasks\n├── storage.ts        # KEYS constant + get/set (Netlify Blobs or mock files)\n├── s3.ts             # S3 image upload utility for external images\n├── docs.md           # Module documentation\n├── mocks/            # Mock data files for local development\n└── .env.example      # Environment variables for data-layer/Trigger.dev\n\nsrc/lib/data/\n└── index.ts          # Next.js caching adapter (createCachedGetter)\n```\n\n## Environment Variables\n\nThe data-layer uses a **dedicated `.env.local`** at `src/data-layer/.env.local`, separate from the root `.env.local`: `cp src/data-layer/.env.example src/data-layer/.env.local`, fill in the keys (see `.env.example` for all options), then `pnpm trigger:dev` runs tasks locally. `GITHUB_TOKEN_READ_ONLY` and Sentry vars are shared with the main app (configure in both files); everything else (API keys, Netlify Blobs tokens, S3 credentials, Trigger.dev config) is data-layer only. In production, configure vars in the Trigger.dev project dashboard — the app and data-layer run in separate environments.\n\n## Key Files\n\n`storage.ts` defines the `KEYS` constant, `tasks.ts` the `WEEKLY`/`DAILY`/`HOURLY` task tuples; `index.ts` holds the one-liner getters — snippets for both under \"Adding a New Data Source\" below.\n\n### storage.ts - Storage Abstraction\n\n`get<T>(key)` / `set(key, data)` switch between Netlify Blobs (prod) and local mock JSON files (`USE_MOCK_DATA=true` for local development).\n\n### s3.ts - Image Upload Utility\n\nCentralized S3 upload for external images. Fetchers use this to upload external images to a single S3 bucket, reducing Next.js `remotePatterns` complexity.\n\n```typescript\n// Upload single image\nconst s3Url = await uploadToS3(sourceUrl, \"events/logos\")\n\n// Batch upload (parallel)\nconst s3Urls = await uploadManyToS3(urls, \"apps/banners\")\n```\n\nKey features:\n\n- **SSRF protection** - Blocks private/internal network addresses\n- **Deduplication** - SHA256 hash of source URL as key\n- **Existence check** - Skips if already uploaded\n- **5MB size limit** - Returns `null` for large images\n- **Content-Type detection** - From header or URL extension fallback\n\n## Rules\n\n### 1. Getters must be pure passthrough\n\nNo transformations in `index.ts` - just `get<T>(KEYS.X)`:\n\n```typescript\n// Correct\nexport const getEventsData = () => get<EventItem[]>(KEYS.EVENTS)\n\n// Wrong - no transformations in getters\nexport const getEventsData = () => {\n  const data = await get<EventItem[]>(KEYS.EVENTS)\n  return data?.map(transform) ?? null\n}\n```\n\nAll transformations belong in the fetcher (`src/data-layer/fetchers/`).\n\n### 2. KEYS is the single source of truth\n\nAll task IDs are defined in `KEYS` in `storage.ts`. The getter in `index.ts` and the task tuple in `WEEKLY`/`DAILY`/`HOURLY` must use the same key.\n\n### 3. Expose via lib/data for caching\n\nAdd cached wrapper in `src/lib/data/index.ts`:\n\n```typescript\nexport const getEventsData = createCachedGetter(\n  dataLayer.getEventsData,\n  [\"events-data\"],\n  CACHE_REVALIDATE_DAY // or CACHE_REVALIDATE_HOUR\n)\n```\n\nThe `revalidate` parameter is `number | false`. Passing `false` is a deliberate pattern to keep a route fully static — a finite revalidate opts the page into ISR, which fails on Netlify for pages reading `public/content/` files. Example: `getStaticAppsData` in `src/lib/data/index.ts`, used by components embedded in MDX pages (data refreshes only on deploy).\n\n### 4. Use S3 for external images\n\nExternal images should be uploaded to S3 in the fetcher to centralize image domains:\n\n```typescript\n// In fetcher - correct\nimport { uploadToS3 } from \"../s3\"\n\nconst logoUrl = await uploadToS3(event.logoImage, \"events/logos\")\nreturn { ...event, logoImage: logoUrl ?? \"\" }\n```\n\nAlways handle `null` returns (upload failures) with fallback/empty string.\n\n### 5. Keep fetchers isolated from the app\n\nFetchers run on Trigger.dev — a separate runtime, deployment, and bundle from the Next.js app. They cannot assume the app's filesystem, environment, or modules are available.\n\nAny import or runtime dependency reaching outside `src/data-layer/` is a warning sign. Allowed: types (`@/lib/types`, `@/lib/interfaces`), pure constants (`@/lib/constants`), and pure utility functions with no app-runtime dependencies. Not allowed: anything that reads `process.cwd()`, anything from `app/` or `public/`, anything from `src/components/`, or `src/lib/data/` (which wraps the data layer and would create a cycle).\n\nIf a fetcher needs data that lives in the app — content files, frontmatter, etc. — fetch it over the network via the GitHub API and treat the repo as an external system. See `fetchGitHubContributors.ts` for the pattern. Don't work around this with `additionalFiles` in `trigger.config.ts`; bundling app files into the data-layer deployment re-creates the coupling.\n\n## Adding a New Data Source\n\n1. **Create fetcher** in `src/data-layer/fetchers/fetchNewData.ts`:\n\n   ```typescript\n   export async function fetchNewData(): Promise<YourDataType> {\n     // Fetch and transform data here\n   }\n   ```\n\n2. **Add key** to `KEYS` in `src/data-layer/storage.ts`:\n\n   ```typescript\n   export const KEYS = {\n     // ...existing keys\n     NEW_DATA: \"fetch-new-data\",\n   } as const\n   ```\n\n3. **Add task tuple** to `WEEKLY`, `DAILY`, or `HOURLY` in `tasks.ts` (getter and tuple must use the same `KEYS` entry — Rule 2):\n\n   ```typescript\n   const DAILY: TaskDef[] = [\n     // ...existing tasks\n     [KEYS.NEW_DATA, fetchNewData],\n   ]\n   ```\n\n4. **Add getter** in `src/data-layer/index.ts`:\n\n   ```typescript\n   export const getNewData = () => get<YourDataType>(KEYS.NEW_DATA)\n   ```\n\n5. **Add mock file** at `src/data-layer/mocks/fetch-new-data.json` (read when `USE_MOCK_DATA=true`)\n\n6. **Add cached wrapper** in `src/lib/data/index.ts`:\n   ```typescript\n   export const getNewData = createCachedGetter(\n     dataLayer.getNewData,\n     [\"new-data\"],\n     CACHE_REVALIDATE_HOUR\n   )\n   ```\n\n<!-- File: .claude/skills/design-system/evals/evals.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Evals Skill\n\nPath: `.claude/skills/design-system/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/evals/evals.json)\n\n<!-- File: .claude/skills/design-system/references/a11y.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/a11y.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/a11y.md)\n\n<!-- File: .claude/skills/design-system/references/callout-walkthrough.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/callout-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/callout-walkthrough.md)\n\n<!-- File: .claude/skills/design-system/references/canonical-imports.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/canonical-imports.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/canonical-imports.md)\n\n<!-- File: .claude/skills/design-system/references/card-walkthrough.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/card-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/card-walkthrough.md)\n\n<!-- File: .claude/skills/design-system/references/cleanup-playbook.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/cleanup-playbook.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/cleanup-playbook.md)\n\n<!-- File: .claude/skills/design-system/references/components.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/components.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/components.md)\n\n<!-- File: .claude/skills/design-system/references/gotchas.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/gotchas.md)\n\n<!-- File: .claude/skills/design-system/references/i18n-rtl.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/i18n-rtl.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/i18n-rtl.md)\n\n<!-- File: .claude/skills/design-system/references/layouts.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/layouts.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/layouts.md)\n\n<!-- File: .claude/skills/design-system/references/new-component-checklist.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/new-component-checklist.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/new-component-checklist.md)\n\n<!-- File: .claude/skills/design-system/references/page-hero-walkthrough.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/page-hero-walkthrough.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/page-hero-walkthrough.md)\n\n<!-- File: .claude/skills/design-system/references/server-vs-client.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/server-vs-client.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/server-vs-client.md)\n\n<!-- File: .claude/skills/design-system/references/spacing-typography.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/spacing-typography.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/spacing-typography.md)\n\n<!-- File: .claude/skills/design-system/references/tokens.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/tokens.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/tokens.md)\n\n<!-- File: .claude/skills/design-system/references/variant-vs-new.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/design-system/references/variant-vs-new.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/design-system/references/variant-vs-new.md)\n\n<!-- File: .claude/skills/design-system/SKILL.md (Tokens: ~2862 | Category: anthropic-skill) -->\n---\nname: design-system\ndescription: Use when building, refactoring, or styling any UI in the ethereum.org Next.js site (`src/components/`, `app/`, `src/styles/`, `public/content/`, or any `.tsx`/`.mdx`/`.css` change that affects the rendered UI). Provides canonical component choices, design tokens, RTL/i18n rules, server/client guidance, and the \"use a variant, not a new component\" pattern for the project's Tailwind v4 + Radix + shadcn-style design system.\n---\n\n# ethereum.org Design System\n\nTailwind v4 (CSS-first config, no `tailwind.config.ts`) + React 19 / Next.js App Router + Radix UI primitives + shadcn-style component layer. Tokens live in CSS. Read this file fully on activation; pull from `references/` only when the listed trigger applies.\n\n## The Core Habit: Reuse Over Reinvent\n\nThe single highest-leverage habit for keeping this codebase consistent: **when you need new UI, look for a primitive or variant first, only invent if nothing fits.** Most \"new component\" instincts are actually \"new variant\" instincts in disguise.\n\nBefore you write any UI code, ask:\n- Is there a primitive that already does this? (`Card`, `Button`, `Alert`, `Tag`, `Hero/*`)\n- Is the difference small enough to express as a *variant* on an existing primitive?\n- Can I compose existing primitives instead of inlining a long Tailwind class chain?\n\nIf you find yourself writing `flex items-center gap-X rounded-Y border bg-... p-Z` for a card-like thing, you're reinventing `<Card>`. If you write `<p className=\"text-4xl font-bold\">N</p>` for a stat, you're reinventing `<BigNumber>`. If you write `<div className=\"text-5xl font-bold\">Title</div>`, you're reinventing `<h1>` (which is already styled by `base.css`). **Compose, don't inline.**\n\nWhen the existing primitive doesn't quite fit, the answer is usually \"add a variant,\" not \"create a new file.\" See `references/variant-vs-new.md`.\n\n## Top Rules\n\n1. **No raw `<a>` or `<button>`.** Use `<Button>`/`<ButtonLink>` from `@/components/ui/buttons/Button` and `InlineLink`/`BaseLink`/`LinkWithArrow` from `@/components/ui/Link`. These primitives handle event tracking, external-link safety, locale routing, and focus rings.\n2. **No raw color values.** Use semantic tokens (`text-body`, `bg-background`, `border-border`, `text-primary`). Hex literals and `rgb()` calls bypass dark mode.\n3. **Prefer adding a variant** to an existing primitive over creating a new component. Card, Button, Alert, Tag are the most common targets.\n4. **Server Components by default.** Only `\"use client\"` when you need state, effects, browser APIs, or inline event handlers.\n5. **All text is translatable.** `getTranslations` from `next-intl/server` (server) or `useTranslations` from `next-intl` (client). One namespace-bound `t` per namespace -- bind a second function (e.g. `const tCommon = useTranslations(\"common\")`) to access another namespace. The legacy `@/hooks/useTranslation` wrapper is deprecated for new code. Never hard-code user-facing English. **In an `app/[locale]/` page or `generateMetadata`, call `setRequestLocale(locale)` before any next-intl API** or on-demand renders throw `static to dynamic ... reason: headers` -- see `references/i18n-rtl.md`.\n6. **Logical CSS for direction.** Use `ms-`/`me-`/`ps-`/`pe-`/`inset-s-`/`inset-e-`/`border-s`/`border-e`/`text-start`/`text-end`. The site supports Arabic and Urdu (RTL). Hard-coded `left-`/`right-`/`ml-`/`mr-`/`pl-`/`pr-` breaks RTL.\n7. **Locale-aware formatters.** `numberFormat()` from `@/lib/utils/numbers`, `dateTimeFormat()` from `@/lib/utils/date`. Never `toLocaleString` / `Intl.NumberFormat` directly.\n8. **`useRtlFlip()` for directional icons** (right-pointing arrows/chevrons). Or use `ChevronNext`/`ChevronPrev` from `@/components/Chevron`.\n9. **Markdown content goes through `MdComponents`.** The `<Card>` markdown shortcode is backed by `@/components/MarkdownCard` (a thin wrapper around the `ui/card` primitives with an MDX-friendly prop shape). For app code, compose the primitives directly from `@/components/ui/card`.\n10. **Storybook stories ship with new UI components.** No automated unit tests; Storybook + Chromatic + types are the verification layer.\n11. **Don't add new layouts.** There are six canonical layouts (`TopicLayout`, `StaticLayout`, `DocsLayout`, `TutorialLayout`, `ContentLayout`, `BaseLayout`). New sectioned content goes in `src/data/topics/<key>.ts` as a `TopicLayout` config -- not a new layout component. See `references/layouts.md`.\n\n## Highest-Value Gotchas (index)\n\nLandmines where the code looks reasonable but the pattern is wrong. One line each; the canonical write-ups live in the named reference.\n\n- **Look-alike imports** -- Tooltip (`@/components/Tooltip`, not `ui/tooltip`), Modal (`ui/dialog-modal` vs `ui/dialog`; don't mix sources within a feature), Card (`ui/card` for app code, not `MarkdownCard`), Heroes (named exports of `@/components/Hero`; `title` is always the `<h1>`, no `header` prop). Decision trees: `references/canonical-imports.md`.\n- **Stale shadcn token names** (`bg-popover`, `bg-muted`, `text-muted-foreground`, ...) linger in a few `ui/` files but don't resolve in this project's tokens. Don't add new uses. List: `references/tokens.md`; replacements: `references/cleanup-playbook.md`.\n- **`useColorModeValue` is a deprecated Chakra leftover** -- use `dark:` + semantic tokens.\n- **`Card` is variant-driven, not `className`-driven.** Padding/spacing/background/radius/text color go through `variant`/`size`; a `className` override means a missing variant case. Full system: `references/card-walkthrough.md`. Surprise behaviors (`isSecondary` no-op on `solid`, `CardBanner fit=\"contain\"` auto-backdrop, `LinkBox` needs `LinkOverlay`, `commonControlClasses` shared with `Switch`): `references/gotchas.md`.\n- **No `Heading` primitive** -- `base.css` styles `<h1>`-`<h6>`; just write the tag. To match a heading level's size anywhere, use `text-h1`-`text-h6` (size + line-height only) -- never the raw `text-3xl lg:text-4xl` pair, and never re-apply a weight on a real heading. Canonical write-up: `references/spacing-typography.md`.\n- **Spacing is `.flow` + the `page`/`space`/`hero` tokens**, not hand-rolled margins. App pages follow a `<main className=\"p-page\"> > <MainArticle className=\"flow\"> > <Section id>` skeleton. Details: `references/spacing-typography.md`; token table: `references/tokens.md`.\n- **Locale formatting** -- one stray `toLocaleString` remains in `ui/chart.tsx`; don't add more. Use `numberFormat()`.\n\n### Shadows: default to the Tailwind scale\n\nPick by surface: dropdowns/tooltips `shadow-md`, cards/popovers/modals `shadow-lg`, large framed boxes/sheets `shadow-xl`. Three rules:\n\n1. Only two custom shadows exist (`shadow-primary-xl`, `shadow-primary-no-blur-*`, both in `utilities.css`) -- anything new needs a brand-tint justification.\n2. Hover elevation is the `hover-lift-*` utility or `Card hoverLift` (both `motion-safe`-gated), never a per-component resting/hover shadow pair.\n3. A genuinely-needed custom shadow is a raw `box-shadow` `@utility`, never an arbitrary `shadow-[...]` -- arbitrary shadows route color through `--tw-shadow-color`, which the global `* { dark:shadow-body }` rule grays out in dark mode.\n\nOld-shadow-token mapping table: `references/cleanup-playbook.md`.\n\n## Quick \"Where Do I Import From?\" Cheatsheet\n\n| I need... | Import |\n|---|---|\n| Card | `import { Card, CardBanner, CardContent, CardTitle, CardParagraph } from \"@/components/ui/card\"` |\n| Modal/Dialog (typical) | `import Modal from \"@/components/ui/dialog-modal\"` (default export) |\n| Side sheet | `import { Sheet, ... } from \"@/components/ui/sheet\"` |\n| Tooltip | `import Tooltip from \"@/components/Tooltip\"` (NOT `@/components/ui/tooltip`) |\n| Button | `import { Button, ButtonLink } from \"@/components/ui/buttons/Button\"` |\n| Anchor (in prose) | `import InlineLink from \"@/components/ui/Link\"` (default) |\n| Anchor (CTA with arrow) | `import { LinkWithArrow } from \"@/components/ui/Link\"` |\n| Page hero | `import { PageHero, HubHero } from \"@/components/Hero\"` |\n| Inline alert | `import { Alert, AlertContent, AlertDescription } from \"@/components/ui/alert\"` |\n| Top-of-page banner | `import { Alert } from \"@/components/ui/alert\"` then `<Alert variant=\"banner\">` |\n| Big numeric display | `import BigNumber from \"@/components/BigNumber\"` |\n| Layout | `import { Stack, HStack, VStack, Flex, Center } from \"@/components/ui/flex\"` |\n| Number formatting | `import { numberFormat } from \"@/lib/utils/numbers\"` |\n| Date formatting | `import { dateTimeFormat } from \"@/lib/utils/date\"` |\n| RTL flip helper | `import { useRtlFlip } from \"@/hooks/useRtlFlip\"` |\n\nFor full decision trees with all the look-alike landmines, see `references/canonical-imports.md`.\n\n## When to Load Each Reference\n\nPull these in only when the trigger applies. Don't read them all upfront.\n\n- **`references/canonical-imports.md`** -- unsure which of several look-alike imports is canonical.\n- **`references/components.md`** -- the full component inventory with variants and usage.\n- **`references/tokens.md`** -- adding a token, gradients, z-index, semantic-token choices, work in `src/styles/`.\n- **`references/spacing-typography.md`** -- page/section layout, heading sizes, spacing rhythm, the `.flow` system, citation and footnote markers.\n- **`references/gotchas.md`** -- unexpected primitive behavior; the long-tail confusion patterns.\n- **`references/variant-vs-new.md`** -- before creating any new component file.\n- **`references/cleanup-playbook.md`** -- refactoring existing anti-patterns; the \"old pattern -> new pattern\" map.\n- **`references/i18n-rtl.md`** -- user-facing text, number/date formatting, RTL, translation keys, `setRequestLocale`.\n- **`references/server-vs-client.md`** -- `\"use client\"` decisions and SSR-boundary structure.\n- **`references/a11y.md`** -- interactive elements, forms, images, heading hierarchy.\n- **`references/card-walkthrough.md`** -- any card-shaped UI work.\n- **`references/callout-walkthrough.md`** -- adding or modifying an in-content `Callout`.\n- **`references/page-hero-walkthrough.md`** -- a new page that needs a hero.\n- **`references/layouts.md`** -- layout selection, new topic hubs, one-off layout refactors.\n- **`references/new-component-checklist.md`** -- before opening a PR for a new component.\n\n## Other Project Skills That May Apply\n\n- **`data-layer`** -- For data fetching/sources. UI work that needs data should compose with this.\n\n## Pre-Merge Smoke Test\n\nBefore opening a PR for any UI work:\n\n- [ ] No raw `<a>` or `<button>`\n- [ ] No hard-coded colors (`#hex`, `rgb()`, `hsla()`); semantic tokens only\n- [ ] No `left-`/`right-`/`ml-`/`mr-`/`pl-`/`pr-` (use logical equivalents)\n- [ ] All user-facing strings translatable\n- [ ] `numberFormat()`/`dateTimeFormat()` for formatting (not native APIs)\n- [ ] Server Components wherever possible\n- [ ] New UI primitives have a `.stories.tsx`\n- [ ] Headings use `<h1>`-`<h6>` (not `<div className=\"text-5xl font-bold\">`)\n- [ ] If introducing a new component, justify why it isn't a variant of an existing one\n\n<!-- File: .claude/skills/image-gen/SKILL.md (Tokens: ~3470 | Category: anthropic-skill) -->\n---\nname: image-gen\ndescription: Generate illustration images (heroes, decorations, section art) for ethereum.org via the OpenRouter image API, then key/trim/check and place them in the repo. Trigger on \"make an image for\", \"generate a hero\", \"new illustration for <page>\", \"fix the glyph\", \"redraw this image\", \"chroma key this\", or edits to `src/scripts/image-gen/`. Not for screenshots, logos, or brand assets.\n---\n\n# Image generation\n\nThe user asks in chat (\"make a hero for /privacy\"); Claude drives the flow below. The script is Claude's tool for the parts that need the key or pixel math: it wraps OpenRouter's `POST /api/v1/images` and the post-processing. Three subcommands:\n\n```bash\npnpm tsx src/scripts/image-gen/main.ts models [--filter gemini]          # live params per model (aspect, resolution, background, n, ref limit)\npnpm tsx src/scripts/image-gen/main.ts generate --type cutout|full-bleed --prompt-file <f> [--ref <img>]... [--model <id>] [--aspect 16:9] [--quality <q>] [--eth-glyph auto|on|off] [--n 1] [--name <slug>] [--out-dir <dir>] [--dry-run]\npnpm tsx src/scripts/image-gen/main.ts process <img> --type cutout|full-bleed --out <file> [--key-color green|magenta] [--pad 0] [--max-size <px>]\n```\n\n## The key\n\n`OPENROUTER_API_KEY` lives in `.env.local`, loaded by the script itself. **Never read `.env.local`** (`.claude/settings.json` denies it; `.env.example` is fine to read). Never echo, log, or pass the key on a command line. Each person uses their own OpenRouter key with a spend limit set.\n\n**If a key ever appears in your context** (tool output, an error body, a file you read), tell the user right away that it was exposed and should be revoked and replaced at openrouter.ai/settings/keys, without repeating the key. Then carry on with the task; rotation happens separately.\n\n## Workflow\n\n1. **Context.** Read the target page and component, then decide the type. If the page doesn't settle it, ask with AskUserQuestion; never guess.\n   - **cutout** -- subject on transparency, nothing touching any edge (`PageHero` `heroImg`, section decorations).\n   - **full-bleed** -- scene fills the frame edge to edge (`HubHero` backgrounds, banners).\n2. **References.** Pick 2-6 existing images that match the target style (look at them first); see **House style** below. More refs pull harder toward house style; mismatched refs muddy it.\n3. **Prompt.** Write the subject/scene and style only. The script appends the cutout or full-bleed constraints (key color, margins, no borders) and, when the prompt mentions Ethereum, the ETH glyph spec and reference (see **The ETH glyph**), so don't restate them.\n4. **Confirm before spending.** Show the prompt, refs, model, aspect, and `--dry-run` output. Generate only after the user agrees, and get a fresh go-ahead for every further call; agreement to one generation isn't agreement to the next. A call costs about $0.11-0.26 and the price follows output pixels (21:9 is the cheapest, 1:1 and 3:2 the most).\n5. **Generate** one image per call: `--n 1`, never higher unless the user asks for a batch. Use a stable kebab `--name` describing the subject (see **Naming** below); reuse it for every iteration on that target, even if the subject drifts. Don't pass `--out-dir`: the default archive, `/tmp/image-gen/<name>/`, sits outside the repo and the session scratchpad, so candidates survive across iterations and sessions. Numbering continues (`-1`, `-2`, ...) and nothing is ever overwritten. Each candidate gets `<name>-<i>-raw.*`, a processed `<name>-<i>.png`, and `<name>-<i>.json` (prompt, refs, params, cost, check results).\n6. **Review.** Look at every output. `CHECK` lines are automatic failures (subject touching an edge, flat border strip, background not a key color); regenerate those rather than placing them. Then zoom in (crop at 3-4x) on every ETH glyph and every structure the user has flagged before, and check them against **The ETH glyph** and **Compositions that fail**. Report defects you find rather than letting the user discover them.\n7. **Place in situ, automatically.** Don't wait for a pick: put the best passing candidate at its target location right away so the user can judge it on the real page in their running dev server.\n   - `process` the archived raw (or `cp` the archived `.png`) to its final path, `public/images/<area>/<descriptive-name>.png`, sized to roughly 2x its largest rendered width with `--max-size`. Cutouts are trimmed to zero margin (`--pad 0`, the default); the page sets the spacing. Run a lossless PNG optimizer (oxipng) on the result.\n   - If the target also uses a portrait version on small viewports (a `*-portrait.png` beside it, e.g. some `HubHero` backgrounds), crop it from the landscape image rather than generating it separately, so both stay identical.\n   - Wire it into the target component (camelCase raster import, see the design-system skill's `page-hero-walkthrough.md`). If it replaces an existing image, repoint the import and leave the old file alone, so reverting is just `git checkout` on the component.\n   - Tell the user the URL to check (e.g. `/organizations/`) and list the other candidates by path.\n   - Never commit, and never start a dev server; the user runs their own.\n8. **Iterate in place.** Each new trial is copied from the archive over the same in-repo file, so the page updates without touching the import again. Static imports pick up the change through their content hash; for a plain `/images/...` string `src`, tell the user to hard-refresh. Revise by adding a candidate as a `--ref` with an updated prompt. To go back to an earlier candidate (\"I prefer the first one\"), copy that archived file in; never regenerate it. Always say which archive number is currently in situ.\n9. **Never delete the archive.** Leave `/tmp/image-gen/<name>/` alone, even after the image is placed. The user decides when it's done. When the user is happy, show `git status` so they can see exactly which files changed.\n\n## Editing an existing image\n\nMost work after the first generation is fixing details. Doing it well:\n\n- **Fix only what the user flagged.** Starting over or \"improving\" unflagged areas changes sizes, colors and objects the user had already accepted, and is usually worse.\n- **Redraw faithfully.** Pass the approved image as the sole (first) `--ref` and open with \"Redraw the reference image faithfully: identical camera, framing, <list every element>, same colors, same linework. Change only <X>: <exact description of the fix>. Everything else stays exactly as it is.\" OpenAI reproduces the rest closely.\n- **Take the whole redraw.** Patching just the fixed region back into the old image fails: a redraw shifts edges by around 10px. If everything else matches, place the full redraw.\n- **Check the neighbors.** Removing or changing one element can break an adjacent one (removing an elevator shaft truncated the next one). Compare the whole image against the previous version before placing.\n- **If the user supplies an edited version, it becomes the source.** Archive it in `/tmp/image-gen/<name>/`, place it, and use it as ref 1 for any later redraw. Never assume the user can edit images; for a detail the model keeps getting wrong, a manual edit can be suggested, not expected.\n\n## House style\n\nethereum.org illustrations are hand-drawn, airy and utopian. Good references: `public/images/home/hero.png` (plus a full-resolution crop of its palms and strokes, which shows the line texture better than the whole image), `public/images/heroes/developers-hub-hero.png`, `learn-hub-hero.png` and `guides-hub-hero.jpg`. Avoid the roadmap, garden and quizzes heroes as references; they lean purple and pink.\n\nCues that work in prompts:\n\n- Lines: hand-drawn in cobalt and royal blue, never black, lighter periwinkle for distant details. Gentle crayon quality: \"lines waver a little in weight and have slightly soft, lightly textured edges, fills carry a faint paper grain; subtle and clean, mostly crisp, not scratchy\". Full crayon wording overshoots into grain the user doesn't want (aim for about 75% clean, 25% texture).\n- Color: mostly white and pale blue with peach, orange and cyan accents; red awnings are fine. Flat fills, no gradients, glow or halos.\n- Architecture: sci-fi, white and rounded, slender columns, curved balconies, glass as pale blue panels with thin white grid lines, thin peach accent bands, rooftop plants. Classical forms (columns, pediments, domes) stay but in that white style. No brick or boxy present-day buildings.\n- Ground: white or pale-blue tiles; floating slab edges in pale periwinkle, not brown earth.\n- Plants: palms with feathery fronds of thin cobalt and periwinkle strokes, in white planters. No green, no round deciduous trees.\n- People: simple outlined figures, flat-colored clothes, simple hair, low detail.\n- Always add \"No text, letters, numbers or currency symbols anywhere, no logos other than the Ethereum diamond, no UI labels.\" Clock faces get no numbers.\n\n## The ETH glyph\n\nImage models get the Ethereum diamond wrong more than anything else: one solid rhombus, a white fill or a line bridging the gap, an extra lattice of facets, a glyph rotated off the surface it sits on, or a smudge on a small coin. The **shape** must match `src/components/icons/eth-home-icon.svg` exactly. Its **colors don't matter**: they can change to suit the design (a coin imprint in coin tones, a white glyph on a pediment).\n\nThe shape, for checking outputs and describing fixes (the script's reference renders it as unfilled outlines for this reason):\n\n- Two separate pieces stacked vertically, with an open gap between them where the background shows through.\n- Upper piece: a tall kite with exactly four facets. Its side corners sit about halfway down the whole glyph and its bottom point about two-thirds down. A center line runs top to bottom, and a line from each side corner rises to meet it about a third of the way down.\n- Lower piece: a downward chevron with exactly two facets. Its top edge is a shallow V parallel to the upper piece's bottom edges, and its tips sit just below the upper piece's corners.\n- About 1.6 times as tall as wide, symmetric, the same width top and bottom. On a tilted or foreshortened surface it follows that surface's perspective and keeps the same structure.\n\nHow the script helps: with `--eth-glyph auto` (the default), any prompt that mentions Ethereum, ETH, a glyph or a diamond gets the detailed shape spec (`src/scripts/image-gen/eth-glyph.ts`) appended and a rendering of the canonical glyph attached as the **last** reference image. User refs keep their order, so \"the first reference image\" still means yours; never call the glyph anything but \"the last reference image\" in your own prompt. Use `--eth-glyph on` to force it, `off` when the image has no glyph and the prompt only names Ethereum in passing, or when the reference slot is needed.\n\nIn prompts, also:\n\n- Ask for few glyphs, each drawn large. A glyph below about 15px in the output, or lying on foreshortened paper, is unreadable at any prompt fidelity; leave it off small coins and badges.\n- Say where each glyph sits and which way it faces (\"upright and centered at the top of the certificate\", \"lying flat in the center of the floor square, aligned with its edges\").\n- Don't ask for glows, highlights or \"dimensional\" facets on it; they turn into extra lines inside the shape.\n- To fix a wrong glyph on an otherwise approved image, redraw faithfully (see **Editing an existing image**) with \"Change only the Ethereum diamond on <where>\" and its position, size and facing; the spec and reference supply the shape.\n\n## Compositions that fail\n\nThese cost several failed generations each. Design them out in the prompt instead of fighting them:\n\n- **Merging or branching rail tracks:** the model can't draw a clean switch, and \"hide it under a train\" produced two misaligned trains. Use a single continuous track.\n- **Hard occlusion:** legs under tables, columns behind foreground objects, a bridge running behind a floating object. Use a solid-front desk or counter that reaches the floor, a single colonnade, fewer overlapping layers.\n- **Repeated structure:** column rows, colonnades and dome drums come out unevenly spaced. Spell it out (\"exactly four identical columns at exactly equal spacing on the same base line\") and check spacing at zoom.\n- **Impossible layering:** floors that don't continue behind objects, staircases that loop. Keep depth simple: one island, one level change.\n\n## Naming\n\nName images for what they depict, never for where they're used: a town square with people used as the `/some-page/` hero is `town-square-buildings-with-people.png`, not `some-page-hero.png`. Placement changes; content doesn't. Kebab-case, specific enough to tell it apart from neighbors in the same folder. The `--name` passed to `generate` follows the same rule, so the archive folder and the final file match. If the final image ends up depicting something different from the original name, rename the in-repo file to match what's in the image (the archive folder keeps its name).\n\n## Models\n\nDefault `openai/gpt-image-2.5-sunburst` at the highest `quality` it offers. It matches the house style (clothing, palms, architecture, ground, people, flat color) where `google/gemini-3-pro-image` (Nano Banana Pro) kept drawing black vector lines, purple and red casts and ordinary buildings despite explicit prompts. Switch with `--model` only when the user asks; run `models` to see what a model accepts -- the script validates flags against that list and drops options a model lacks.\n\n- **Cutouts:** OpenAI supports `background: transparent`, so the script requests real alpha and skips keying. Models without it get a flat key background that the script removes.\n- **Size:** OpenRouter exposes no size parameter; output tops out around 1536px on the long side (21:9 comes back 1536x656). For a wide hero that needs more, a local Real-ESRGAN upscale reads better on the page than the raw 1536px, though it flattens some crayon texture.\n- `n` is 1 per call on Gemini; `--n 3` makes three calls. Models that accept a higher `n` batch it.\n\n## Chroma keying\n\nOnly for models without native alpha, or for chroma images made elsewhere.\n\n- Green (`#00FF00`) by default. Use `--key-color magenta` when the subject itself is green or teal; a green subject keyed on green turns partly transparent.\n- Alpha comes from how strongly a pixel leans toward the sampled border color, then the background is unmixed from partially transparent pixels. Despill applies only to a 2px rim near transparency, so interior colors are untouched.\n- Prefer PNG sources. JPEG compression smears the key into dark outlines, which leaves a faint tinted rim.\n- `process` works on any existing chroma image, e.g. one generated by hand on the OpenRouter site.\n\n<!-- File: .claude/skills/intl-pipeline/evals/evals.json (Tokens: ~300 | Category: anthropic-skill) -->\n# Evals Skill\n\nPath: `.claude/skills/intl-pipeline/evals/evals.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/evals/evals.json)\n\n<!-- File: .claude/skills/intl-pipeline/references/architecture.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/architecture.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/architecture.md)\n\n<!-- File: .claude/skills/intl-pipeline/references/ethglossary.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/ethglossary.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/ethglossary.md)\n\n<!-- File: .claude/skills/intl-pipeline/references/gotchas.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/gotchas.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/gotchas.md)\n\n<!-- File: .claude/skills/intl-pipeline/references/manifests.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/manifests.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/manifests.md)\n\n<!-- File: .claude/skills/intl-pipeline/references/non-english-edits.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/non-english-edits.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/non-english-edits.md)\n\n<!-- File: .claude/skills/intl-pipeline/references/orchestration.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/orchestration.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/orchestration.md)\n\n<!-- File: .claude/skills/intl-pipeline/references/recovery.md (Tokens: ~300 | Category: anthropic-skill) -->\n# References Skill\n\nPath: `.claude/skills/intl-pipeline/references/recovery.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/recovery.md)\n\n<!-- File: .claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Runbooks Skill\n\nPath: `.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/cost-guard-tripped.md)\n\n<!-- File: .claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Runbooks Skill\n\nPath: `.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/ethereum/ethereum-org-website/HEAD/.claude/skills/intl-pipeline/references/runbooks/fix-sanitizer-bug.md)\n\n<!-- ... +31 more modular skills available via /api/skills/ethereum/ethereum-org-website?path={path} -->\n</yakaai_skills>"}