Imported from mobetter20/mobetter20.github.io (
AGENTS.md). Install upstream withnpx skills add mobetter20/mobetter20.github.io. Copyright stays with the author.
mobetter20.github.io
Static site hosted on GitHub Pages. Contains the creative house at ajin.im/is/writing/ and the Avian Municipal District universe.
Design System — typographic tiers
Governing rule: consistent chrome, free content. The wrapper is uniform; what's inside is free to vary by tier. When adding or restyling a page, classify it first, then give it that tier's chrome.
Tier test — does the visitor dwell in the page, or scan past it?
| Tier | Test | Serif | Where |
|---|---|---|---|
| Frame | scan-and-navigate; points at things | EB Garamond | /, /is/running, /is/reading, /is/learning, /is/building |
| House | dwell; the page is the made thing | EB Garamond (creative-house.css) |
/is/writing + rooms (bird-coo, bird-docket) |
| Bespoke | a singular work whose form is part of the art | its own | avian-district, secondnest, konbini, /writes (the running log) + /wrote (sealed comedy archive) + their reading pages (writes.css), perch-chat, Small Ware's sub-tools |
Tier follows a page's nature, not its URL parent — konbini (bespoke) sits under frame /is/learning; Small Ware (house) sits under frame /is/building. A sub-page keeps its own tier and gets a working up-link to its parent; it does NOT inherit the parent's serif.
Chrome = a 5-part envelope: (1) the "ajin.im is ___" title device — faint prefix, italic verb, "ajin.im" links home (target="_self"); (2) a way home / up-link; (3) a contact/footer line; (4) DM Mono for labels; (5) warm-dark palette (bg #1a1612 frame / #110d0b house, text #e8e0d0).
Chrome is a gradient, not a switch:
- Frame + House wear the FULL envelope. The title device renders in the page's own serif: frame uses
.title(intemplates/*.html), house uses.house-title(increative-house.css). Same device, shared serif EB Garamond — tier is carried by palette + chrome + content, not typeface. - Bespoke wear only the ANCHORS — a way home + palette sympathy + DM Mono if labels appear. Do NOT force the title device on them; their singular design is intentional, and flattening it is a regression.
- The "made by ajin.im" colophon is the standard form of that way home for Bespoke pages and standalone sites (own domain) that don't wear the title device. It's a maker's signature — meta, sitting outside the fiction — so it rides fiction-heavy worlds (the bird-universe, BIC) where a back-breadcrumb would break character; keep it to one discreet, muted footer line. Internal ajin.im pages link
/(same tab); standalone sites linkhttps://ajin.im(new tab,rel=noopener). On: omen.ops, BIC (propagandaformyself.xyz), seoulcrushing, bird-universe avian-district (in-world hub). NOT on: standard section pages (title device already links home), the sealed bird-universe leaf rooms, bird-coo / The Municipal Coo (it added footer bulk — rejected 2026-06), or archived/noindex pages. Per-site bird-universe status (front door = bird-coo, internal hub = avian-district, archived = bird-docket) lives in_scripts/bird-universe/bird_universe_registry.json.
Within the house, .house-title marks section entrances and orphan URLs (/is/writing → "ajin.im is writing"). The writing archive is not in the house: /writes (a running log of thoughts — the essays) and /wrote (the sealed Medium-years comedy archive) are their own Bespoke world — writes.css, a monospace log index + serif reading pages, not creative-house.css — wearing only the Bespoke anchors (a way home, a thread of palette sympathy). The tense pair is load-bearing: is writing (the masked worlds) · writes (the living log) · wrote (the sealed archive); pieces carry no genre labels. Coo issues carry their own title as content. The device tolerates the page's tense/phrasing — a sub-hub may extend its parent's verb, e.g. Small Ware → "ajin.im is building small."
Favicon scheme: a1 = frame hubs (/, running, reading, learning, building); a2 = error/404; a3 = house + all generated pages; bespoke pages carry their own art. (Single 180px PNGs today; full sized-set + manifest deferred.)
Information architecture — what surfaces where
Treat the apex surfaces as curated teasers, not mirrors of what sits below; surface each thing at its grain.
- Home (
/, fromtemplates/root.html) is a balanced teaser. Eachajin.im is ___verb shows exactly two detail lines (running: 2 stats; writing: 2 worlds; building: 2 collections). To feature something new on a verb, swap a line, don't append a third; the two-per-verb symmetry is the device. The building lines show collections only, never loose tools. /is/building= collection cards (container builds) up top + loose single builds under "Lately."- Grain descends: home (2/verb, collections) → section index (collections + "Lately") → hub → piece. Put each thing at its grain: a collection on the home teaser, a loose piece in "Lately," not the reverse.
Build Scripts
_scripts/build_root.py generates the stats-driven pages — index.html (the root ajin.im is … page), is/running/, is/reading/, is/learning/ — from templates/{root,running,reading,learning}.html + content/stats.md. Edit the templates, never the generated index.html files. Direct edits are overwritten on the next build, and .github/workflows/rebuild-root-on-stats.yml rebuilds them automatically whenever a template, stats.md, or the script changes. Each generated file carries a GENERATED FILE — DO NOT EDIT banner under its doctype as an in-file reminder.
_scripts/canary/canary_watcher.py (runtime copy at ~/.local/bin/, LaunchAgent com.ajin.canary-watcher, every 30 min) machine-updates is/building/did-claude-just-reset-usage/state.json ON MASTER via gh api content PUTs. Hand edits to that file may be overwritten within 30 minutes; the page's index.html stays hand-maintained. See _scripts/canary/README.md.
_scripts/build_bird_coo.py generates all Municipal Coo HTML (index, issues, archive). Manual edits to generated files will be overwritten on next build. Make template changes in the build script.
The script also auto-updates the "From the Municipal Coo" excerpt block on is/writing/avian-district/index.html. The block lives between <!-- COO-EXCERPT-START --> and <!-- COO-EXCERPT-END --> markers — manual edits inside that range will be overwritten on next build. Edit the renderer in _scripts/build_bird_coo.py (render_avian_district_excerpt) instead.
The writing archive renders in its own voice — writes.css + _scripts/writes_common.py (the shared reading-page and log-index templates), not creative-house.css. _scripts/build_comedy.py renders comedy pieces into /wrote/<slug>/ (external Medium exports + is/writing/comedy/_src/). _scripts/build_essays.py renders essay pieces into /writes/<slug>/ (from is/writing/essays/_src/) and leaves a redirect stub at the old /wrote/<slug>/. _scripts/build_writes.py renders the living log index /writes/index.html (imports build_essays); _scripts/build_wrote.py renders the sealed comedy index /wrote/index.html (imports build_comedy) — each is the source of truth for its listing, so neither can drift from the pieces on disk. Run order in publish.sh: build_comedy.py → build_essays.py → build_writes.py → build_wrote.py. The old section indexes is/writing/essays/index.html (→ /writes/) and is/writing/comedy/index.html (→ /wrote/) are redirect stubs; do not add piece links by hand. To add an essay: drop its markdown in is/writing/essays/_src/ and that's it — the directory is the registry. Every *.md in it is an essay; _-prefixed files are notes and are skipped. Front matter needs title: and a unique order: (ascending, 0 = newest — duplicates fail the build loudly rather than silently guessing); date: (year only, e.g. 2026) and excerpt: are optional. There is no second list to keep in sync: POST_DEFS and the Medium-export scaffolding retired 2026-09-16. For a hand-built bespoke essay that lives at /writes/<slug>/ (rendered by hand, not from _src/), add it to build_writes.BESPOKE_ESSAYS. Essays normally arrive on their own: _scripts/writes_publisher/ watches an Obsidian drop folder and publishes anything marked publish: true, assigning order itself — so if you are hand-editing _src/ while that is armed, read _scripts/writes_publisher/README.md first (it refuses to publish over a hand-edited source, and refuses entirely if this repo has uncommitted or unpushed work).
CSS / Layout Changes
Any change to padding, margin, width, max-width, grid, or flexbox layout requires verification at three widths before committing:
- Mobile (375px) — check nothing overflows or misaligns
- Tablet (~768px) — check breakpoint transitions
- Desktop (1280px+) — check content is properly constrained
Before adding padding or margin to any element, check if that selector appears in a @media block — mobile overrides will stack with your change and cause double-padding or misalignment.
When changing a container's width or padding, inspect the computed width of the changed element AND the elements visually adjacent to it (above/below) to confirm they still align.
Cross-Site Links
- Use root-relative paths (
/is/writing/nest-court/) not relative (../nest-court/) - The pre-push hook enforces root-relative paths for structural links
- New tabs are global, by design.
/analytics.js(loaded on every page) runs aretarget()on load that setstarget="_blank"on every<a href>except in-page#anchorsand links that already declare a target. So virtually every link on ajin.im opens in a new tab — internal page-to-page navigation included, not just cross-site/external links. This is intentional: the current page stays open when you click out.- You don't need to hand-add
target="_blank"— analytics.js does it. Adding it explicitly (e.g. on a cross-site link) is a harmless fallback; analytics.js skips links that already have a target. - Don't infer tab behavior from a page's own markup — it's overridden at runtime. To verify, inspect the rendered DOM (read
a.target), not the source. - Don't put an "opens in new tab" marker (e.g. a ↗) on only some links — since all links open new tabs, a per-link arrow is misleading; at most it can distinguish off-site (different domain) from on-site.
- You don't need to hand-add
AMD Tooling
_scripts/bird-universe/ contains three tools that automate cross-reference checks
when working on Avian Municipal District pages. Read _scripts/bird-universe/README.md
for full details. Quick reference:
generate_graph.py— producesbird_universe_graph.json, a precomputed cross-reference cache (sites + characters + cases, with reverse-indexmentionsper entity). Runs frompublish.shbefore other build scripts. When you need to know "where is X mentioned?" or "is this case in the registry?", read this file instead of re-greping HTML and re-reading02-registry.md. Path to the registry repo is configured in_scripts/bird-universe/config.json.check_bird_universe_links.py— pre-push validator. In addition to registry/structural rules, now also verifies that every<a href="#X">in bird-universe pages has a matchingid="X"on the same page. SVG internal references (<textPath>,<use>,<clipPath>) are intentionally not checked.lint_identifiers.py— pre-push warning system. Scans HTML for case-number patterns (AMNC-YYYY-NNN[A-Z]) and form-ID patterns (CL-XX-NN). Errors on case-format typos; warns on identifiers in HTML that aren't in the registry yet. Also warns whenbird_universe_graph.jsonis stale relative to02-registry.md(rebuild withgenerate_graph.py). Character-name linting is intentionally deferred — single-token names like "Conrad" / "Dennis" collide with ordinary English.
Metro Cards (was: World Metros Atlas)
A bespoke build at /is/building/metro-match/ (soft-launched, noindex; URL
moved from world-metros at D33, stub redirects; the
atlas-era prototype is live and gets rebuilt as a card deck per DECISIONS
D17–D19). Before touching anything metro-related, read
_scripts/world_metros/README.md — reading order, product contract, data
contract, and the decision log live there. Open forks need owner ratification
before frontend work starts (see _scripts/world_metros/STATUS.md).