git:20260924.0e01459 to git:20260924.fa28c43

99 added, 243 removed. Audit A to A.

# Working on `web/`
- The site is two halves that share one ground, built by Astro and deployed to
- Firebase Hosting at https://eklavya-run.web.app by `.github/workflows/firebase.yml`.
+ The hand-written landing page and Astro Starlight manual share design tokens
+ and deploy to Firebase at https://eklavya-run.web.app. Read the root `CLAUDE.md`
+ and `.claude/skills/eklavya-design/SKILL.md` before changing them.
- | Path | What it is |
+ ## File map
+
+ | Path | Owns |
|---|---|
- | `public/index.html` | the landing page — the pitch. Hand-written HTML, copied verbatim into the build; there is no framework in it. |
- | `public/styles.css` | landing-page components. |
- | `public/tokens.css` | the design tokens: the accent ramp and type scale, then the two grounds. Read by **both** halves. |
- | `public/app.js` | landing-page behaviour: the interactive terminal, the scroll reveals, the ground toggle. |
- | `src/content/docs/docs/*.mdx` | the manual, one file per topic. The nesting is what puts them at `/docs/…`. |
- | `src/styles/docs.css` | Starlight skinned onto our grounds — it re-points `--sl-color-*` at the roles in `tokens.css`. |
- | `src/components/` | the three Starlight overrides: our wordmark, the ground toggle, and `PageTitle.astro` — the `<h1>` comes from frontmatter, so it paints the product's name itself. Plus `EmbeddedDiagram.astro`, the still-picture frame for a generated diagram. |
- | `src/plugins/rehype-brand.mjs` | paints every prose mention of **Eklavya** in the accent, at build time, over the rendered tree. |
- | `astro.config.mjs` | the sidebar, which is hand-ordered rather than autogenerated. |
+ | `public/index.html`, `styles.css`, `app.js` | Landing copy, components and interactive terminal |
+ | `public/tokens.css` | Shared scale and semantic roles for ink/paper themes |
+ | `src/content/docs/docs/*.mdx` | Manual pages at `/docs/…` |
+ | `src/styles/docs.css` | Starlight theme integration |
+ | `src/components/` | Branding, theme/head overrides, generated-diagram embed and simple `DocFlow` sequences |
+ | `src/plugins/rehype-brand.mjs` | Build-time accent on prose mentions of Eklavya |
+ | `astro.config.mjs` | Hand-ordered sidebar; every page needs an entry |
+ | `SEO.md`, `src/lib/seo.mjs` | Metadata, canonical identity and build/publication checks |
```bash
- npm run dev # localhost:4321, hot reload
- npm run build # -> dist/
- npm run preview # serve dist/ exactly as Firebase will
+ npm ci
+ npm run dev # development at localhost:4321
+ npm run build # production output plus SEO/link audit
+ npm run preview # serve the built site
```
- Firebase is the only host, and should stay that way: Astro's `base` is fixed at
- build time, so one build cannot serve both a root host and a subfolder host. A
- second host means a second build, which drifts.
-
- `firebase.json` sets the cache policy, and the split matters: **HTML is
- `no-cache`** (stored, but revalidated on every request) while `/_astro/**` is
- immutable for a year. Firebase's default is `max-age=3600` on everything, which
- meant a fix could ship, deploy green, and still be invisible to a reader for an
- hour — their cached HTML points at the previous build's fingerprinted CSS. The
- globs are deliberately non-overlapping: header precedence between two matching
- `headers` entries is undocumented.
-
- ## The rule that matters
+ ## Keep documentation aligned with source
- **The manual is a test of the source, not prose about it.** Every default, flag,
- tier, threshold and command name in it was read out of the code. When you change
- behaviour, update the matching page in the same commit — and read the value out
- of the file below rather than copying what the page already says, or the page
- compounds its own drift.
+ Behavior and documentation ship in the same PR. Check each factual claim
+ against the implementation, not against another page. Guides teach a task;
+ reference pages enumerate complete fields and commands. Keep a single canonical
+ home for details and link to it from shorter introductions.
- Every `.mdx` under `src/content/docs/docs/` has a row here, in sidebar order.
+ Every manual page has a row below. Add a row, sidebar entry, unique frontmatter
+ description and incoming link with each new page. Moving a page requires fixing
+ links and preserving useful anchors or adding a redirect.
| Page | Source of truth |
|---|---|
- | `index.mdx` | the loop as it actually runs: the seven hooks in `mcp/src/hooks/` and `max_questions_per_task` in `mcp/src/config.ts`. It also carries the one prerequisite — `mcp/package.json` `engines` — because `before-you-start.mdx` was merged into it. Keep it short: it is the page that says what the tool is, not how it works |
- | `installing.mdx` | `mcp/src/install.ts` (its numbered steps are the numbered steps on the page), `mcp/src/safe-write.ts` (the `.eklavya-bak` backups and the refusal to touch an unparseable file), `.mcp.json`, `hooks/run.mjs` (the background runtime heal). `README.md` points here rather than repeating it — keep it that way |
+ | `index.mdx` | the loop as it actually runs: the seven hooks in `mcp/src/hooks/` and `max_questions_per_task` in `mcp/src/config.ts`. It also carries the one prerequisite — `mcp/package.json` `engines` — read directly from `mcp/package.json`. Keep it short: it introduces the product and directs the reader to the next guide |
+ | `installing.mdx` | `mcp/src/install.ts` (recommended install and verification path), `mcp/src/onboard.ts` (settings walkthrough), `mcp/src/safe-write.ts` (the `.eklavya-bak` backups and the refusal to touch an unparseable file), `.mcp.json`, `hooks/run.mjs` (the background runtime heal). `README.md` points here rather than repeating it — keep it that way |
+ | `installation-options.mdx` | `mcp/src/install.ts`, plugin manifests, `.mcp.json`, `hooks/run.mjs`, `mcp/src/surface.ts`; verify marketplace, Cowork and contributor paths and their limitations |
+ | `updates.mdx` | `mcp/src/update.ts`, `install.ts`, `safe-write.ts`, `hooks/run.mjs`; update timing, repair, uninstall and retained data |
| `first-run.mdx` | `skills/setup/SKILL.md` |
| `first-session.mdx` | `hooks/hooks.json` and the seven hooks in `mcp/src/hooks/` |
| `dials.mdx` | `QuizConfig`, `Focus`, `Cadence`, `Difficulty`, `MemoryConfig` in `mcp/src/config.ts` — **and** `mcp/src/tools/get_session_quiz_plan.ts`, because the `interleaved` one-question cap and its exemptions live in the planner, not in the config |
| `levels-and-tiers.mdx` | `LEVEL_BANDS`, `LEVEL_UP_MIN_CONCEPTS`, `checkPromotion` in `mcp/src/srs.ts`; the tier labels are `TIER` in `mcp/src/assets/dashboard.html` and the tier table in `skills/tutor/SKILL.md` — `TIER_LABEL` left `ask.ts` with the settings line in 1.14 |
| `memory.mdx` | `mcp/src/memory/` — `privacy.ts` for the exclusion list and the secret shapes, `summarize.ts` for the local summariser and its ceiling, `embed.ts` and `search.ts` for what "semantic" means here, `tokens.ts` for what the savings percentage measures, `code.ts` for the scanner's ceiling. The tool arguments are `mcp/src/tools/memory_read_tools.ts`, `memory_write_tools.ts`, `code_tools.ts` and `collection_tools.ts`; the honesty constraints are ADR-03 and ADR-07. Privacy lives here rather than on its own page |
- | `commands.mdx` | one `###` per slash command — the skills under `skills/` carrying `disable-model-invocation: true`, nine of them; `skills/tutor/` is model-invocable only and gets no heading. Plus one for each skill under `user-skill/` — `eklavya` and `eklavya-artifacts` |
- | `cli.mdx` | the `USAGE` string in `mcp/src/cli.ts` |
+ | `commands.mdx` | the complete user-invocable inventory from `skills/` frontmatter, plus plain-chat skills under `user-skill/`; explain task, arguments and side effects without requiring one heading per command |
+ | `cli.mdx` | the `USAGE` string and command handlers in `mcp/src/cli.ts`, memory handlers in `cli-memory.ts`, and artifact handlers in `artifacts.ts` |
| `configuration.mdx` | `DEFAULT_CONFIG` in `mcp/src/config.ts` — every key, no omissions |
- | `packs.mdx` | `mcp/src/packs.ts` (the two directories, the merge order, the fail-open read) and `validateSeedGraph` in `mcp/src/seed.ts` for the field rules. `skills/pack/SKILL.md` is the command it documents |
+ | `packs.mdx` | `mcp/src/packs.ts` (seed, global, legacy checkout and project layers; merge order and fail-open reads) and `validateSeedGraph` in `mcp/src/seed.ts` for the field rules. `skills/pack/SKILL.md` is the command it documents |
| `commit-gate.mdx` | `mcp/src/store.ts` (`PASSING_GRADE`, `syncGate`, `gateRetryConcepts`), the `gate_work` pass in `mcp/src/tools/get_session_quiz_plan.ts`, `cli/eklavya-gate`, `mcp/src/hooks/pre-tool-gate.ts` and `commit-lib.ts` (what counts as a commit, and the accepted misses), `scripts/install-git-hook.sh` |
| `dashboard.mdx` | `DEFAULT_PORT` (in `mcp/src/paths.ts`), `startDashboard`, `dashboardState`, `ARTIFACT_CSP` and `projectInventory` (which rows establish a project, and how worktrees and missing checkouts are named) in `mcp/src/dashboard.ts`, `listArtifacts` in `mcp/src/artifacts.ts`; the three workflows, their sidebars, routes and legacy aliases in `mcp/src/assets/dashboard.html` (`WORKFLOWS`, `LEGACY`, `resolve`), and its page sizes (`PER`) |
- | `how-it-works.mdx` | the `mcp/` / `hooks/` / `skills/` split in the root `CLAUDE.md`, `mcp/package.json` `engines` and its `better-sqlite3` dependency, and `cli/eklavya-gate` for the one place `jq` and `sqlite3` are still needed. This is where architecture and rationale trimmed off `index.mdx` belongs. It also embeds the runtime diagram — see below |
+ | `how-it-works.mdx` | the `mcp/` / `hooks/` / `skills/` split in the root `CLAUDE.md`, `mcp/package.json` `engines` and its `better-sqlite3` dependency, and `cli/eklavya-gate` for the one place `jq` and `sqlite3` are still needed. This is where architecture and rationale trimmed off `index.mdx` belongs. It also embeds the runtime diagram and `DocFlow.astro` sequences; verify their labels against hook and planner code |
| `grading-engine.mdx` | `mcp/src/srs.ts` constants; the `get_session_quiz_plan` and `record_attempt` tool descriptions; `mcp/src/mcq.ts` and `skills/tutor/SKILL.md` for the option count, the "Other" escape hatch and the MCQ grade ceiling — none of which are in `srs.ts` |
| `your-data.mdx` | `mcp/src/paths.ts` (and `makePrivate`, the `0700`/`0600` modes), `mcp/src/db.ts`, `mcp/src/safe-write.ts`, `mcp/src/migrations/` |
- | `migrating.mdx` | `mcp/src/memory/import.ts` and the `memory import` handler in `mcp/src/cli.ts` — the flags and the field-disposition report are quoted from there |
+ | `migrating.mdx` | `mcp/src/memory/import.ts` and the `memory import` handler in `mcp/src/cli-memory.ts` — the flags and the field-disposition report are quoted from there |
| `troubleshooting.mdx` | `doctor` in `mcp/src/cli.ts`, and the fail-open paths — `mcp/src/hooks/lib.ts`, `cli/eklavya-gate` |
| `faq.mdx` | `mcp/src/paths.ts`, the `AttemptOutcome` handling in `mcp/src/store.ts` (what a decline does), `uninstall` in `mcp/src/install.ts`, and `localTokens` in `mcp/src/dashboard.ts` — the dashboard strips the shared tokens' Google Fonts import and loads no font of its own, which is what lets the FAQ say it makes no outbound request at all |
- Adding a page means adding its row here **and** an entry in `astro.config.mjs`'s
- sidebar — it is hand-ordered, so a new file that nobody links to is invisible.
- ## The runtime diagram is embedded, not copied
-
- `how-it-works.mdx` shows `docs/eklavya-runtime.html` in an `EmbeddedDiagram`.
- That file is **generated** — the root `CLAUDE.md` says how — so the site does not
- keep its own copy: `prebuild` and `predev` run `scripts/embed-diagram.mjs`,
- which copies it into `public/` (gitignored), and
- `.github/workflows/firebase.yml` lists it as a deploy trigger so regenerating
- the diagram republishes the page.
-
- The copy gains the site's icons, canonical URL and social preview metadata.
- It is marked `noindex, follow` because the explanatory manual page is the search
- entry point. The script changes only the copied file; the generated source in
- `docs/` remains untouched. A missing injection marker fails the build rather
- than silently publishing a viewer without metadata.
-
- The component is a still picture, deliberately. The frame loads `?embed=1`
- (which hides the viewer's toolbar) with `pointer-events: none`, because
- `?embed=1` alone leaves the click handlers live — a click zooms into a node and
- strands the reader with nothing on screen saying what happened. The way in is
- the real `<a>` at the top right, so middle-click and copy-link-address still
- work.
-
- Two things it does to the generated document on the iframe's `load`, by
- injection rather than by editing the HTML (hand-edits die on the next
- regeneration):
-
- - **A fit.** The viewer sizes its SVG `width: 100%; height: auto`, so a short
- frame would only crop it. The injected style gives the SVG both dimensions,
- and `preserveAspectRatio` centres it.
- - **A crop.** The canvas carries empty band above and below the drawing. After
- `document.fonts.ready` — text measures narrow before its face loads, and an
- early crop clips the widest labels — the union of the children's bounding
- boxes becomes the `viewBox`, skipping `defs` and the full-canvas background
- rect. Both are wrapped in try/catch: a diagram it cannot measure renders
- exactly as generated.
-
- The frame's `data-theme` is set from the page's, and re-set when the ground
- toggle fires — the viewer keys off the same `light`/`dark` values we do, so the
- picture follows the page rather than reloading.
-
- The caption is required, not decoration. An iframe has nowhere to put an
- `aria-label`, so the caption is what a screen-reader user gets — describe the
- whole picture in sentences.
-
- ## The landing page quotes real output
-
- `public/index.html` is the half that went stale once: it shipped promising two
- dials when four had landed, and later described `mode: off` as "installed but
- dormant" for as long as that dial existed — memory was recording the whole time. The root `CLAUDE.md` carries the section-by-section
- map. What that table hides is that the hero terminal is not a mock-up — it
- quotes **three separate pieces of real output**, each from its own file, and
- each has to be re-quoted when that file changes:
-
- | Terminal line | Where the string is built |
- |---|---|
- | the `[Eklavya] Learner profile: …` banner | `mcp/src/hooks/session-start.ts` — one line in the real hook, wrapped over two rows here the way a terminal wraps it |
- | the `[EKLAVYA concept · interleaved · easy]` status bar | `statusLine` in `mcp/src/statusline.ts`, composed centrally so it cannot drift. There is no settings line above a question any more — do not draw one |
- | the checkpoint instruction | `mcp/src/hooks/checkpoint-quiz.ts` |
-
- `public/styles.css` holds a **no-JS fallback copy** of the terminal's grade,
- explanation and done text, in `.term__screen:not([data-ready]) …::after` rules.
- Edit `VERDICTS` in `app.js` without mirroring them and the two disagree
- silently, for exactly the readers who cannot see the disagreement.
-
- The git commit gate is **opt-in**: nothing in `eklavya install` puts a
- `pre-commit` hook on a repo, `scripts/install-git-hook.sh` does, as a separate
- step. Any line here promising a bare-terminal commit is held has to attach that
- step, or it is a promise the default install does not keep.
-
- ## Writing conventions for the manual
-
- - **Audience: a developer in their first week.** No prior MCP knowledge. Explain
- the jargon the first time it appears, in one clause, then use it freely.
- - **Say what happens, then why.** The why is one sentence, never a paragraph.
- - Sentence case. No emoji. Second person. Present tense.
- - **Numbers over adjectives.** "Four options, capped at grade 4" beats "a few
- carefully chosen options".
- - **Never document unshipped work.** "The code exists" is not enough on its own
- — it has to actually work end to end for the reader.
- **Cursor is the standing example:** `eklavya export-rules` is implemented and
- tested, and the git gate is genuinely editor-agnostic, but Cursor gets no hooks
- at all — no session profile, no mid-task checkpoints, no end-of-task sweep,
- which is the entire ambient loop. Documenting it was promising the headline
- feature somewhere it does not run, so it stays out of the manual, the landing
- page and `README.md`. Put it back when Cursor gets hooks, and lead with what
- Cursor does *not* get.
- - Prefer a table when every row has the same shape, `<Steps>` when order
- matters, and prose when neither is true. A two-row table is prose pretending
- to be data.
- - Every command block a reader would paste must be runnable verbatim.
- Placeholders are `/path/to/eklavya` and `<topic>`, consistently.
- - Do not invent Claude Code UI affordances. If a flow is not verified in
- `docs/verified-schemas.md`, the README, or a skill, describe it as the
- interactive `/plugin` menu rather than guessing a subcommand.
+ ## Write for a developer's first week
- Starlight supplies the page title from frontmatter, the contents list from your
- `##`/`###` headings, and prev/next from the sidebar order. Use `:::note`,
- `:::tip` and `:::caution` for asides, `<Steps>` for ordered procedures, and a
- fenced block's `title="…"` for a filename label.
+ Lead with the outcome, show the shortest working path, then separate optional
+ routes and troubleshooting. Explain jargon once. Use second person, present
+ tense, sentence case and concrete examples. Prefer short paragraphs, ordered
+ steps for procedures and tables for repeated fields; avoid long bullet walls.
+ Do not use emoji or unsupported performance/privacy promises.
- ## CSS
+ Installation's primary page should get the reader installed and verified.
+ Alternative hosts and development setup belong in `installation-options`;
+ upgrades/removal in `updates`; complete flags in `cli`; complete settings in
+ `configuration`. Do not grow the quick start back into an exhaustive reference.
- - **Name a role, never a scale step or a raw hex.** `--ink`, `--dim`, `--faint`,
- `--line`, `--line-2`, `--panel`, `--mass`, `--spot`, `--spot-ink`. A raw hex or
- a `--vd-*` step in a component pins it to one ground and the theme toggle
- silently stops working for it. The one exception is the terminal, whose
- palette is fixed on both grounds and declared at the top of `styles.css`.
- - This check catches the whole class of mistake — it skips the two blocks
- allowed to hold fixed values and reports anything else:
+ Command examples must run as written; identify required replacements such as
+ `/path/to/eklavya` and `<topic>`. Describe unverified host UI as the interactive
+ `/plugin` menu rather than guessing subcommands. Exported rules and a standalone
+ MCP connection do not provide another editor with Claude Code's ambient hooks.
- ```bash
- cd public && python3 - <<'EOF'
- import re
- src = open('styles.css').read().split('\n')
- def span(a, b):
- i = next(n for n, l in enumerate(src) if l.strip() == a)
- j = next(n for n, l in enumerate(src) if l.strip() == b and n > i)
- return range(i, j)
- skip = set(span("THE TERMINAL'S OWN PALETTE", '}')) | set(span('THE TERMINAL', 'WHY'))
- bad = [f'{n+1}: {l.strip()}' for n, l in enumerate(src)
- if n not in skip and re.search(r'var\(--(neutral|green|vd)-|#[0-9a-fA-F]{3,8}\b', l)]
- print('\n'.join(bad) if bad else 'clean')
- EOF
- ```
+ Starlight supplies the title, table of contents and previous/next navigation.
+ Use `##`/`###`, `<Steps>`, restrained notes/tips/cautions, and code-fence titles
+ for filenames. Keep warnings next to the action they affect.
- - Also check that no rule paints text the same colour as its own fill. That is
- what a collapsed token looks like, and it renders as an invisible label rather
- than an error.
- - `src/styles/docs.css` only re-points Starlight's `--sl-color-*` at our roles.
- Starlight declares its accents inside its own `[data-theme]` blocks, which
- outrank a bare `:root` — so an accent override has to appear in **both**
- theme blocks or it silently loses in one of them.
- - `.claude/skills/eklavya-design/SKILL.md` is the visual language: verdigris on
- warm ink or warm paper, Archivo and Inter and JetBrains Mono, square chrome,
- hairline rules, bow-and-arrow motifs, no emoji.
- - **The hero terminal and the arrow flight are showpieces and are not to be
- redesigned.** Recolour them onto the ground tokens if you must; leave the
- geometry, the timing and the behaviour alone. The terminal deliberately keeps
- one dark palette on *both* grounds — it is a picture of a terminal.
- - **The product's name is accented wherever the manual says it.**
- `src/plugins/rehype-brand.mjs` wraps prose `Eklavya` in `.brand-word`;
- `PageTitle.astro` does the same for the frontmatter `<h1>`. Do not hand-write
- the span — a page added later would miss it. The plugin deliberately skips
- `code`, `pre`, `kbd` and `a`, and only matches the capitalised name standing
- alone: lowercase `eklavya` is a binary, a slug or a config key.
- - Tables: cells use `overflow-wrap: **break-word**`, never `anywhere`. Both
- break a long slug rather than let it push the table, but `anywhere` also
- shrinks a cell's *min-content* to one character — and the key column is
- `width: 1%` ("as narrow as you can be"), so the browser obliged and the
- troubleshooting table read `S Y M P T O M` down the page. The key column also
- carries a `14ch` floor, and inline code wraps everywhere but the first column.
- - Asides come back to our palette: note and tip take `--spot`, caution
- `--warning`, danger `--error`. Starlight's own blue, purple, orange and red
- are four hues that appear nowhere else on this site.
- - `public/` assets carry a `?v=N` query. Bump it when you change one, or
- returning readers get the cached copy.
- - Grid columns holding code blocks need `minmax(0, 1fr)`, not `1fr` — an `auto`
- track refuses to shrink below the widest `<pre>` and pushes the page sideways.
- - Honour `prefers-reduced-motion` in anything animated.
+ ## Diagrams
- ## Before you commit
+ Use `DocFlow.astro` for short ordered sequences or timelines. Keep stage labels
+ brief and put conditions, branch outcomes and limitations in nearby prose.
+ Meaning must survive without arrows, color or JavaScript; maintain a readable
+ text order, accessible caption and narrow-screen layout. Update each flow with
+ the behavior it depicts and inspect both themes.
- ```bash
- npm run build && npm run preview # localhost:4321
- ```
+ `EmbeddedDiagram.astro` displays the detailed generated runtime architecture.
+ `prebuild`/`predev` copy `docs/eklavya-runtime.html` through
+ `scripts/embed-diagram.mjs`; do not edit the generated HTML or commit the public
+ copy. Root guidance owns regeneration. The copy alone receives website icons,
+ canonical/share metadata and `noindex, follow`; missing markers fail the build.
- Check, every time:
+ The embedded viewer is a noninteractive still picture with a real link to open
+ the full viewer. Preserve disabled pointer events, fit/crop after fonts load,
+ fallback when measuring fails, and theme synchronization. Supply an iframe
+ title and explanatory caption; neither alone substitutes for the diagram's
+ information in text.
- 1. **1280, 900 and 560px, in both grounds.** Use the toggle in the nav — a bug
- that only shows on paper is the commonest kind now.
- 2. **No horizontal page scroll** at 560:
- `document.documentElement.scrollWidth === document.documentElement.clientWidth`.
- 3. **No console errors**, the hero terminal boots and accepts input, and the
- docs sidebar marks the current page.
- 4. **Every link and anchor resolves**, across the built site:
+ ## Landing output and design invariants
- ```bash
- python3 - <<'EOF'
- import re
- from pathlib import Path
- dist = Path('dist'); pages = list(dist.rglob('*.html'))
- text = {f: f.read_text('utf8', errors='ignore') for f in pages}
- ids = {f: set(re.findall(r'id="([^"]+)"', t)) for f, t in text.items()}
- def resolve(href):
- p = href.split('#')[0].split('?')[0].lstrip('/')
- for c in (['index.html'] if p == '' else [p, p.rstrip('/') + '/index.html', p.rstrip('/') + '.html']):
- if (dist / c).is_file():
- return dist / c
- bad = []
- for f, t in text.items():
- for h in set(re.findall(r'href="(/[^"]*)"', t)):
- tgt = resolve(h)
- if tgt is None: bad.append(f'{f.relative_to(dist)} -> {h} (no page)')
- elif '#' in h and h.split('#', 1)[1] not in ids[tgt]:
- bad.append(f'{f.relative_to(dist)} -> {h} (no anchor)')
- for x in set(re.findall(r'href="#([^"]+)"', t)):
- if x not in ids[f]: bad.append(f'{f.relative_to(dist)} -> #{x} (no anchor)')
- print('\n'.join(bad) if bad else f'{len(pages)} pages, all links and anchors resolve')
- EOF
- ```
+ The hero quotes real output: SessionStart banner from `session-start.ts`, dials
+ from `statusLine` in `statusline.ts`, checkpoint from `checkpoint-quiz.ts`.
+ Re-quote affected examples when those outputs change. Keep `app.js` `VERDICTS`
+ aligned with the no-JS fallback in `.term__screen:not([data-ready])` CSS.
+ Terminal commit gating requires the separate git-hook installer and project
+ enforcement; never promise it from the default install alone.
- Site-only work is a `docs:` commit — it does not cut a release.
+ - Use role tokens (`--ink`, `--dim`, `--line`, `--spot`, etc.), not raw colors or
+ palette scale steps. The hero terminal deliberately keeps its fixed palette.
+ - Preserve terminal/arrow geometry, timing and behavior. Honor reduced motion.
+ - Override Starlight colors in both theme blocks. Check text against its own
+ fill so token mistakes do not create invisible labels.
+ - Keep table cells at `overflow-wrap: break-word`, not `anywhere`; retain the
+ first-column width floor. Code-containing grids need `minmax(0, 1fr)`.
+ - Let `rehype-brand` and `PageTitle.astro` accent the product name; do not add
+ manual spans. Lowercase `eklavya` remains a binary/key, not branded prose.
+ - Bump affected `?v=N` queries when changing hand-written public assets.
+ - Preserve Firebase HTML revalidation and immutable fingerprinted assets in
+ non-overlapping header rules. A second hosting path requires another build.
- ## Search metadata and share images
+ ## Before opening a PR
- `SEO.md` describes the public identity, build checks and publication checklist.
- Every manual page needs a unique frontmatter `description`. Its share card is
- rendered automatically from the same title and description. `Head.astro` adds
- social tags and structured data without replacing Starlight's head behavior.
+ Build and require the SEO audit to pass. It checks internal pages, fragments,
+ resources, metadata and README website links. Preview changed pages at 1280,
+ 900 and 560px in both themes, with no horizontal page scroll, console errors,
+ broken sidebar state or inaccessible controls. Test terminal interaction when
+ landing behavior changes. Report the actual checks, including any not run.
- `src/lib/seo.mjs` owns the canonical origin and product identity. After changing
- it, run `npm run seo:refresh` to refresh the handwritten homepage's marked SEO
- block. After editing `public/brand/mark.svg`, run `npm run icons` and commit the
- exports. Every build runs `check:seo` over the output; it must pass before deploy.
+ After changing shared identity, run `npm run seo:refresh` and commit the
+ homepage update. The root README reuses `public/brand/mark.svg` directly; do not introduce another logo.
+ After changing `public/brand/mark.svg`, run `npm run icons`
+ and commit exports. Share cards derive from page title/description. Follow
+ `SEO.md` for publishing checks; a local build is not proof of indexing or a
+ production deployment. Site-only commits use `docs:` and do not cut a release.